mirror of https://github.com/helm/helm
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
parent
7fd3c5e8f2
commit
6f5c2f5f72
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue