feat(storage): add internal/storage/driver for release v2 (#32667)

* feat(storage): add internal/storage/driver for release v2

Copy of pkg/storage/driver operating on internal/release/v2, with the
storage object Type field set to helm.sh/release.v2. The
sh.helm.release.v1 name prefix is unchanged.

Signed-off-by: Evans Mungai <mbuevans@gmail.com>

* feat(storage): mark configmap release schema with a label

ConfigMaps have no Type field to record it in, unlike Secrets. Absence
of the label means v1. Also documents why the sh.helm.release.v1 name
prefix is decoupled from Type.

Signed-off-by: Evans Mungai <mbuevans@gmail.com>

* Update internal/storage/driver/sql.go

Co-authored-by: Terry Howe <terrylhowe@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>

* Update test

Signed-off-by: Evans Mungai <mbuevans@gmail.com>

* PR comment fixes

- Add comment to explain sql driver update behaviour when it comes to updating custom labels
- Apply custom labels in memory driver

Signed-off-by: Evans Mungai <mbuevans@gmail.com>

* Update internal/storage/driver/records.go

Co-authored-by: Terry Howe <terrylhowe@gmail.com>
Signed-off-by: Evans Mungai <mbuevans@gmail.com>

---------

Signed-off-by: Evans Mungai <mbuevans@gmail.com>
Co-authored-by: Terry Howe <terrylhowe@gmail.com>
pull/32676/head
Evans Mungai 1 week ago committed by GitHub
parent 7fd3c5e8f2
commit 6f5c2f5f72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,290 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"context"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kblabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"helm.sh/helm/v4/internal/logging"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
)
var _ Driver = (*ConfigMaps)(nil)
// ConfigMapsDriverName is the string name of the driver.
const ConfigMapsDriverName = "ConfigMap"
// ConfigMaps is a wrapper around an implementation of a kubernetes
// ConfigMapsInterface.
type ConfigMaps struct {
impl corev1.ConfigMapInterface
// Embed a LogHolder to provide logger functionality
logging.LogHolder
}
// NewConfigMaps initializes a new ConfigMaps wrapping an implementation of
// the kubernetes ConfigMapsInterface.
func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps {
c := &ConfigMaps{
impl: impl,
}
c.SetLogger(slog.Default().Handler())
return c
}
// Name returns the name of the driver.
func (cfgmaps *ConfigMaps) Name() string {
return ConfigMapsDriverName
}
// Get fetches the release named by key. The corresponding release is returned
// or error if not found.
func (cfgmaps *ConfigMaps) Get(key string) (release.Releaser, error) {
// fetch the configmap holding the release named by key
obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrReleaseNotFound
}
cfgmaps.Logger().Debug("failed to get release", slog.String("key", key), slog.Any("error", err))
return nil, err
}
// found the configmap, decode the base64 data string
r, err := decodeRelease(obj.Data["release"])
if err != nil {
cfgmaps.Logger().Debug("failed to decode data", slog.String("key", key), slog.Any("error", err))
return nil, err
}
r.Labels = filterSystemLabels(obj.Labels)
// return the release object
return r, nil
}
// List fetches all releases and returns the list releases such
// that filter(release) == true. An error is returned if the
// configmap fails to retrieve the releases.
func (cfgmaps *ConfigMaps) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
lsel := kblabels.Set{"owner": "helm"}.AsSelector()
opts := metav1.ListOptions{LabelSelector: lsel.String()}
list, err := cfgmaps.impl.List(context.Background(), opts)
if err != nil {
cfgmaps.Logger().Debug("failed to list releases", slog.Any("error", err))
return nil, err
}
var results []release.Releaser
// iterate over the configmaps object list
// and decode each release
for _, item := range list.Items {
rls, err := decodeRelease(item.Data["release"])
if err != nil {
cfgmaps.Logger().Debug("failed to decode release", slog.Any("item", item), slog.Any("error", err))
continue
}
rls.Labels = item.Labels
if filter(rls) {
results = append(results, rls)
}
}
return results, nil
}
// Query fetches all releases that match the provided map of labels.
// An error is returned if the configmap fails to retrieve the releases.
func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]release.Releaser, error) {
ls := kblabels.Set{}
for k, v := range labels {
if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
return nil, fmt.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; "))
}
ls[k] = v
}
opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()}
list, err := cfgmaps.impl.List(context.Background(), opts)
if err != nil {
cfgmaps.Logger().Debug("failed to query with labels", slog.Any("error", err))
return nil, err
}
if len(list.Items) == 0 {
return nil, ErrReleaseNotFound
}
var results []release.Releaser
for _, item := range list.Items {
rls, err := decodeRelease(item.Data["release"])
if err != nil {
cfgmaps.Logger().Debug("failed to decode release", slog.Any("error", err))
continue
}
rls.Labels = item.Labels
results = append(results, rls)
}
return results, nil
}
// Create creates a new ConfigMap holding the release. If the
// ConfigMap already exists, ErrReleaseExists is returned.
func (cfgmaps *ConfigMaps) Create(key string, rls release.Releaser) error {
// set labels for configmaps object meta data
var lbs labels
rac, err := release.NewAccessor(rls)
if err != nil {
return err
}
lbs.init()
lbs.fromMap(rac.Labels())
lbs.set("createdAt", strconv.FormatInt(time.Now().Unix(), 10))
rel, err := releaserToV1Release(rls)
if err != nil {
return err
}
// create a new configmap to hold the release
obj, err := newConfigMapsObject(key, rel, lbs)
if err != nil {
cfgmaps.Logger().Debug("failed to encode release", slog.String("name", rac.Name()), slog.Any("error", err))
return err
}
// push the configmap object out into the kubiverse
if _, err := cfgmaps.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil {
if apierrors.IsAlreadyExists(err) {
return ErrReleaseExists
}
cfgmaps.Logger().Debug("failed to create release", slog.Any("error", err))
return err
}
return nil
}
// Update updates the ConfigMap holding the release. If not found
// the ConfigMap is created to hold the release.
func (cfgmaps *ConfigMaps) Update(key string, rel release.Releaser) error {
// set labels for configmaps object meta data
var lbs labels
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
lbs.init()
lbs.fromMap(rls.Labels)
lbs.set("modifiedAt", strconv.FormatInt(time.Now().Unix(), 10))
// create a new configmap object to hold the release
obj, err := newConfigMapsObject(key, rls, lbs)
if err != nil {
cfgmaps.Logger().Debug(
"failed to encode release",
slog.String("name", rls.Name),
slog.Any("error", err),
)
return err
}
// push the configmap object out into the kubiverse
_, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{})
if err != nil {
cfgmaps.Logger().Debug("failed to update release", slog.Any("error", err))
return err
}
return nil
}
// Delete deletes the ConfigMap holding the release named by key.
func (cfgmaps *ConfigMaps) Delete(key string) (rls release.Releaser, err error) {
// fetch the release to check existence
if rls, err = cfgmaps.Get(key); err != nil {
return nil, err
}
// delete the release
if err := cfgmaps.impl.Delete(context.Background(), key, metav1.DeleteOptions{}); err != nil {
return rls, err
}
return rls, nil
}
// newConfigMapsObject constructs a kubernetes ConfigMap object
// to store a release. Each configmap data entry is the base64
// encoded gzipped string of a release.
//
// The following labels are used within each configmap:
//
// "modifiedAt" - timestamp indicating when this configmap was last modified. (set in Update)
// "createdAt" - timestamp indicating when this configmap was created. (set in Create)
// "version" - version of the release.
// "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"
// encode the release
s, err := encodeRelease(rls)
if err != nil {
return nil, err
}
if lbs == nil {
lbs.init()
}
// apply custom labels
lbs.fromMap(rls.Labels)
// apply labels
lbs.set("name", rls.Name)
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{
ObjectMeta: metav1.ObjectMeta{
Name: key,
Labels: lbs.toMap(),
},
Data: map[string]string{"release": s},
}, nil
}

@ -0,0 +1,231 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"encoding/base64"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
)
func TestConfigMapName(t *testing.T) {
c := newTestFixtureCfgMaps(t)
assert.Equal(t, ConfigMapsDriverName, c.Name(), "Expected name to be %q, got %q", ConfigMapsDriverName, c.Name())
}
func TestConfigMapGet(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)
// get release with key
got, err := cfgmaps.Get(key)
require.NoError(t, err, "Failed to get release")
// compare fetched release with original
assert.Equalf(t, rel, got, "Expected {%v}, got {%v}", rel, got)
}
func TestUncompressedConfigMapGet(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// Create a test fixture which contains an uncompressed release
cfgmap, err := newConfigMapsObject(key, rel, nil)
require.NoError(t, err, "Failed to create configmap")
b, err := json.Marshal(rel)
require.NoError(t, err, "Failed to marshal release")
cfgmap.Data["release"] = base64.StdEncoding.EncodeToString(b)
var mock MockConfigMapsInterface
mock.objects = map[string]*v1.ConfigMap{key: cfgmap}
cfgmaps := NewConfigMaps(&mock)
// get release with key
got, err := cfgmaps.Get(key)
require.NoError(t, err, "Failed to get release")
// compare fetched release with original
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) {
case rspb.Release:
return &r
case *rspb.Release:
return r
case nil:
return nil
}
t.Fatalf("Unsupported release type: %T", rel)
return nil
}
func TestConfigMapList(t *testing.T) {
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{
releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...)
// list all deleted releases
del, err := cfgmaps.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
})
// check
require.NoError(t, err, "Failed to list deleted")
assert.Len(t, del, 2, "Expected 2 deleted")
// list all deployed releases
dpl, err := cfgmaps.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
})
// check
require.NoError(t, err, "Failed to list deployed")
assert.Len(t, dpl, 2, "Expected 2 deployed")
// list all superseded releases
ssd, err := cfgmaps.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
})
// check
require.NoError(t, err, "Failed to list superseded")
assert.Len(t, ssd, 2, "Expected 2 superseded")
// Check if release having both system and custom labels, this is needed to ensure that selector filtering would work.
rls := convertReleaserToV1(t, ssd[0])
require.Contains(t, rls.Labels, "name", "Expected 'name' label in results, actual %v", rls.Labels)
require.Contains(t, rls.Labels, "key1", "Expected 'key1' label in results, actual %v", rls.Labels)
}
func TestConfigMapQuery(t *testing.T) {
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{
releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...)
rls, err := cfgmaps.Query(map[string]string{"status": "deployed"})
require.NoError(t, err, "Failed to query")
assert.Len(t, rls, 2, "Expected 2 results")
_, err = cfgmaps.Query(map[string]string{"name": "notExist"})
assert.ErrorIs(t, err, ErrReleaseNotFound)
}
func TestConfigMapCreate(t *testing.T) {
cfgmaps := newTestFixtureCfgMaps(t)
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// store the release in a configmap
require.NoErrorf(t, cfgmaps.Create(key, rel), "Failed to create release with key %q", key)
// get the release back
got, err := cfgmaps.Get(key)
require.NoError(t, err, "Failed to get release with key %q", key)
// compare created release with original
assert.Equalf(t, rel, got, "Expected {%v}, got {%v}", rel, got)
}
func TestConfigMapUpdate(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)
// modify release status code
rel.Info.Status = common.StatusSuperseded
// perform the update
require.NoErrorf(t, cfgmaps.Update(key, rel), "Failed to update release")
// fetch the updated release
goti, err := cfgmaps.Get(key)
require.NoError(t, err, "Failed to get release with key %q", key)
got := convertReleaserToV1(t, goti)
// check release has actually been updated by comparing modified fields
assert.Equal(t, got.Info.Status, rel.Info.Status, "Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String())
}
func TestConfigMapDelete(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)
// perform the delete on a non-existent release
_, err := cfgmaps.Delete("nonexistent")
require.ErrorIs(t, err, ErrReleaseNotFound)
// perform the delete
rls, err := cfgmaps.Delete(key)
require.NoError(t, err, "Failed to delete release with key %q", key)
assert.Equalf(t, rel, rls, "Expected {%v}, got {%v}", rel, rls)
_, err = cfgmaps.Get(key)
assert.ErrorIs(t, err, ErrReleaseNotFound)
}

@ -0,0 +1,120 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"errors"
"fmt"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
)
var (
// ErrReleaseNotFound indicates that a release is not found.
ErrReleaseNotFound = errors.New("release: not found")
// ErrReleaseExists indicates that a release already exists.
ErrReleaseExists = errors.New("release: already exists")
// ErrInvalidKey indicates that a release key could not be parsed.
ErrInvalidKey = errors.New("release: invalid key")
// ErrNoDeployedReleases indicates that there are no releases with the given key in the deployed state
ErrNoDeployedReleases = errors.New("has no deployed releases")
)
// StorageDriverError records an error and the release name that caused it
type StorageDriverError struct {
ReleaseName string
Err error
}
func (e *StorageDriverError) Error() string {
return fmt.Sprintf("%q %s", e.ReleaseName, e.Err.Error())
}
func (e *StorageDriverError) Unwrap() error { return e.Err }
func NewErrNoDeployedReleases(releaseName string) error {
return &StorageDriverError{
ReleaseName: releaseName,
Err: ErrNoDeployedReleases,
}
}
// Creator is the interface that wraps the Create method.
//
// Create stores the release or returns ErrReleaseExists
// if an identical release already exists.
type Creator interface {
Create(key string, rls release.Releaser) error
}
// Updator is the interface that wraps the Update method.
//
// Update updates an existing release or returns
// ErrReleaseNotFound if the release does not exist.
type Updator interface {
Update(key string, rls release.Releaser) error
}
// Deletor is the interface that wraps the Delete method.
//
// Delete deletes the release named by key or returns
// ErrReleaseNotFound if the release does not exist.
type Deletor interface {
Delete(key string) (release.Releaser, error)
}
// Queryor is the interface that wraps the Get and List methods.
//
// Get returns the release named by key or returns ErrReleaseNotFound
// if the release does not exist.
//
// List returns the set of all releases that satisfy the filter predicate.
//
// Query returns the set of all releases that match the provided label set.
type Queryor interface {
Get(key string) (release.Releaser, error)
List(filter func(release.Releaser) bool) ([]release.Releaser, error)
Query(labels map[string]string) ([]release.Releaser, error)
}
// Driver is the interface composed of Creator, Updator, Deletor, and Queryor
// interfaces. It defines the behavior for storing, updating, deleted,
// and retrieving Helm releases from some underlying storage mechanism,
// e.g. memory, configmaps.
type Driver interface {
Creator
Updator
Deletor
Queryor
Name() string
}
// releaserToV1Release is a helper function to convert a v1 release passed by interface
// into the type object.
func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) {
switch r := rel.(type) {
case rspb.Release:
return &r, nil
case *rspb.Release:
return r, nil
case nil:
return nil, nil
default:
return nil, fmt.Errorf("unsupported release type: %T", rel)
}
}

@ -0,0 +1,48 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
// labels is a map of key value pairs to be included as metadata in a configmap object.
type labels map[string]string
func (lbs *labels) init() { *lbs = labels(make(map[string]string)) }
func (lbs labels) get(key string) string { return lbs[key] }
func (lbs labels) set(key, val string) { lbs[key] = val }
func (lbs labels) keys() (ls []string) {
for key := range lbs {
ls = append(ls, key)
}
return ls
}
func (lbs labels) match(set labels) bool {
for _, key := range set.keys() {
if lbs.get(key) != set.get(key) {
return false
}
}
return true
}
func (lbs labels) toMap() map[string]string { return lbs }
func (lbs *labels) fromMap(kvs map[string]string) {
for k, v := range kvs {
lbs.set(k, v)
}
}

@ -0,0 +1,55 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestLabelsMatch(t *testing.T) {
tests := []struct {
desc string
set1 labels
set2 labels
expect bool
}{
{
"equal labels sets",
labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}),
labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}),
true,
},
{
"disjoint label sets",
labels(map[string]string{"KEY_C": "VAL_C", "KEY_D": "VAL_D"}),
labels(map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"}),
false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
if tt.expect {
require.True(t, tt.set1.match(tt.set2))
} else {
require.False(t, tt.set1.match(tt.set2))
}
})
}
}

@ -0,0 +1,255 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"log/slog"
"strconv"
"strings"
"sync"
"helm.sh/helm/v4/internal/logging"
"helm.sh/helm/v4/pkg/release"
)
var _ Driver = (*Memory)(nil)
const (
// MemoryDriverName is the string name of this driver.
MemoryDriverName = "Memory"
defaultNamespace = "default"
)
// A map of release names to list of release records
type memReleases map[string]records
// Memory is the in-memory storage driver implementation.
type Memory struct {
sync.RWMutex
namespace string
// A map of namespaces to releases
cache map[string]memReleases
// Embed a LogHolder to provide logger functionality
logging.LogHolder
}
// NewMemory initializes a new memory driver.
func NewMemory() *Memory {
m := &Memory{cache: map[string]memReleases{}, namespace: "default"}
m.SetLogger(slog.Default().Handler())
return m
}
// SetNamespace sets a specific namespace in which releases will be accessed.
// An empty string indicates all namespaces (for the list operation)
func (mem *Memory) SetNamespace(ns string) {
mem.namespace = ns
}
// Name returns the name of the driver.
func (mem *Memory) Name() string {
return MemoryDriverName
}
// Get returns the release named by key or returns ErrReleaseNotFound.
func (mem *Memory) Get(key string) (release.Releaser, error) {
defer unlock(mem.rlock())
keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.")
switch elems := strings.Split(keyWithoutPrefix, ".v"); len(elems) {
case 2:
name, ver := elems[0], elems[1]
if _, err := strconv.Atoi(ver); err != nil {
return nil, ErrInvalidKey
}
if recs, ok := mem.cache[mem.namespace][name]; ok {
if r := recs.Get(key); r != nil {
return r.rls, nil
}
}
return nil, ErrReleaseNotFound
default:
return nil, ErrInvalidKey
}
}
// List returns the list of all releases such that filter(release) == true
func (mem *Memory) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
defer unlock(mem.rlock())
var ls []release.Releaser
for namespace := range mem.cache {
if mem.namespace != "" {
// Should only list releases of this namespace
namespace = mem.namespace
}
for _, recs := range mem.cache[namespace] {
recs.Iter(func(_ int, rec *record) bool {
if filter(rec.rls) {
ls = append(ls, rec.rls)
}
return true
})
}
if mem.namespace != "" {
// Should only list releases of this namespace
break
}
}
return ls, nil
}
// Query returns the set of releases that match the provided set of labels
func (mem *Memory) Query(keyvals map[string]string) ([]release.Releaser, error) {
defer unlock(mem.rlock())
var lbs labels
lbs.init()
lbs.fromMap(keyvals)
var ls []release.Releaser
for namespace := range mem.cache {
if mem.namespace != "" {
// Should only query releases of this namespace
namespace = mem.namespace
}
for _, recs := range mem.cache[namespace] {
recs.Iter(func(_ int, rec *record) bool {
// A query for a release name that doesn't exist (has been deleted)
// can cause rec to be nil.
if rec == nil {
return false
}
if rec.lbs.match(lbs) {
ls = append(ls, rec.rls)
}
return true
})
}
if mem.namespace != "" {
// Should only query releases of this namespace
break
}
}
if len(ls) == 0 {
return nil, ErrReleaseNotFound
}
return ls, nil
}
// Create creates a new release or returns ErrReleaseExists.
func (mem *Memory) Create(key string, rel release.Releaser) error {
defer unlock(mem.wlock())
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
// For backwards compatibility, we protect against an unset namespace
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
mem.SetNamespace(namespace)
if _, ok := mem.cache[namespace]; !ok {
mem.cache[namespace] = memReleases{}
}
if recs, ok := mem.cache[namespace][rls.Name]; ok {
if err := recs.Add(newRecord(key, rls)); err != nil {
return err
}
mem.cache[namespace][rls.Name] = recs
return nil
}
mem.cache[namespace][rls.Name] = records{newRecord(key, rls)}
return nil
}
// Update updates a release or returns ErrReleaseNotFound.
func (mem *Memory) Update(key string, rel release.Releaser) error {
defer unlock(mem.wlock())
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
// For backwards compatibility, we protect against an unset namespace
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
mem.SetNamespace(namespace)
if _, ok := mem.cache[namespace]; ok {
if rs, ok := mem.cache[namespace][rls.Name]; ok && rs.Exists(key) {
rs.Replace(key, newRecord(key, rls))
return nil
}
}
return ErrReleaseNotFound
}
// Delete deletes a release or returns ErrReleaseNotFound.
func (mem *Memory) Delete(key string) (release.Releaser, error) {
defer unlock(mem.wlock())
keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.")
elems := strings.Split(keyWithoutPrefix, ".v")
if len(elems) != 2 {
return nil, ErrInvalidKey
}
name, ver := elems[0], elems[1]
if _, err := strconv.Atoi(ver); err != nil {
return nil, ErrInvalidKey
}
if _, ok := mem.cache[mem.namespace]; ok {
if recs, ok := mem.cache[mem.namespace][name]; ok {
if r := recs.Remove(key); r != nil {
// recs.Remove changes the slice reference, so we have to re-assign it.
mem.cache[mem.namespace][name] = recs
return r.rls, nil
}
}
}
return nil, ErrReleaseNotFound
}
// wlock locks mem for writing
func (mem *Memory) wlock() func() {
mem.Lock()
return func() { mem.Unlock() }
}
// rlock locks mem for reading
func (mem *Memory) rlock() func() {
mem.RLock()
return func() { mem.RUnlock() }
}
// unlock calls fn which reverses a mem.rlock or mem.wlock. e.g:
// ```defer unlock(mem.rlock())```, locks mem for reading at the
// call point of defer and unlocks upon exiting the block.
func unlock(fn func()) { fn() }

@ -0,0 +1,266 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
)
func TestMemoryName(t *testing.T) {
mem := NewMemory()
assert.Equalf(t, MemoryDriverName, mem.Name(), "Expected name to be %q, got %q", MemoryDriverName, mem.Name())
}
func TestMemoryCreate(t *testing.T) {
tests := []struct {
desc string
rls *rspb.Release
err bool
}{
{
"create should succeed",
releaseStub("rls-c", 1, "default", common.StatusDeployed),
false,
},
{
"create should fail (release already exists)",
releaseStub("rls-a", 1, "default", common.StatusDeployed),
true,
},
{
"create in namespace should succeed",
releaseStub("rls-a", 1, "mynamespace", common.StatusDeployed),
false,
},
{
"create in other namespace should fail (release already exists)",
releaseStub("rls-c", 1, "mynamespace", common.StatusDeployed),
true,
},
}
ts := tsFixtureMemory(t)
for _, tt := range tests {
key := testKey(tt.rls.Name, tt.rls.Version)
rls := tt.rls
err := ts.Create(key, rls)
if tt.err {
require.Error(t, err, "Did not get expected error for %q\n", tt.desc)
} else {
require.NoError(t, err, "failed to create %q", tt.desc)
}
}
}
func TestMemoryGet(t *testing.T) {
tests := []struct {
desc string
key string
namespace string
err bool
}{
{"release key should exist", "rls-a.v1", "default", false},
{"release key should not exist", "rls-a.v5", "default", true},
{"release key in namespace should exist", "rls-c.v1", "mynamespace", false},
{"release key in namespace should not exist", "rls-a.v1", "mynamespace", true},
}
ts := tsFixtureMemory(t)
for _, tt := range tests {
ts.SetNamespace(tt.namespace)
_, err := ts.Get(tt.key)
if tt.err {
require.Error(t, err, "Did not get expected error for %q '%s'\n", tt.desc, tt.key)
} else {
require.NoError(t, err, "Failed %q to get '%s'", tt.desc, tt.key)
}
}
}
func TestMemoryList(t *testing.T) {
ts := tsFixtureMemory(t)
ts.SetNamespace("default")
// list all deployed releases
dpl, err := ts.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
})
// check
require.NoError(t, err, "Failed to list deployed releases")
assert.Len(t, dpl, 2, "Expected 2 deployed")
// list all superseded releases
ssd, err := ts.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
})
// check
require.NoError(t, err, "Failed to list superseded releases")
assert.Len(t, ssd, 6, "Expected 6 superseded")
// list all deleted releases
del, err := ts.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
})
// check
require.NoError(t, err, "Failed to list deleted releases")
assert.Empty(t, del, "Expected 0 deleted, got %d", len(del))
}
func TestMemoryQuery(t *testing.T) {
tests := []struct {
desc string
xlen int
namespace string
lbs map[string]string
}{
{
"should be 2 query results",
2,
"default",
map[string]string{"status": "deployed"},
},
{
"should be 1 query result",
1,
"mynamespace",
map[string]string{"status": "deployed"},
},
}
ts := tsFixtureMemory(t)
for _, tt := range tests {
ts.SetNamespace(tt.namespace)
l, err := ts.Query(tt.lbs)
require.NoError(t, err, "Failed to query")
require.Equal(t, len(l), tt.xlen, "Expected %d results, actual %d\n", tt.xlen, len(l))
}
}
func TestMemoryUpdate(t *testing.T) {
tests := []struct {
desc string
key string
rls *rspb.Release
err bool
}{
{
"update release status",
"rls-a.v4",
releaseStub("rls-a", 4, "default", common.StatusSuperseded),
false,
},
{
"update release does not exist",
"rls-c.v1",
releaseStub("rls-c", 1, "default", common.StatusUninstalled),
true,
},
{
"update release status in namespace",
"rls-c.v4",
releaseStub("rls-c", 4, "mynamespace", common.StatusSuperseded),
false,
},
{
"update release in namespace does not exist",
"rls-a.v1",
releaseStub("rls-a", 1, "mynamespace", common.StatusUninstalled),
true,
},
}
ts := tsFixtureMemory(t)
for _, tt := range tests {
err := ts.Update(tt.key, tt.rls)
if tt.err {
require.Error(t, err, "Did not get expected error for %q '%s'\n", tt.desc, tt.key)
} else {
require.NoError(t, err, "Failed %q", tt.desc)
ts.SetNamespace(tt.rls.Namespace)
r, err := ts.Get(tt.key)
require.NoError(t, err, "Failed to get")
require.Equalf(t, r, tt.rls, "Expected %v, actual %v\n", tt.rls, r)
}
}
}
func TestMemoryDelete(t *testing.T) {
tests := []struct {
desc string
key string
namespace string
err bool
}{
{"release key should exist", "rls-a.v4", "default", false},
{"release key should not exist", "rls-a.v5", "default", true},
{"release key from other namespace should not exist", "rls-c.v4", "default", true},
{"release key from namespace should exist", "rls-c.v4", "mynamespace", false},
{"release key from namespace should not exist", "rls-c.v5", "mynamespace", true},
{"release key from namespace2 should not exist", "rls-a.v4", "mynamespace", true},
}
ts := tsFixtureMemory(t)
ts.SetNamespace("")
start, err := ts.Query(map[string]string{"status": "deployed"})
require.NoError(t, err, "Query failed")
startLen := len(start)
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ts.SetNamespace(tt.namespace)
rel, err := ts.Delete(tt.key)
if tt.err {
require.Errorf(t, err, "Did not get expected error for %q '%s'\n", tt.desc, tt.key)
} else {
require.NoErrorf(t, err, "Failed %q to get '%s'", tt.desc, tt.key)
rls := convertReleaserToV1(t, rel)
require.Equalf(t, tt.key, fmt.Sprintf("%s.v%d", rls.Name, rls.Version), "Asked for delete on %s, but deleted %d", tt.key, rls.Version)
}
_, err = ts.Get(tt.key)
require.Error(t, err, "Expected an error when asking for a deleted key")
})
}
// Make sure that the deleted records are gone.
ts.SetNamespace("")
end, err := ts.Query(map[string]string{"status": "deployed"})
require.NoError(t, err, "Query failed")
if !assert.Len(t, end, startLen-2) {
for _, ee := range end {
rac, err := release.NewAccessor(ee)
require.NoError(t, err, "unable to get release accessor")
t.Logf("Name: %s, Version: %d", rac.Name(), rac.Version())
}
}
}

@ -0,0 +1,267 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"context"
"fmt"
"testing"
sqlmock "github.com/DATA-DOG/go-sqlmock"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kblabels "k8s.io/apimachinery/pkg/labels"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release/common"
)
func releaseStub(name string, vers int, namespace string, status common.Status) *rspb.Release {
return &rspb.Release{
Name: name,
Version: vers,
Namespace: namespace,
Info: &rspb.Info{Status: status},
Labels: map[string]string{
"key1": "val1",
"key2": "val2",
},
}
}
func testKey(name string, vers int) string {
return fmt.Sprintf("%s.v%d", name, vers)
}
func tsFixtureMemory(t *testing.T) *Memory {
t.Helper()
hs := []*rspb.Release{
// rls-a
releaseStub("rls-a", 4, "default", common.StatusDeployed),
releaseStub("rls-a", 1, "default", common.StatusSuperseded),
releaseStub("rls-a", 3, "default", common.StatusSuperseded),
releaseStub("rls-a", 2, "default", common.StatusSuperseded),
// rls-b
releaseStub("rls-b", 4, "default", common.StatusDeployed),
releaseStub("rls-b", 1, "default", common.StatusSuperseded),
releaseStub("rls-b", 3, "default", common.StatusSuperseded),
releaseStub("rls-b", 2, "default", common.StatusSuperseded),
// rls-c in other namespace
releaseStub("rls-c", 4, "mynamespace", common.StatusDeployed),
releaseStub("rls-c", 1, "mynamespace", common.StatusSuperseded),
releaseStub("rls-c", 3, "mynamespace", common.StatusSuperseded),
releaseStub("rls-c", 2, "mynamespace", common.StatusSuperseded),
}
mem := NewMemory()
for _, tt := range hs {
require.NoError(t, mem.Create(testKey(tt.Name, tt.Version), tt), "Test setup failed to create")
}
return mem
}
// newTestFixtureCfgMaps initializes a MockConfigMapsInterface.
// ConfigMaps are created for each release provided.
func newTestFixtureCfgMaps(t *testing.T, releases ...*rspb.Release) *ConfigMaps {
t.Helper()
var mock MockConfigMapsInterface
mock.Init(t, releases...)
return NewConfigMaps(&mock)
}
// MockConfigMapsInterface mocks a kubernetes ConfigMapsInterface
type MockConfigMapsInterface struct {
corev1.ConfigMapInterface
objects map[string]*v1.ConfigMap
}
// Init initializes the MockConfigMapsInterface with the set of releases.
func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) {
t.Helper()
mock.objects = map[string]*v1.ConfigMap{}
for _, rls := range releases {
objkey := testKey(rls.Name, rls.Version)
cfgmap, err := newConfigMapsObject(objkey, rls, nil)
require.NoError(t, err, "Failed to create configmap")
mock.objects[objkey] = cfgmap
}
}
// Get returns the ConfigMap by name.
func (mock *MockConfigMapsInterface) Get(_ context.Context, name string, _ metav1.GetOptions) (*v1.ConfigMap, error) {
object, ok := mock.objects[name]
if !ok {
return nil, apierrors.NewNotFound(v1.Resource("tests"), name)
}
return object, nil
}
// List returns all ConfigMaps.
func (mock *MockConfigMapsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.ConfigMapList, error) {
var list v1.ConfigMapList
labelSelector, err := kblabels.Parse(opts.LabelSelector)
if err != nil {
return nil, err
}
for _, cfgmap := range mock.objects {
if labelSelector.Matches(kblabels.Set(cfgmap.Labels)) {
list.Items = append(list.Items, *cfgmap)
}
}
return &list, nil
}
// Create creates a new ConfigMap.
func (mock *MockConfigMapsInterface) Create(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.CreateOptions) (*v1.ConfigMap, error) {
name := cfgmap.Name
if object, ok := mock.objects[name]; ok {
return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name)
}
mock.objects[name] = cfgmap
return cfgmap, nil
}
// Update updates a ConfigMap.
func (mock *MockConfigMapsInterface) Update(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.UpdateOptions) (*v1.ConfigMap, error) {
name := cfgmap.Name
if _, ok := mock.objects[name]; !ok {
return nil, apierrors.NewNotFound(v1.Resource("tests"), name)
}
mock.objects[name] = cfgmap
return cfgmap, nil
}
// Delete deletes a ConfigMap by name.
func (mock *MockConfigMapsInterface) Delete(_ context.Context, name string, _ metav1.DeleteOptions) error {
if _, ok := mock.objects[name]; !ok {
return apierrors.NewNotFound(v1.Resource("tests"), name)
}
delete(mock.objects, name)
return nil
}
// newTestFixtureSecrets initializes a MockSecretsInterface.
// Secrets are created for each release provided.
func newTestFixtureSecrets(t *testing.T, releases ...*rspb.Release) *Secrets {
t.Helper()
var mock MockSecretsInterface
mock.Init(t, releases...)
return NewSecrets(&mock)
}
// MockSecretsInterface mocks a kubernetes SecretsInterface
type MockSecretsInterface struct {
corev1.SecretInterface
objects map[string]*v1.Secret
}
// Init initializes the MockSecretsInterface with the set of releases.
func (mock *MockSecretsInterface) Init(t *testing.T, releases ...*rspb.Release) {
t.Helper()
mock.objects = map[string]*v1.Secret{}
for _, rls := range releases {
objkey := testKey(rls.Name, rls.Version)
secret, err := newSecretsObject(objkey, rls, nil)
require.NoError(t, err, "Failed to create secret")
mock.objects[objkey] = secret
}
}
// Get returns the Secret by name.
func (mock *MockSecretsInterface) Get(_ context.Context, name string, _ metav1.GetOptions) (*v1.Secret, error) {
object, ok := mock.objects[name]
if !ok {
return nil, apierrors.NewNotFound(v1.Resource("tests"), name)
}
return object, nil
}
// List returns all Secrets.
func (mock *MockSecretsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.SecretList, error) {
var list v1.SecretList
labelSelector, err := kblabels.Parse(opts.LabelSelector)
if err != nil {
return nil, err
}
for _, secret := range mock.objects {
if labelSelector.Matches(kblabels.Set(secret.Labels)) {
list.Items = append(list.Items, *secret)
}
}
return &list, nil
}
// Create creates a new Secret.
func (mock *MockSecretsInterface) Create(_ context.Context, secret *v1.Secret, _ metav1.CreateOptions) (*v1.Secret, error) {
name := secret.Name
if object, ok := mock.objects[name]; ok {
return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name)
}
mock.objects[name] = secret
return secret, nil
}
// Update updates a Secret.
func (mock *MockSecretsInterface) Update(_ context.Context, secret *v1.Secret, _ metav1.UpdateOptions) (*v1.Secret, error) {
name := secret.Name
if _, ok := mock.objects[name]; !ok {
return nil, apierrors.NewNotFound(v1.Resource("tests"), name)
}
mock.objects[name] = secret
return secret, nil
}
// Delete deletes a Secret by name.
func (mock *MockSecretsInterface) Delete(_ context.Context, name string, _ metav1.DeleteOptions) error {
if _, ok := mock.objects[name]; !ok {
return apierrors.NewNotFound(v1.Resource("tests"), name)
}
delete(mock.objects, name)
return nil
}
// newTestFixtureSQL mocks the SQL database (for testing purposes)
func newTestFixtureSQL(t *testing.T, _ ...*rspb.Release) (*SQL, sqlmock.Sqlmock) {
t.Helper()
sqlDB, mock, err := sqlmock.New()
require.NoError(t, err, "error when opening stub database connection")
sqlxDB := sqlx.NewDb(sqlDB, "sqlmock")
return &SQL{
db: sqlxDB,
namespace: "default",
statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}, mock
}

@ -0,0 +1,127 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"sort"
"strconv"
rspb "helm.sh/helm/v4/internal/release/v2"
)
// records holds a list of in-memory release records
type records []*record
func (rs records) Len() int { return len(rs) }
func (rs records) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
func (rs records) Less(i, j int) bool { return rs[i].rls.Version < rs[j].rls.Version }
func (rs *records) Add(r *record) error {
if r == nil {
return nil
}
if rs.Exists(r.key) {
return ErrReleaseExists
}
*rs = append(*rs, r)
sort.Sort(*rs)
return nil
}
func (rs records) Get(key string) *record {
if i, ok := rs.Index(key); ok {
return rs[i]
}
return nil
}
func (rs *records) Iter(fn func(int, *record) bool) {
cp := make([]*record, len(*rs))
copy(cp, *rs)
for i, r := range cp {
if !fn(i, r) {
return
}
}
}
func (rs *records) Index(key string) (int, bool) {
for i, r := range *rs {
if r.key == key {
return i, true
}
}
return -1, false
}
func (rs records) Exists(key string) bool {
_, ok := rs.Index(key)
return ok
}
func (rs *records) Remove(key string) (r *record) {
if i, ok := rs.Index(key); ok {
return rs.removeAt(i)
}
return nil
}
func (rs *records) Replace(key string, rec *record) *record {
if i, ok := rs.Index(key); ok {
old := (*rs)[i]
(*rs)[i] = rec
return old
}
return nil
}
func (rs *records) removeAt(index int) *record {
r := (*rs)[index]
(*rs)[index] = nil
copy((*rs)[index:], (*rs)[index+1:])
*rs = (*rs)[:len(*rs)-1]
return r
}
// record is the data structure used to cache releases
// for the in-memory storage driver
type record struct {
key string
lbs labels
rls *rspb.Release
}
// newRecord creates a new in-memory release record
func newRecord(key string, rls *rspb.Release) *record {
var lbs labels
lbs.init()
// apply custom labels
lbs.fromMap(rls.Labels)
lbs.set("name", rls.Name)
lbs.set("owner", "helm")
lbs.set("status", rls.Info.Status.String())
lbs.set("version", strconv.Itoa(rls.Version))
return &record{key: key, lbs: lbs, rls: rls}
}

@ -0,0 +1,226 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/release/common"
)
func TestRecordsAdd(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
})
tests := []struct {
desc string
key string
ok bool
rec *record
}{
{
"add valid key",
"rls-a.v3",
false,
newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)),
},
{
"add already existing key",
"rls-a.v1",
true,
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusDeployed)),
},
}
for _, tt := range tests {
err := rs.Add(tt.rec)
if !tt.ok {
require.NoError(t, err, "failed: %q:", tt.desc)
} else {
require.Error(t, err)
}
}
}
func TestRecordsRemove(t *testing.T) {
tests := []struct {
desc string
key string
ok bool
}{
{"remove valid key", "rls-a.v1", false},
{"remove invalid key", "rls-a.v", true},
{"remove non-existent key", "rls-z.v1", true},
}
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
})
startLen := rs.Len()
for _, tt := range tests {
r := rs.Remove(tt.key)
if tt.ok {
require.Nil(t, r, "Failed to %q (key = %s). Expected nil, got %v", tt.desc, tt.key, r)
} else {
require.NotNil(t, r)
}
}
// We expect the total number of records will be less now than there were
// when we started.
endLen := rs.Len()
assert.Lessf(t, endLen, startLen, "expected ending length %d to be less than starting length %d", endLen, startLen)
}
func TestRecordsRemoveAt(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
})
require.Len(t, rs, 2, "Expected len=2 for mock")
rs.Remove("rls-a.v1")
require.Len(t, rs, 1, "Expected length of rs to be 1, got %d", len(rs))
}
func TestRecordsGet(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
})
tests := []struct {
desc string
key string
rec *record
}{
{
"get valid key",
"rls-a.v1",
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
},
{
"get invalid key",
"rls-a.v3",
nil,
},
}
for _, tt := range tests {
got := rs.Get(tt.key)
require.Equal(t, tt.rec, got, "Expected %v, got %v", tt.rec, got)
}
}
func TestRecordsIndex(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
})
tests := []struct {
desc string
key string
sort int
}{
{
"get valid key",
"rls-a.v1",
0,
},
{
"get invalid key",
"rls-a.v3",
-1,
},
}
for _, tt := range tests {
got, _ := rs.Index(tt.key)
require.Equal(t, tt.sort, got, "Expected %d, got %d", tt.sort, got)
}
}
func TestRecordsExists(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
})
tests := []struct {
desc string
key string
ok bool
}{
{
"get valid key",
"rls-a.v1",
true,
},
{
"get invalid key",
"rls-a.v3",
false,
},
}
for _, tt := range tests {
got := rs.Exists(tt.key)
require.Equal(t, tt.ok, got, "Expected %t, got %t", tt.ok, got)
}
}
func TestRecordsReplace(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
})
tests := []struct {
desc string
key string
rec *record
expected *record
}{
{
"replace with existing key",
"rls-a.v2",
newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)),
},
{
"replace with non existing key",
"rls-a.v4",
newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", common.StatusDeployed)),
nil,
},
}
for _, tt := range tests {
got := rs.Replace(tt.key, tt.rec)
require.Equalf(t, tt.expected, got, "Expected %v, got %v", tt.expected, got)
}
}

@ -0,0 +1,287 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"context"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kblabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"helm.sh/helm/v4/internal/logging"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
)
var _ Driver = (*Secrets)(nil)
// SecretsDriverName is the string name of the driver.
const SecretsDriverName = "Secret"
// Secrets is a wrapper around an implementation of a kubernetes
// SecretsInterface.
type Secrets struct {
impl corev1.SecretInterface
// Embed a LogHolder to provide logger functionality
logging.LogHolder
}
// NewSecrets initializes a new Secrets wrapping an implementation of
// the kubernetes SecretsInterface.
func NewSecrets(impl corev1.SecretInterface) *Secrets {
s := &Secrets{
impl: impl,
}
s.SetLogger(slog.Default().Handler())
return s
}
// Name returns the name of the driver.
func (secrets *Secrets) Name() string {
return SecretsDriverName
}
// Get fetches the release named by key. The corresponding release is returned
// or error if not found.
func (secrets *Secrets) Get(key string) (release.Releaser, error) {
// fetch the secret holding the release named by key
obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrReleaseNotFound
}
return nil, fmt.Errorf("get: failed to get %q: %w", key, err)
}
// found the secret, decode the base64 data string
r, err := decodeRelease(string(obj.Data["release"]))
if err != nil {
return r, fmt.Errorf("get: failed to decode data %q: %w", key, err)
}
r.Labels = filterSystemLabels(obj.Labels)
return r, nil
}
// List fetches all releases and returns the list releases such
// that filter(release) == true. An error is returned if the
// secret fails to retrieve the releases.
func (secrets *Secrets) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
lsel := kblabels.Set{"owner": "helm"}.AsSelector()
opts := metav1.ListOptions{LabelSelector: lsel.String()}
list, err := secrets.impl.List(context.Background(), opts)
if err != nil {
return nil, fmt.Errorf("list: failed to list: %w", err)
}
var results []release.Releaser
// iterate over the secrets object list
// and decode each release
for _, item := range list.Items {
rls, err := decodeRelease(string(item.Data["release"]))
if err != nil {
secrets.Logger().Debug(
"list failed to decode release", slog.String("key", item.Name),
slog.Any("error", err),
)
continue
}
rls.Labels = item.Labels
if filter(rls) {
results = append(results, rls)
}
}
return results, nil
}
// Query fetches all releases that match the provided map of labels.
// An error is returned if the secret fails to retrieve the releases.
func (secrets *Secrets) Query(labels map[string]string) ([]release.Releaser, error) {
ls := kblabels.Set{}
for k, v := range labels {
if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
return nil, fmt.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; "))
}
ls[k] = v
}
opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()}
list, err := secrets.impl.List(context.Background(), opts)
if err != nil {
return nil, fmt.Errorf("query: failed to query with labels: %w", err)
}
if len(list.Items) == 0 {
return nil, ErrReleaseNotFound
}
var results []release.Releaser
for _, item := range list.Items {
rls, err := decodeRelease(string(item.Data["release"]))
if err != nil {
secrets.Logger().Debug(
"failed to decode release",
slog.String("key", item.Name),
slog.Any("error", err),
)
continue
}
rls.Labels = item.Labels
results = append(results, rls)
}
return results, nil
}
// Create creates a new Secret holding the release. If the
// Secret already exists, ErrReleaseExists is returned.
func (secrets *Secrets) Create(key string, rel release.Releaser) error {
// set labels for secrets object meta data
var lbs labels
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
lbs.init()
lbs.fromMap(rls.Labels)
lbs.set("createdAt", strconv.FormatInt(time.Now().Unix(), 10))
// create a new secret to hold the release
obj, err := newSecretsObject(key, rls, lbs)
if err != nil {
return fmt.Errorf("create: failed to encode release %q: %w", rls.Name, err)
}
// push the secret object out into the kubiverse
if _, err := secrets.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil {
if apierrors.IsAlreadyExists(err) {
return ErrReleaseExists
}
return fmt.Errorf("create: failed to create: %w", err)
}
return nil
}
// Update updates the Secret holding the release. If not found
// the Secret is created to hold the release.
func (secrets *Secrets) Update(key string, rel release.Releaser) error {
// set labels for secrets object meta data
var lbs labels
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
lbs.init()
lbs.fromMap(rls.Labels)
lbs.set("modifiedAt", strconv.FormatInt(time.Now().Unix(), 10))
// create a new secret object to hold the release
obj, err := newSecretsObject(key, rls, lbs)
if err != nil {
return fmt.Errorf("update: failed to encode release %q: %w", rls.Name, err)
}
// push the secret object out into the kubiverse
_, err = secrets.impl.Update(context.Background(), obj, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("update: failed to update: %w", err)
}
return nil
}
// Delete deletes the Secret holding the release named by key.
func (secrets *Secrets) Delete(key string) (rls release.Releaser, err error) {
// fetch the release to check existence
if rls, err = secrets.Get(key); err != nil {
return nil, err
}
// delete the release
err = secrets.impl.Delete(context.Background(), key, metav1.DeleteOptions{})
if err != nil {
return nil, err
}
return rls, nil
}
// newSecretsObject constructs a kubernetes Secret object
// to store a release. Each secret data entry is the base64
// encoded gzipped string of a release.
//
// The following labels are used within each secret:
//
// "modifiedAt" - timestamp indicating when this secret was last modified. (set in Update)
// "createdAt" - timestamp indicating when this secret was created. (set in Create)
// "version" - version of the release.
// "status" - status of the release (see pkg/release/status.go for variants)
// "owner" - owner of the secret, currently "helm".
// "name" - name of the release.
func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, error) {
const owner = "helm"
// encode the release
s, err := encodeRelease(rls)
if err != nil {
return nil, err
}
if lbs == nil {
lbs.init()
}
// apply custom labels
lbs.fromMap(rls.Labels)
// apply labels
lbs.set("name", rls.Name)
lbs.set("owner", owner)
lbs.set("status", rls.Info.Status.String())
lbs.set("version", strconv.Itoa(rls.Version))
// create and return secret object.
// Helm 3 introduced setting the 'Type' field
// in the Kubernetes storage object.
// Helm defines the field content as follows:
// <helm_domain>/<helm_object>.v<helm_object_version>
// Type field for Chart v3: helm.sh/release.v2
// Note: Version starts at 'v1' for Helm 3 and
// should be incremented if the release object
// metadata is modified.
// This would potentially be a breaking change
// and should only happen between major versions.
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: key,
Labels: lbs.toMap(),
},
Type: "helm.sh/release.v2",
Data: map[string][]byte{"release": []byte(s)},
}, nil
}

@ -0,0 +1,198 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"encoding/base64"
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
)
func TestSecretName(t *testing.T) {
c := newTestFixtureSecrets(t)
assert.Equal(t, SecretsDriverName, c.Name(), "Expected name to be %q, got %q", SecretsDriverName, c.Name())
}
func TestSecretGet(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
// get release with key
got, err := secrets.Get(key)
require.NoError(t, err, "Failed to get release")
// compare fetched release with original
assert.Equalf(t, rel, got, "Expected {%v}, got {%v}", rel, got)
}
func TestUNcompressedSecretGet(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// Create a test fixture which contains an uncompressed release
secret, err := newSecretsObject(key, rel, nil)
require.NoError(t, err, "Failed to create secret")
b, err := json.Marshal(rel)
require.NoError(t, err, "Failed to marshal release")
secret.Data["release"] = []byte(base64.StdEncoding.EncodeToString(b))
var mock MockSecretsInterface
mock.objects = map[string]*v1.Secret{key: secret}
secrets := NewSecrets(&mock)
// get release with key
got, err := secrets.Get(key)
require.NoError(t, err, "Failed to get release")
// compare fetched release with original
assert.Equalf(t, rel, got, "Expected {%v}, got {%v}", rel, got)
}
func TestSecretList(t *testing.T) {
secrets := newTestFixtureSecrets(t, []*rspb.Release{
releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...)
// list all deleted releases
del, err := secrets.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
})
// check
require.NoError(t, err, "Failed to list deleted")
assert.Len(t, del, 2, "Expected 2 deleted")
// list all deployed releases
dpl, err := secrets.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
})
// check
require.NoError(t, err, "Failed to list deployed")
assert.Len(t, dpl, 2, "Expected 2 deployed")
// list all superseded releases
ssd, err := secrets.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
})
// check
require.NoError(t, err, "Failed to list superseded")
require.Len(t, ssd, 2, "Expected 2 superseded")
// Check if release having both system and custom labels, this is needed to ensure that selector filtering would work.
rls := convertReleaserToV1(t, ssd[0])
require.Contains(t, rls.Labels, "name", "Expected 'name' label in results, actual %v", rls.Labels)
require.Contains(t, rls.Labels, "key1", "Expected 'key1' label in results, actual %v", rls.Labels)
}
func TestSecretQuery(t *testing.T) {
secrets := newTestFixtureSecrets(t, []*rspb.Release{
releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", common.StatusSuperseded),
}...)
rls, err := secrets.Query(map[string]string{"status": "deployed"})
require.NoError(t, err, "Failed to query")
require.Len(t, rls, 2, "Expected 2 results, actual %d", len(rls))
_, err = secrets.Query(map[string]string{"name": "notExist"})
assert.ErrorIs(t, err, ErrReleaseNotFound)
}
func TestSecretCreate(t *testing.T) {
secrets := newTestFixtureSecrets(t)
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
// store the release in a secret
require.NoErrorf(t, secrets.Create(key, rel), "Failed to create release with key %q", key)
// get the release back
got, err := secrets.Get(key)
require.NoError(t, err, "Failed to get release with key %q", key)
// compare created release with original
assert.Equalf(t, rel, got, "Expected {%v}, got {%v}", rel, got)
}
func TestSecretUpdate(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
// modify release status code
rel.Info.Status = common.StatusSuperseded
// perform the update
require.NoErrorf(t, secrets.Update(key, rel), "Failed to update release")
// fetch the updated release
goti, err := secrets.Get(key)
require.NoError(t, err, "Failed to get release with key %q", key)
got := convertReleaserToV1(t, goti)
// check release has actually been updated by comparing modified fields
assert.Equal(t, got.Info.Status, rel.Info.Status, "Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String())
}
func TestSecretDelete(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
// perform the delete on a non-existing release
_, err := secrets.Delete("nonexistent")
require.ErrorIs(t, err, ErrReleaseNotFound, "Expected ErrReleaseNotFound")
// perform the delete
rls, err := secrets.Delete(key)
require.NoError(t, err, "Failed to delete release with key %q", key)
assert.Equalf(t, rel, rls, "Expected {%v}, got {%v}", rel, rls)
_, err = secrets.Get(key)
assert.ErrorIs(t, err, ErrReleaseNotFound)
}

@ -0,0 +1,742 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"fmt"
"log/slog"
"maps"
"sort"
"strconv"
"time"
"github.com/jmoiron/sqlx"
migrate "github.com/rubenv/sql-migrate"
sq "github.com/Masterminds/squirrel"
// Import pq for postgres dialect
_ "github.com/lib/pq"
"helm.sh/helm/v4/internal/logging"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
)
var _ Driver = (*SQL)(nil)
var labelMap = map[string]struct{}{
"modifiedAt": {},
"createdAt": {},
"version": {},
"status": {},
"owner": {},
"name": {},
}
const postgreSQLDialect = "postgres"
// SQLDriverName is the string name of this driver.
const SQLDriverName = "SQL"
const (
sqlReleaseTableName = "releases_v1"
sqlCustomLabelsTableName = "custom_labels_v1"
)
const (
sqlReleaseTableKeyColumn = "key"
sqlReleaseTableTypeColumn = "type"
sqlReleaseTableBodyColumn = "body"
sqlReleaseTableNameColumn = "name"
sqlReleaseTableNamespaceColumn = "namespace"
sqlReleaseTableVersionColumn = "version"
sqlReleaseTableStatusColumn = "status"
sqlReleaseTableOwnerColumn = "owner"
sqlReleaseTableCreatedAtColumn = "createdAt"
sqlReleaseTableModifiedAtColumn = "modifiedAt"
sqlCustomLabelsTableReleaseKeyColumn = "releaseKey"
sqlCustomLabelsTableReleaseNamespaceColumn = "releaseNamespace"
sqlCustomLabelsTableKeyColumn = "key"
sqlCustomLabelsTableValueColumn = "value"
)
// Following limits based on k8s labels limits - https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
const (
sqlCustomLabelsTableKeyMaxLength = 253 + 1 + 63
sqlCustomLabelsTableValueMaxLength = 63
)
const (
sqlReleaseDefaultOwner = "helm"
sqlReleaseDefaultType = "helm.sh/release.v2"
)
// SQL is the sql storage driver implementation.
type SQL struct {
db *sqlx.DB
namespace string
statementBuilder sq.StatementBuilderType
// Embed a LogHolder to provide logger functionality
logging.LogHolder
}
// Name returns the name of the driver.
func (s *SQL) Name() string {
return SQLDriverName
}
// Check if all migrations al
func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool {
// make map (set) of ids for fast search
migrationsIDs := make(map[string]struct{})
for _, migration := range migrations {
migrationsIDs[migration.Id] = struct{}{}
}
// get list of applied migrations
migrate.SetDisableCreateTable(true)
records, err := migrate.GetMigrationRecords(s.db.DB, postgreSQLDialect)
migrate.SetDisableCreateTable(false)
if err != nil {
s.Logger().Debug("failed to get migration records", slog.Any("error", err))
return false
}
for _, record := range records {
if _, ok := migrationsIDs[record.Id]; ok {
s.Logger().Debug("found previous migration", "id", record.Id, "appliedAt", record.AppliedAt)
delete(migrationsIDs, record.Id)
}
}
// check if all migrations applied
if len(migrationsIDs) != 0 {
for id := range migrationsIDs {
s.Logger().Debug("find unapplied migration", "id", id)
}
return false
}
return true
}
func (s *SQL) ensureDBSetup() error {
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "init",
Up: []string{
fmt.Sprintf(`
CREATE TABLE %s (
%s VARCHAR(90),
%s VARCHAR(64) NOT NULL,
%s TEXT NOT NULL,
%s VARCHAR(64) NOT NULL,
%s VARCHAR(64) NOT NULL,
%s INTEGER NOT NULL,
%s TEXT NOT NULL,
%s TEXT NOT NULL,
%s INTEGER NOT NULL,
%s INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(%s, %s)
);
CREATE INDEX ON %s (%s, %s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
GRANT ALL ON %s TO PUBLIC;
ALTER TABLE %s ENABLE ROW LEVEL SECURITY;
`,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableTypeColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableNameColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableVersionColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableCreatedAtColumn,
sqlReleaseTableModifiedAtColumn,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableName,
sqlReleaseTableVersionColumn,
sqlReleaseTableName,
sqlReleaseTableStatusColumn,
sqlReleaseTableName,
sqlReleaseTableOwnerColumn,
sqlReleaseTableName,
sqlReleaseTableCreatedAtColumn,
sqlReleaseTableName,
sqlReleaseTableModifiedAtColumn,
sqlReleaseTableName,
sqlReleaseTableName,
),
},
Down: []string{
fmt.Sprintf(`
DROP TABLE %s;
`, sqlReleaseTableName),
},
},
{
Id: "custom_labels",
Up: []string{
fmt.Sprintf(`
CREATE TABLE %s (
%s VARCHAR(64),
%s VARCHAR(67),
%s VARCHAR(%d),
%s VARCHAR(%d)
);
CREATE INDEX ON %s (%s, %s);
GRANT ALL ON %s TO PUBLIC;
ALTER TABLE %s ENABLE ROW LEVEL SECURITY;
`,
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableKeyMaxLength,
sqlCustomLabelsTableValueColumn,
sqlCustomLabelsTableValueMaxLength,
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableName,
sqlCustomLabelsTableName,
),
},
Down: []string{
fmt.Sprintf(`
DELETE TABLE %s;
`, sqlCustomLabelsTableName),
},
},
},
}
// Check that init migration already applied
if s.checkAlreadyApplied(migrations.Migrations) {
return nil
}
// Populate the database with the relations we need if they don't exist yet
_, err := migrate.Exec(s.db.DB, postgreSQLDialect, migrations, migrate.Up)
return err
}
// SQLReleaseWrapper describes how Helm releases are stored in an SQL database
type SQLReleaseWrapper struct {
// The primary key, made of {release-name}.{release-version}
Key string `db:"key"`
// See https://github.com/helm/helm/blob/c9fe3d118caec699eb2565df9838673af379ce12/pkg/storage/driver/secrets.go#L231
Type string `db:"type"`
// The rspb.Release body, as a base64-encoded string
Body string `db:"body"`
// Release "labels" that can be used as filters in the storage.Query(labels map[string]string)
// we implemented. Note that allowing Helm users to filter against new dimensions will require a
// new migration to be added, and the Create and/or update functions to be updated accordingly.
Name string `db:"name"`
Namespace string `db:"namespace"`
Version int `db:"version"`
Status string `db:"status"`
Owner string `db:"owner"`
CreatedAt int `db:"createdAt"`
ModifiedAt int `db:"modifiedAt"`
}
type SQLReleaseCustomLabelWrapper struct {
ReleaseKey string `db:"release_key"`
ReleaseNamespace string `db:"release_namespace"`
Key string `db:"key"`
Value string `db:"value"`
}
// NewSQL initializes a new sql driver.
func NewSQL(connectionString, namespace string) (*SQL, error) {
db, err := sqlx.Connect(postgreSQLDialect, connectionString)
if err != nil {
return nil, err
}
driver := &SQL{
db: db,
statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}
if err := driver.ensureDBSetup(); err != nil {
return nil, err
}
driver.namespace = namespace
driver.SetLogger(slog.Default().Handler())
return driver, nil
}
// Get returns the release named by key.
func (s *SQL) Get(key string) (release.Releaser, error) {
var record SQLReleaseWrapper
qb := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace})
query, args, err := qb.ToSql()
if err != nil {
s.Logger().Debug("failed to build query", slog.Any("error", err))
return nil, err
}
// Get will return an error if the result is empty
if err := s.db.Get(&record, query, args...); err != nil {
s.Logger().Debug("got SQL error when getting release", slog.String("key", key), slog.Any("error", err))
return nil, ErrReleaseNotFound
}
release, err := decodeRelease(record.Body)
if err != nil {
s.Logger().Debug("failed to decode data", slog.String("key", key), slog.Any("error", err))
return nil, err
}
if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil {
s.Logger().Debug(
"failed to get release custom labels",
slog.String("namespace", s.namespace),
slog.String("key", key),
slog.Any("error", err),
)
return nil, err
}
return release, nil
}
// List returns the list of all releases such that filter(release) == true
func (s *SQL) List(filter func(release.Releaser) bool) ([]release.Releaser, error) {
sb := s.statementBuilder.
Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableOwnerColumn: sqlReleaseDefaultOwner})
// If a namespace was specified, we only list releases from that namespace
if s.namespace != "" {
sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace})
}
query, args, err := sb.ToSql()
if err != nil {
s.Logger().Debug("failed to build query", slog.Any("error", err))
return nil, err
}
records := []SQLReleaseWrapper{}
if err := s.db.Select(&records, query, args...); err != nil {
s.Logger().Debug("failed to list", slog.Any("error", err))
return nil, err
}
var releases []release.Releaser
for _, record := range records {
release, err := decodeRelease(record.Body)
if err != nil {
s.Logger().Debug("failed to decode release", slog.Any("record", record), slog.Any("error", err))
continue
}
if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil {
s.Logger().Debug(
"failed to get release custom labels",
slog.String("namespace", record.Namespace),
slog.String("key", record.Key),
slog.Any("error", err),
)
return nil, err
}
maps.Copy(release.Labels, getReleaseSystemLabels(release))
if filter(release) {
releases = append(releases, release)
}
}
return releases, nil
}
// Query returns the set of releases that match the provided set of labels.
func (s *SQL) Query(labels map[string]string) ([]release.Releaser, error) {
sb := s.statementBuilder.
Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn).
From(sqlReleaseTableName)
keys := make([]string, 0, len(labels))
for key := range labels {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
_, ok := labelMap[key]
if !ok {
s.Logger().Debug("unknown label", "key", key)
return nil, fmt.Errorf("unknown label %s", key)
}
sb = sb.Where(sq.Eq{key: labels[key]})
}
// If a namespace was specified, we only list releases from that namespace
if s.namespace != "" {
sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace})
}
// Build our query
query, args, err := sb.ToSql()
if err != nil {
s.Logger().Debug("failed to build query", slog.Any("error", err))
return nil, err
}
records := []SQLReleaseWrapper{}
if err := s.db.Select(&records, query, args...); err != nil {
s.Logger().Debug("failed to query with labels", slog.Any("error", err))
return nil, err
}
if len(records) == 0 {
return nil, ErrReleaseNotFound
}
var releases []release.Releaser
for _, record := range records {
release, err := decodeRelease(record.Body)
if err != nil {
s.Logger().Debug("failed to decode release", slog.Any("record", record), slog.Any("error", err))
continue
}
if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil {
s.Logger().Debug(
"failed to get release custom labels",
slog.String("namespace", record.Namespace),
slog.String("key", record.Key),
slog.Any("error", err),
)
return nil, err
}
releases = append(releases, release)
}
if len(releases) == 0 {
return nil, ErrReleaseNotFound
}
return releases, nil
}
// Create creates a new release.
func (s *SQL) Create(key string, rel release.Releaser) error {
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
s.namespace = namespace
body, err := encodeRelease(rls)
if err != nil {
s.Logger().Debug("failed to encode release", slog.Any("error", err))
return err
}
transaction, err := s.db.Beginx()
if err != nil {
s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err))
return fmt.Errorf("error beginning transaction: %w", err)
}
insertQuery, args, err := s.statementBuilder.
Insert(sqlReleaseTableName).
Columns(
sqlReleaseTableKeyColumn,
sqlReleaseTableTypeColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableNameColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableVersionColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableCreatedAtColumn,
).
Values(
key,
sqlReleaseDefaultType,
body,
rls.Name,
namespace,
int(rls.Version),
rls.Info.Status.String(),
sqlReleaseDefaultOwner,
int(time.Now().Unix()),
).ToSql()
if err != nil {
s.Logger().Debug("failed to build insert query", slog.Any("error", err))
return err
}
if _, err := transaction.Exec(insertQuery, args...); err != nil {
defer transaction.Rollback()
selectQuery, args, buildErr := s.statementBuilder.
Select(sqlReleaseTableKeyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}).
ToSql()
if buildErr != nil {
s.Logger().Debug("failed to build select query", "error", buildErr)
return err
}
var record SQLReleaseWrapper
if err := transaction.Get(&record, selectQuery, args...); err == nil {
s.Logger().Debug("release already exists", slog.String("key", key))
return ErrReleaseExists
}
s.Logger().Debug("failed to store release in SQL database", slog.String("key", key), slog.Any("error", err))
return err
}
// Filtering labels before insert cause in SQL storage driver system releases are stored in separate columns of release table
for k, v := range filterSystemLabels(rls.Labels) {
insertLabelsQuery, args, err := s.statementBuilder.
Insert(sqlCustomLabelsTableName).
Columns(
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableValueColumn,
).
Values(
key,
namespace,
k,
v,
).ToSql()
if err != nil {
defer transaction.Rollback()
s.Logger().Debug("failed to build insert query", slog.Any("error", err))
return err
}
if _, err := transaction.Exec(insertLabelsQuery, args...); err != nil {
defer transaction.Rollback()
s.Logger().Debug("failed to write Labels", slog.Any("error", err))
return err
}
}
defer transaction.Commit()
return nil
}
// Update updates a release.
//
// Custom labels on an existing revision are meant to be preserved. A release's
// labels are set by Create, and a revision keeps the labels it was created with
// once it is superseded, rather than picking up the labels of the upgrade that
// superseded it. TestUpgradeRelease_Labels in pkg/action is what asserts this.
//
// The drivers arrive at that from opposite directions. Here labels live in a
// separate table written only by Create, so an update leaves them untouched. The
// configmaps, memory and secrets drivers instead store labels on the record
// itself and replace the record wholesale on update, so they have to re-apply
// the labels every time or the update would discard all of them. A side effect
// is that those drivers persist a label change where this driver silently would
// not. No caller changes labels between Create and Update, so the two agree in
// practice.
func (s *SQL) Update(key string, rel release.Releaser) error {
rls, err := releaserToV1Release(rel)
if err != nil {
return err
}
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
s.namespace = namespace
body, err := encodeRelease(rls)
if err != nil {
s.Logger().Debug("failed to encode release", slog.Any("error", err))
return err
}
query, args, err := s.statementBuilder.
Update(sqlReleaseTableName).
Set(sqlReleaseTableBodyColumn, body).
Set(sqlReleaseTableNameColumn, rls.Name).
Set(sqlReleaseTableVersionColumn, int(rls.Version)).
Set(sqlReleaseTableStatusColumn, rls.Info.Status.String()).
Set(sqlReleaseTableOwnerColumn, sqlReleaseDefaultOwner).
Set(sqlReleaseTableTypeColumn, sqlReleaseDefaultType).
Set(sqlReleaseTableModifiedAtColumn, int(time.Now().Unix())).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: namespace}).
ToSql()
if err != nil {
s.Logger().Debug("failed to build update query", slog.Any("error", err))
return err
}
if _, err := s.db.Exec(query, args...); err != nil {
s.Logger().Debug("failed to update release in SQL database", slog.String("key", key), slog.Any("error", err))
return err
}
return nil
}
// Delete deletes a release or returns ErrReleaseNotFound.
func (s *SQL) Delete(key string) (release.Releaser, error) {
transaction, err := s.db.Beginx()
if err != nil {
s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err))
return nil, fmt.Errorf("error beginning transaction: %w", err)
}
selectQuery, args, err := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
s.Logger().Debug("failed to build select query", slog.Any("error", err))
return nil, err
}
var record SQLReleaseWrapper
err = transaction.Get(&record, selectQuery, args...)
if err != nil {
s.Logger().Debug("release not found", slog.String("key", key), slog.Any("error", err))
return nil, ErrReleaseNotFound
}
release, err := decodeRelease(record.Body)
if err != nil {
s.Logger().Debug("failed to decode release", slog.String("key", key), slog.Any("error", err))
transaction.Rollback()
return nil, err
}
defer transaction.Commit()
deleteQuery, args, err := s.statementBuilder.
Delete(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
s.Logger().Debug("failed to build delete query", slog.Any("error", err))
return nil, err
}
_, err = transaction.Exec(deleteQuery, args...)
if err != nil {
s.Logger().Debug("failed perform delete query", slog.Any("error", err))
return release, err
}
if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil {
s.Logger().Debug(
"failed to get release custom labels",
slog.String("namespace", s.namespace),
slog.String("key", key),
slog.Any("error", err))
return nil, err
}
deleteCustomLabelsQuery, args, err := s.statementBuilder.
Delete(sqlCustomLabelsTableName).
Where(sq.Eq{sqlCustomLabelsTableReleaseKeyColumn: key}).
Where(sq.Eq{sqlCustomLabelsTableReleaseNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
s.Logger().Debug("failed to build delete Labels query", slog.Any("error", err))
return nil, err
}
_, err = transaction.Exec(deleteCustomLabelsQuery, args...)
return release, err
}
// Get release custom labels from database
func (s *SQL) getReleaseCustomLabels(key, _ string) (map[string]string, error) {
query, args, err := s.statementBuilder.
Select(sqlCustomLabelsTableKeyColumn, sqlCustomLabelsTableValueColumn).
From(sqlCustomLabelsTableName).
Where(sq.Eq{
sqlCustomLabelsTableReleaseKeyColumn: key,
sqlCustomLabelsTableReleaseNamespaceColumn: s.namespace,
}).
ToSql()
if err != nil {
return nil, err
}
labelsList := []SQLReleaseCustomLabelWrapper{}
if err := s.db.Select(&labelsList, query, args...); err != nil {
return nil, err
}
labelsMap := make(map[string]string)
for _, i := range labelsList {
labelsMap[i.Key] = i.Value
}
return filterSystemLabels(labelsMap), nil
}
// Rebuild system labels from release object
func getReleaseSystemLabels(rls *rspb.Release) map[string]string {
return map[string]string{
"name": rls.Name,
"owner": sqlReleaseDefaultOwner,
"status": rls.Info.Status.String(),
"version": strconv.Itoa(rls.Version),
}
}

@ -0,0 +1,564 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"database/sql/driver"
"errors"
"fmt"
"reflect"
"regexp"
"testing"
"time"
sqlmock "github.com/DATA-DOG/go-sqlmock"
migrate "github.com/rubenv/sql-migrate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common"
)
const recentTimestampTolerance = time.Second
func recentUnixTimestamp() sqlmock.Argument {
return recentUnixTimestampArgument{}
}
type recentUnixTimestampArgument struct{}
func (recentUnixTimestampArgument) Match(value driver.Value) bool {
var ts int64
switch v := value.(type) {
case int:
ts = int64(v)
case int64:
ts = v
default:
return false
}
diff := time.Since(time.Unix(ts, 0))
if diff < 0 {
diff = -diff
}
return diff <= recentTimestampTolerance
}
func TestSQLName(t *testing.T) {
sqlDriver, _ := newTestFixtureSQL(t)
assert.Equal(t, SQLDriverName, sqlDriver.Name(), "Expected name to be %s, got %s", SQLDriverName, sqlDriver.Name())
}
func TestSQLGet(t *testing.T) {
vers := int(1)
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
body, _ := encodeRelease(rel)
sqlDriver, mock := newTestFixtureSQL(t)
query := fmt.Sprintf(
regexp.QuoteMeta("SELECT %s FROM %s WHERE %s = $1 AND %s = $2"),
sqlReleaseTableBodyColumn,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
)
mock.
ExpectQuery(query).
WithArgs(key, namespace).
WillReturnRows(
mock.NewRows([]string{
sqlReleaseTableBodyColumn,
}).AddRow(
body,
),
).RowsWillBeClosed()
mockGetReleaseCustomLabels(mock, key, namespace, rel.Labels)
got, err := sqlDriver.Get(key)
require.NoError(t, err, "Failed to get release")
assert.Equalf(t, rel, got, "Expected release {%v}, got {%v}", rel, got)
assert.NoErrorf(t, mock.ExpectationsWereMet(), "sql expectations weren't met")
}
func TestSQLList(t *testing.T) {
releases := []*rspb.Release{}
releases = append(releases,
releaseStub("key-1", 1, "default", common.StatusUninstalled),
releaseStub("key-2", 1, "default", common.StatusUninstalled),
releaseStub("key-3", 1, "default", common.StatusDeployed),
releaseStub("key-4", 1, "default", common.StatusDeployed),
releaseStub("key-5", 1, "default", common.StatusSuperseded),
releaseStub("key-6", 1, "default", common.StatusSuperseded),
)
sqlDriver, mock := newTestFixtureSQL(t)
for range 3 {
query := fmt.Sprintf(
"SELECT %s, %s, %s FROM %s WHERE %s = $1 AND %s = $2",
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableName,
sqlReleaseTableOwnerColumn,
sqlReleaseTableNamespaceColumn,
)
rows := mock.NewRows([]string{
sqlReleaseTableBodyColumn,
})
for _, r := range releases {
body, _ := encodeRelease(r)
rows.AddRow(body)
}
mock.
ExpectQuery(regexp.QuoteMeta(query)).
WithArgs(sqlReleaseDefaultOwner, sqlDriver.namespace).
WillReturnRows(rows).RowsWillBeClosed()
for _, r := range releases {
mockGetReleaseCustomLabels(mock, "", r.Namespace, r.Labels)
}
}
// list all deleted releases
del, err := sqlDriver.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusUninstalled
})
// check
require.NoError(t, err, "Failed to list deleted")
assert.Len(t, del, 2, "Expected 2 deleted")
// list all deployed releases
dpl, err := sqlDriver.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusDeployed
})
// check
require.NoError(t, err, "Failed to list deployed")
assert.Len(t, dpl, 2, "Expected 2 deployed")
// list all superseded releases
ssd, err := sqlDriver.List(func(rel release.Releaser) bool {
rls := convertReleaserToV1(t, rel)
return rls.Info.Status == common.StatusSuperseded
})
// check
require.NoError(t, err, "Failed to list superseded")
require.Len(t, ssd, 2, "Expected 2 superseded")
require.NoErrorf(t, mock.ExpectationsWereMet(), "sql expectations weren't met")
// Check if release having both system and custom labels, this is needed to ensure that selector filtering would work.
rls := convertReleaserToV1(t, ssd[0])
require.Contains(t, rls.Labels, "name", "Expected 'name' label in results, actual %v", rls.Labels)
require.Contains(t, rls.Labels, "key1", "Expected 'key1' label in results, actual %v", rls.Labels)
}
func TestSqlCreate(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
sqlDriver, mock := newTestFixtureSQL(t)
body, _ := encodeRelease(rel)
query := fmt.Sprintf(
"INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)",
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableTypeColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableNameColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableVersionColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableCreatedAtColumn,
)
mock.ExpectBegin()
mock.
ExpectExec(regexp.QuoteMeta(query)).
WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()).
WillReturnResult(sqlmock.NewResult(1, 1))
labelsQuery := fmt.Sprintf(
"INSERT INTO %s (%s,%s,%s,%s) VALUES ($1,$2,$3,$4)",
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableValueColumn,
)
mock.MatchExpectationsInOrder(false)
for k, v := range filterSystemLabels(rel.Labels) {
mock.
ExpectExec(regexp.QuoteMeta(labelsQuery)).
WithArgs(key, rel.Namespace, k, v).
WillReturnResult(sqlmock.NewResult(1, 1))
}
mock.ExpectCommit()
require.NoErrorf(t, sqlDriver.Create(key, rel), "failed to create release with key %s", key)
assert.NoErrorf(t, mock.ExpectationsWereMet(), "sql expectations weren't met")
}
func TestSqlCreateAlreadyExists(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
sqlDriver, mock := newTestFixtureSQL(t)
body, _ := encodeRelease(rel)
insertQuery := fmt.Sprintf(
"INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)",
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableTypeColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableNameColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableVersionColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableCreatedAtColumn,
)
// Insert fails (primary key already exists)
mock.ExpectBegin()
mock.
ExpectExec(regexp.QuoteMeta(insertQuery)).
WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()).
WillReturnError(errors.New("dialect dependent SQL error"))
selectQuery := fmt.Sprintf(
regexp.QuoteMeta("SELECT %s FROM %s WHERE %s = $1 AND %s = $2"),
sqlReleaseTableKeyColumn,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
)
// Let's check that we do make sure the error is due to a release already existing
mock.
ExpectQuery(selectQuery).
WithArgs(key, namespace).
WillReturnRows(
mock.NewRows([]string{
sqlReleaseTableKeyColumn,
}).AddRow(
key,
),
).RowsWillBeClosed()
mock.ExpectRollback()
require.Errorf(t, sqlDriver.Create(key, rel), "failed to create release with key %s", key)
assert.NoErrorf(t, mock.ExpectationsWereMet(), "sql expectations weren't met")
}
func TestSqlUpdate(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
sqlDriver, mock := newTestFixtureSQL(t)
body, _ := encodeRelease(rel)
query := fmt.Sprintf(
"UPDATE %s SET %s = $1, %s = $2, %s = $3, %s = $4, %s = $5, %s = $6, %s = $7 WHERE %s = $8 AND %s = $9",
sqlReleaseTableName,
sqlReleaseTableBodyColumn,
sqlReleaseTableNameColumn,
sqlReleaseTableVersionColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableTypeColumn,
sqlReleaseTableModifiedAtColumn,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
)
mock.
ExpectExec(regexp.QuoteMeta(query)).
WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, sqlReleaseDefaultType, recentUnixTimestamp(), key, namespace).
WillReturnResult(sqlmock.NewResult(0, 1))
require.NoErrorf(t, sqlDriver.Update(key, rel), "failed to update release with key %s", key)
assert.NoErrorf(t, mock.ExpectationsWereMet(), "sql expectations weren't met")
}
func TestSqlQuery(t *testing.T) {
// Reflect actual use cases in ../storage.go
labelSetUnknown := map[string]string{
"name": "smug-pigeon",
"owner": sqlReleaseDefaultOwner,
"status": "unknown",
}
labelSetDeployed := map[string]string{
"name": "smug-pigeon",
"owner": sqlReleaseDefaultOwner,
"status": "deployed",
}
labelSetAll := map[string]string{
"name": "smug-pigeon",
"owner": sqlReleaseDefaultOwner,
}
supersededRelease := releaseStub("smug-pigeon", 1, "default", common.StatusSuperseded)
supersededReleaseBody, _ := encodeRelease(supersededRelease)
deployedRelease := releaseStub("smug-pigeon", 2, "default", common.StatusDeployed)
deployedReleaseBody, _ := encodeRelease(deployedRelease)
// Let's actually start our test
sqlDriver, mock := newTestFixtureSQL(t)
query := fmt.Sprintf(
"SELECT %s, %s, %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3 AND %s = $4",
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableName,
sqlReleaseTableNameColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableNamespaceColumn,
)
mock.
ExpectQuery(regexp.QuoteMeta(query)).
WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "unknown", "default").
WillReturnRows(
mock.NewRows([]string{
sqlReleaseTableBodyColumn,
}),
).RowsWillBeClosed()
mock.
ExpectQuery(regexp.QuoteMeta(query)).
WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "deployed", "default").
WillReturnRows(
mock.NewRows([]string{
sqlReleaseTableBodyColumn,
}).AddRow(
deployedReleaseBody,
),
).RowsWillBeClosed()
mockGetReleaseCustomLabels(mock, "", deployedRelease.Namespace, deployedRelease.Labels)
query = fmt.Sprintf(
"SELECT %s, %s, %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3",
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableName,
sqlReleaseTableNameColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableNamespaceColumn,
)
mock.
ExpectQuery(regexp.QuoteMeta(query)).
WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "default").
WillReturnRows(
mock.NewRows([]string{
sqlReleaseTableBodyColumn,
}).AddRow(
supersededReleaseBody,
).AddRow(
deployedReleaseBody,
),
).RowsWillBeClosed()
mockGetReleaseCustomLabels(mock, "", supersededRelease.Namespace, supersededRelease.Labels)
mockGetReleaseCustomLabels(mock, "", deployedRelease.Namespace, deployedRelease.Labels)
_, err := sqlDriver.Query(labelSetUnknown)
require.Errorf(t, err, "Expected error {%v}, got nil", ErrReleaseNotFound)
require.ErrorIsf(t, err, ErrReleaseNotFound, "failed to query for unknown smug-pigeon release")
results, err := sqlDriver.Query(labelSetDeployed)
require.NoError(t, err, "failed to query for deployed smug-pigeon release")
for _, res := range results {
assert.Equalf(t, res, deployedRelease, "Expected release {%v}, got {%v}", deployedRelease, res)
}
results, err = sqlDriver.Query(labelSetAll)
require.NoError(t, err, "failed to query release history for smug-pigeon")
assert.Len(t, results, 2, "expected a resultset of size 2")
for _, res := range results {
assert.False(t, !reflect.DeepEqual(res, deployedRelease) && !reflect.DeepEqual(res, supersededRelease), "Expected release {%v} or {%v}, got {%v}", deployedRelease, supersededRelease, res)
}
assert.NoErrorf(t, mock.ExpectationsWereMet(), "sql expectations weren't met")
}
func TestSqlDelete(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, common.StatusDeployed)
body, _ := encodeRelease(rel)
sqlDriver, mock := newTestFixtureSQL(t)
selectQuery := fmt.Sprintf(
"SELECT %s FROM %s WHERE %s = $1 AND %s = $2",
sqlReleaseTableBodyColumn,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
)
mock.ExpectBegin()
mock.
ExpectQuery(regexp.QuoteMeta(selectQuery)).
WithArgs(key, namespace).
WillReturnRows(
mock.NewRows([]string{
sqlReleaseTableBodyColumn,
}).AddRow(
body,
),
).RowsWillBeClosed()
deleteQuery := fmt.Sprintf(
"DELETE FROM %s WHERE %s = $1 AND %s = $2",
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
)
mock.
ExpectExec(regexp.QuoteMeta(deleteQuery)).
WithArgs(key, namespace).
WillReturnResult(sqlmock.NewResult(0, 1))
mockGetReleaseCustomLabels(mock, key, namespace, rel.Labels)
deleteLabelsQuery := fmt.Sprintf(
"DELETE FROM %s WHERE %s = $1 AND %s = $2",
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
)
mock.
ExpectExec(regexp.QuoteMeta(deleteLabelsQuery)).
WithArgs(key, namespace).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
deletedRelease, err := sqlDriver.Delete(key)
require.NoError(t, err)
err = mock.ExpectationsWereMet()
assert.NoErrorf(t, err, "sql expectations weren't met")
require.NoError(t, err, "failed to delete release with key %q", key)
assert.Equalf(t, rel, deletedRelease, "Expected release {%v}, got {%v}", rel, deletedRelease)
}
func mockGetReleaseCustomLabels(mock sqlmock.Sqlmock, key, namespace string, labels map[string]string) {
query := fmt.Sprintf(
regexp.QuoteMeta("SELECT %s, %s FROM %s WHERE %s = $1 AND %s = $2"),
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableValueColumn,
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
)
eq := mock.ExpectQuery(query).
WithArgs(key, namespace)
returnRows := mock.NewRows([]string{
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableValueColumn,
})
for k, v := range labels {
returnRows.AddRow(k, v)
}
eq.WillReturnRows(returnRows).RowsWillBeClosed()
}
func TestSqlCheckAppliedMigrations(t *testing.T) {
cases := []struct {
migrationsToApply []*migrate.Migration
appliedMigrationsIDs []string
expectedResult bool
errorExplanation string
}{
{
migrationsToApply: []*migrate.Migration{{Id: "init1"}, {Id: "init2"}, {Id: "init3"}},
appliedMigrationsIDs: []string{"1", "2", "init1", "3", "init2", "4", "5"},
expectedResult: false,
errorExplanation: "Has found one migration id \"init3\" as applied, that was not applied",
},
{
migrationsToApply: []*migrate.Migration{{Id: "init1"}, {Id: "init2"}, {Id: "init3"}},
appliedMigrationsIDs: []string{"1", "2", "init1", "3", "init2", "4", "init3", "5"},
expectedResult: true,
errorExplanation: "Has not found one or more migration ids, that was applied",
},
{
migrationsToApply: []*migrate.Migration{{Id: "init"}},
appliedMigrationsIDs: []string{"1", "2", "3", "inits", "4", "tinit", "5"},
expectedResult: false,
errorExplanation: "Has found single \"init\", that was not applied",
},
{
migrationsToApply: []*migrate.Migration{{Id: "init"}},
appliedMigrationsIDs: []string{"1", "2", "init", "3", "init2", "4", "init3", "5"},
expectedResult: true,
errorExplanation: "Has not found single migration id \"init\", that was applied",
},
}
for i, c := range cases {
sqlDriver, mock := newTestFixtureSQL(t)
rows := sqlmock.NewRows([]string{"id", "applied_at"})
for _, id := range c.appliedMigrationsIDs {
rows.AddRow(id, time.Time{})
}
mock.
ExpectQuery("").
WillReturnRows(rows)
mock.ExpectCommit()
assert.Equal(t, c.expectedResult, sqlDriver.checkAlreadyApplied(c.migrationsToApply), "Test case: %v, Expected: %v, Have: %v, Explanation: %v", i, c.expectedResult, !c.expectedResult, c.errorExplanation)
}
}

@ -0,0 +1,131 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/json"
"io"
"slices"
rspb "helm.sh/helm/v4/internal/release/v2"
)
var b64 = base64.StdEncoding
var magicGzip = []byte{0x1f, 0x8b, 0x08}
// 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.
func encodeRelease(rls *rspb.Release) (string, error) {
b, err := json.Marshal(rls)
if err != nil {
return "", err
}
var buf bytes.Buffer
w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return "", err
}
if _, err = w.Write(b); err != nil {
w.Close()
return "", err
}
if err := w.Close(); err != nil {
return "", err
}
return b64.EncodeToString(buf.Bytes()), nil
}
// decodeRelease decodes the bytes of data into a release
// type. Data must contain a base64 encoded gzipped string of a
// valid release, otherwise an error is returned.
func decodeRelease(data string) (*rspb.Release, error) {
// base64 decode string
b, err := b64.DecodeString(data)
if err != nil {
return nil, err
}
// For backwards compatibility with releases that were stored before
// compression was introduced we skip decompression if the
// gzip magic header is not found
if len(b) > 3 && bytes.Equal(b[0:3], magicGzip) {
r, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {
return nil, err
}
defer r.Close()
b2, err := io.ReadAll(r)
if err != nil {
return nil, err
}
b = b2
}
var rls rspb.Release
// unmarshal release object bytes
if err := json.Unmarshal(b, &rls); err != nil {
return nil, err
}
return &rls, nil
}
// Checks if label is system
func isSystemLabel(key string) bool {
return slices.Contains(GetSystemLabels(), key)
}
// Removes system labels from labels map
func filterSystemLabels(lbs map[string]string) map[string]string {
result := make(map[string]string)
for k, v := range lbs {
if !isSystemLabel(k) {
result[k] = v
}
}
return result
}
// Checks if labels array contains system labels
func ContainsSystemLabels(lbs map[string]string) bool {
for k := range lbs {
if isSystemLabel(k) {
return true
}
}
return false
}
func GetSystemLabels() []string {
return systemLabels
}

@ -0,0 +1,105 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package driver
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetSystemLabel(t *testing.T) {
output := GetSystemLabels()
assert.Equalf(t, systemLabels, output, "Expected {%v}, got {%v}", systemLabels, output)
}
func TestIsSystemLabel(t *testing.T) {
tests := map[string]bool{
"name": true,
"owner": true,
"test": false,
"NaMe": false,
}
for label, result := range tests {
output := isSystemLabel(label)
assert.Equalf(t, output, result, "Output %t not equal to expected %t", output, result)
}
}
func TestFilterSystemLabels(t *testing.T) {
tests := [][2]map[string]string{
{nil, map[string]string{}},
{map[string]string{}, map[string]string{}},
{map[string]string{
"name": "name",
"owner": "owner",
"status": "status",
"version": "version",
"createdAt": "createdAt",
"modifiedAt": "modifiedAt",
}, map[string]string{}},
{map[string]string{
"StaTus": "status",
"name": "name",
"owner": "owner",
"key": "value",
}, map[string]string{
"StaTus": "status",
"key": "value",
}},
{map[string]string{
"key1": "value1",
"key2": "value2",
}, map[string]string{
"key1": "value1",
"key2": "value2",
}},
}
for _, test := range tests {
output := filterSystemLabels(test[0])
assert.Equalf(t, test[1], output, "Expected {%v}, got {%v}", test[1], output)
}
}
func TestContainsSystemLabels(t *testing.T) {
tests := []struct {
input map[string]string
output bool
}{
{nil, false},
{map[string]string{}, false},
{map[string]string{
"name": "name",
"owner": "owner",
"status": "status",
"version": "version",
"createdAt": "createdAt",
"modifiedAt": "modifiedAt",
}, true},
{map[string]string{
"StaTus": "status",
"name": "name",
"owner": "owner",
"key": "value",
}, true},
{map[string]string{
"key1": "value1",
"key2": "value2",
}, false},
}
for _, test := range tests {
output := ContainsSystemLabels(test.input)
assert.Equalf(t, test.output, output, "Expected {%v}, got {%v}", test.output, output)
}
}

@ -114,6 +114,10 @@ func newRecord(key string, rls *rspb.Release) *record {
var lbs labels var lbs labels
lbs.init() lbs.init()
// apply custom labels
lbs.fromMap(rls.Labels)
lbs.set("name", rls.Name) lbs.set("name", rls.Name)
lbs.set("owner", "helm") lbs.set("owner", "helm")
lbs.set("status", rls.Info.Status.String()) lbs.set("status", rls.Info.Status.String())

@ -576,6 +576,20 @@ func (s *SQL) Create(key string, rel release.Releaser) error {
} }
// Update updates a release. // Update updates a release.
//
// Custom labels on an existing revision are meant to be preserved. A release's
// labels are set by Create, and a revision keeps the labels it was created with
// once it is superseded, rather than picking up the labels of the upgrade that
// superseded it. TestUpgradeRelease_Labels in pkg/action is what asserts this.
//
// The drivers arrive at that from opposite directions. Here labels live in a
// separate table written only by Create, so an update leaves them untouched. The
// configmaps, memory and secrets drivers instead store labels on the record
// itself and replace the record wholesale on update, so they have to re-apply
// the labels every time or the update would discard all of them. A side effect
// is that those drivers persist a label change where this driver silently would
// not. No caller changes labels between Create and Update, so the two agree in
// practice.
func (s *SQL) Update(key string, rel release.Releaser) error { func (s *SQL) Update(key string, rel release.Releaser) error {
rls, err := releaserToV1Release(rel) rls, err := releaserToV1Release(rel)
if err != nil { if err != nil {

@ -30,11 +30,23 @@ import (
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
) )
// HelmStorageType is the type field of the Kubernetes storage object which stores the Helm release // HelmStorageType is the prefix used for the name of the Kubernetes storage object
// version. It is modified slightly replacing the '/': sh.helm/release.v1 // that holds a release. It is derived from that object's 'Type' field
// Note: The version 'v1' is incremented if the release object metadata is // (helm.sh/release.v1) by reversing the domain and replacing the '/' with a '.'.
// modified between major releases. //
// This constant is used as a prefix for the Kubernetes storage object name. // 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" const HelmStorageType = "sh.helm.release.v1"
// Storage represents a storage engine for a Release. // Storage represents a storage engine for a Release.

Loading…
Cancel
Save