From 09f8cc9487a8a5291b6c263b5c7873ea1d59177d Mon Sep 17 00:00:00 2001 From: Liu Ming Date: Thu, 7 May 2020 23:38:14 +0800 Subject: [PATCH] feat: support customized labels for storage like secret, configmap close #7007, #8098 Signed-off-by: Liu Ming --- cmd/helm/flags.go | 4 + cmd/helm/install.go | 10 +++ pkg/action/install.go | 8 +- pkg/action/rollback.go | 8 ++ pkg/action/upgrade.go | 9 +++ pkg/cli/values/options.go | 17 +++++ pkg/cli/values/options_test.go | 21 ++++++ pkg/storage/driver/cfgmaps.go | 39 ++++++++++ pkg/storage/driver/cfgmaps_test.go | 115 ++++++++++++++++++++++++++-- pkg/storage/driver/driver.go | 11 +++ pkg/storage/driver/labels.go | 34 +++++++++ pkg/storage/driver/labels_test.go | 45 +++++++++++ pkg/storage/driver/memory.go | 10 +++ pkg/storage/driver/mock_test.go | 51 ++++++++++--- pkg/storage/driver/secrets.go | 37 +++++++++ pkg/storage/driver/secrets_test.go | 117 +++++++++++++++++++++++++++-- pkg/storage/driver/sql.go | 10 +++ pkg/storage/storage.go | 5 ++ 18 files changed, 523 insertions(+), 28 deletions(-) diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index aefa836c7..c0a4b1d57 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -60,6 +60,10 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") } +func addStorageOptionsFlags(f *pflag.FlagSet, v *values.Options) { + f.StringArrayVarP(&v.Labels, "set-label", "l", []string{}, "specify labels for release secret/configmap on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") +} + // bindOutputFlag will add the output flag to the given command and bind the // value to the given format pointer func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 8b468d2f5..b028b61be 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -157,6 +157,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") addValueOptionsFlags(f, valueOpts) addChartPathOptionsFlags(f, &client.ChartPathOptions) + addStorageOptionsFlags(f, valueOpts) err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { requiredArgs := 2 @@ -204,6 +205,15 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, err } + labels, err := valueOpts.ParseLabels() + if err != nil { + return nil, err + } + client.Labels = labels + if len(client.Labels) > 0 { + debug("customized labels: %+v", client.Labels) + } + // Check chart dependencies to make sure all are present in /charts chartRequested, err := loader.Load(cp) if err != nil { diff --git a/pkg/action/install.go b/pkg/action/install.go index b84a57271..5fb9a9f2a 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -109,6 +109,8 @@ type Install struct { PostRenderer postrender.PostRenderer // Lock to control raceconditions when the process receives a SIGTERM Lock sync.Mutex + // Labels customized release secret/configmap labels + Labels map[string]string } // ChartPathOptions captures common options used for controlling chart paths @@ -302,6 +304,10 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma return rel, nil } + if err = i.cfg.Releases.SetLabels(i.Labels); err != nil { + return nil, err + } + if i.CreateNamespace { ns := &v1.Namespace{ TypeMeta: metav1.TypeMeta{ @@ -328,7 +334,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma } } - // If Replace is true, we need to supercede the last release. + // If Replace is true, we need to supersede the last release. if i.Replace { if err := i.replaceRelease(rel); err != nil { return nil, err diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index f3f958f3d..be8517221 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -117,6 +117,14 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele return nil, nil, err } + labels, err := r.cfg.Releases.GetLabels(name, previousVersion) + if err != nil { + return nil, nil, err + } + if err = r.cfg.Releases.SetLabels(labels); err != nil { + return nil, nil, err + } + // Store a new release object with previous release's configuration targetRelease := &release.Release{ Name: name, diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index a4a1a0883..6bed5d901 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -181,6 +181,15 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, errPending } + // get previous release secret labels + labels, err := u.cfg.Releases.GetLabels(name, lastRelease.Version) + if err != nil { + return nil, nil, err + } + if err = u.cfg.Releases.SetLabels(labels); err != nil { + return nil, nil, err + } + var currentRelease *release.Release if lastRelease.Info.Status == release.StatusDeployed { // no need to retrieve the last deployed release from storage as the last release is deployed diff --git a/pkg/cli/values/options.go b/pkg/cli/values/options.go index e6ad71767..7e3caa992 100644 --- a/pkg/cli/values/options.go +++ b/pkg/cli/values/options.go @@ -34,6 +34,8 @@ type Options struct { StringValues []string Values []string FileValues []string + // Labels customized release secret/configmap labels + Labels []string } // MergeValues merges values from files specified via -f/--values and directly @@ -119,3 +121,18 @@ func readFile(filePath string, p getter.Providers) ([]byte, error) { data, err := g.Get(filePath, getter.WithURL(filePath)) return data.Bytes(), err } + +func (opts *Options) ParseLabels() (map[string]string, error) { + base := map[string]interface{}{} + for _, l := range opts.Labels { + if err := strvals.ParseIntoString(l, base); err != nil { + return nil, errors.Wrap(err, "failed parsing --set-label data") + } + } + + r := make(map[string]string) + for k, v := range base { + r[k] = v.(string) + } + return r, nil +} diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go index d988274bf..65e5aa3dd 100644 --- a/pkg/cli/values/options_test.go +++ b/pkg/cli/values/options_test.go @@ -19,6 +19,8 @@ package values import ( "reflect" "testing" + + "github.com/gosuri/uitable/util/strutil" ) func TestMergeValues(t *testing.T) { @@ -75,3 +77,22 @@ func TestMergeValues(t *testing.T) { t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) } } + +func TestParseLabels(t *testing.T) { + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + + labels := make([]string, 0) + for k, v := range lbs { + labels = append(labels, strutil.Join([]string{k, v}, "=")) + } + + opts := &Options{ + Labels: labels, + } + gotLbs, _ := opts.ParseLabels() + + // compare created parsed labels with original + if !reflect.DeepEqual(lbs, gotLbs) { + t.Errorf("Expected {%v}, got {%v}", lbs, gotLbs) + } +} diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 94c278875..46a8707c0 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -43,6 +43,8 @@ const ConfigMapsDriverName = "ConfigMap" type ConfigMaps struct { impl corev1.ConfigMapInterface Log func(string, ...interface{}) + // labels specifies the customized labels for release configmap + labels } // NewConfigMaps initializes a new ConfigMaps wrapping an implementation of @@ -157,6 +159,8 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { var lbs labels lbs.init() + // set customized labels + lbs.fromMap(cfgmaps.labels) lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap to hold the release @@ -183,7 +187,14 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { // set labels for configmaps object meta data var lbs labels + // get the existed configmap to re-use it's labels + exist, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{}) + if err != nil { + return errors.Wrapf(err, "update: failed to get configmap %q", key) + } + lbs.init() + lbs.fromMap(exist.Labels) lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap object to hold the release @@ -192,6 +203,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { cfgmaps.Log("update: failed to encode release %q: %s", rls.Name, err) return err } + // push the configmap object out into the kubiverse _, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) if err != nil { @@ -255,3 +267,30 @@ func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigM Data: map[string]string{"release": s}, }, nil } + +// SetLabels set the customized labels, must be executed before Create function +func (cfgmaps *ConfigMaps) SetLabels(labels map[string]string) error { + if err := validate(labels); err != nil { + return err + } + if cfgmaps.labels == nil { + cfgmaps.labels.init() + } + cfgmaps.labels.fromMap(labels) + return nil +} + +// GetLabels return the customized labels +func (cfgmaps *ConfigMaps) GetLabels(key string) (map[string]string, 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.Log("get: failed to get %q: %s", key, err) + return nil, err + } + return retrieveCustomizedLabels(obj.Labels), nil +} diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index 626c36cb9..c383d8d4a 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -38,7 +38,7 @@ func TestConfigMapGet(t *testing.T) { key := testKey(name, vers) rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + cfgmaps := newTestFixtureCfgMaps(t, wrapReleases(rel)...) // get release with key got, err := cfgmaps.Get(key) @@ -84,14 +84,14 @@ func TestUncompressedConfigMapGet(t *testing.T) { } func TestConfigMapList(t *testing.T) { - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ + cfgmaps := newTestFixtureCfgMaps(t, wrapReleases( releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", rspb.StatusSuperseded), - }...) + )...) // list all deleted releases del, err := cfgmaps.List(func(rel *rspb.Release) bool { @@ -131,14 +131,14 @@ func TestConfigMapList(t *testing.T) { } func TestConfigMapQuery(t *testing.T) { - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ + cfgmaps := newTestFixtureCfgMaps(t, wrapReleases( releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", rspb.StatusSuperseded), - }...) + )...) rls, err := cfgmaps.Query(map[string]string{"status": "deployed"}) if err != nil { @@ -180,6 +180,41 @@ func TestConfigMapCreate(t *testing.T) { } } +func TestConfigMapCreateWithLabels(t *testing.T) { + cfgmaps := newTestFixtureCfgMaps(t) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + cfgmaps.SetLabels(lbs) + + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + // store the release in a configmap + if err := cfgmaps.Create(key, rel); err != nil { + t.Fatalf("Failed to create release with key %q: %s", key, err) + } + + // get the release back + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // compare created release with original + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected {%v}, got {%v}", rel, got) + } + + // compare created configmap's labels with original + gotLbs, _ := cfgmaps.GetLabels(key) + if !reflect.DeepEqual(lbs, gotLbs) { + t.Errorf("Expected {%v}, got {%v}", lbs, gotLbs) + } +} + func TestConfigMapUpdate(t *testing.T) { vers := 1 name := "smug-pigeon" @@ -187,7 +222,7 @@ func TestConfigMapUpdate(t *testing.T) { key := testKey(name, vers) rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + cfgmaps := newTestFixtureCfgMaps(t, wrapReleases(rel)...) // modify release status code rel.Info.Status = rspb.StatusSuperseded @@ -209,6 +244,43 @@ func TestConfigMapUpdate(t *testing.T) { } } +func TestConfigMapUpdateWithLabels(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + + cfgmaps := newTestFixtureCfgMaps(t, []*releaseInfo{newReleaseInfoWithLabels(rel, deepCopyStringMap(lbs))}...) + + // modify release status code + rel.Info.Status = rspb.StatusSuperseded + + // perform the update + if err := cfgmaps.Update(key, rel); err != nil { + t.Fatalf("Failed to update release: %s", err) + } + + // fetch the updated release + got, err := cfgmaps.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // check release has actually been updated by comparing modified fields + if rel.Info.Status != got.Info.Status { + t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String()) + } + + // compare created configmap's labels with original + gotLbs, _ := cfgmaps.GetLabels(key) + if !reflect.DeepEqual(lbs, gotLbs) { + t.Errorf("Expected {%v}, got {%v}", lbs, gotLbs) + } +} + func TestConfigMapDelete(t *testing.T) { vers := 1 name := "smug-pigeon" @@ -216,7 +288,7 @@ func TestConfigMapDelete(t *testing.T) { key := testKey(name, vers) rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) - cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + cfgmaps := newTestFixtureCfgMaps(t, wrapReleases(rel)...) // perform the delete on a non-existent release _, err := cfgmaps.Delete("nonexistent") @@ -239,3 +311,32 @@ func TestConfigMapDelete(t *testing.T) { t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) } } + +func TestConfigMapSetLabels(t *testing.T) { + cfgmaps := newTestFixtureCfgMaps(t) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + cfgmaps.SetLabels(lbs) + + if !reflect.DeepEqual(lbs, cfgmaps.labels.toMap()) { + t.Errorf("Expected {%v}, got {%v}", lbs, cfgmaps.labels) + } +} + +func TestConfigMapGetLabels(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + + cfgmaps := newTestFixtureCfgMaps(t, []*releaseInfo{newReleaseInfoWithLabels(rel, deepCopyStringMap(lbs))}...) + + // compare created configmap's labels with original + gotLbs, _ := cfgmaps.GetLabels(key) + if !reflect.DeepEqual(lbs, gotLbs) { + t.Errorf("Expected {%v}, got {%v}", lbs, gotLbs) + } +} diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index 9c01f3766..d1622a404 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -92,6 +92,16 @@ type Queryor interface { Query(labels map[string]string) ([]*rspb.Release, error) } +// Labelor is the interface that wraps the SetLabels and GetLabels methods. +// +// SetLabels label the release storage (like secret, configmap etc.) with given labels +// +// GetLabels get labels of the release storage (like secret, configmap etc.) +type Labelor interface { + SetLabels(labels map[string]string) error + GetLabels(key string) (map[string]string, 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, @@ -101,5 +111,6 @@ type Driver interface { Updator Deletor Queryor + Labelor Name() string } diff --git a/pkg/storage/driver/labels.go b/pkg/storage/driver/labels.go index eb7118fe5..35dbc6ccb 100644 --- a/pkg/storage/driver/labels.go +++ b/pkg/storage/driver/labels.go @@ -16,9 +16,16 @@ limitations under the License. package driver +import ( + "fmt" +) + // labels is a map of key value pairs to be included as metadata in a configmap object. type labels map[string]string +// reservedLabels specifies release secret/configmap labels for helm +var reservedLabels = []string{"createdAt", "modifiedAt", "name", "owner", "status", "version"} + 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 } @@ -46,3 +53,30 @@ func (lbs *labels) fromMap(kvs map[string]string) { lbs.set(k, v) } } + +// validate validates whether user set the labels using Helm preserved labels +func validate(labels map[string]string) error { + for _, lk := range reservedLabels { + if _, found := labels[lk]; found { + return fmt.Errorf("label key '%s' is reserved for helm, not available for users", lk) + } + } + return nil +} + +// retrieveCustomizedLabels retrieves the real customized labels from the given labels that might contain Helm preserved labels +func retrieveCustomizedLabels(labels map[string]string) map[string]string { + copiedLabels := deepCopyStringMap(labels) + for _, lk := range reservedLabels { + delete(copiedLabels, lk) + } + return copiedLabels +} + +func deepCopyStringMap(m map[string]string) map[string]string { + ret := make(map[string]string, len(m)) + for k, v := range m { + ret[k] = v + } + return ret +} diff --git a/pkg/storage/driver/labels_test.go b/pkg/storage/driver/labels_test.go index bfd80911b..844f68aa4 100644 --- a/pkg/storage/driver/labels_test.go +++ b/pkg/storage/driver/labels_test.go @@ -47,3 +47,48 @@ func TestLabelsMatch(t *testing.T) { } } } + +func TestValidate(t *testing.T) { + // empty map + var nilMap map[string]string + if err := validate(nilMap); err != nil { + t.Errorf("Nil label map should not fail when validating: %s", err) + } + // customized labels have no preserved labels of Helm + labels0 := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + if err := validate(labels0); err != nil { + t.Errorf("Customized labels with no preserved labels of Helm should not fail when validating: %s", err) + } + // customized labels contain preserved labels of Helm + labels1 := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B", "owner": "Helm"} + if err := validate(labels1); err == nil { + t.Errorf("Customized labels with preserved labels of Helm must fail when validating") + } +} + +func TestRetrieveCustomizedLabels(t *testing.T) { + // empty map + var nilMap map[string]string + customizedLabels := retrieveCustomizedLabels(nilMap) + if len(customizedLabels) != 0 { + t.Errorf("Nil label map retrieved result should be empy map") + } + // customized labels with no preserved labels of Helm + labels0 := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + customizedLabels = retrieveCustomizedLabels(labels0) + if len(customizedLabels) != len(labels0) { + t.Errorf("Customized labels with no preserved labels of Helm retrieved result should return the origin labels map") + } + // customized labels that every key in preserved labels of Helm + labels1 := map[string]string{"name": "name", "owner": "helm"} + customizedLabels = retrieveCustomizedLabels(labels1) + if len(customizedLabels) != 0 { + t.Errorf("customized labels that every key in preserved labels of Helm retrieved result should be empy map") + } + // customized labels contain preserved labels of Helm + labels2 := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B", "owner": "Helm"} + customizedLabels = retrieveCustomizedLabels(labels2) + if len(customizedLabels) != 2 { + t.Errorf("customized labels contain preserved labels of Helm retrieved result should not contain Helm preserved label key") + } +} diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index 91378f588..8a21fc495 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -238,3 +238,13 @@ func (mem *Memory) rlock() func() { // ```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() } + +func (mem *Memory) SetLabels(labels map[string]string) error { + // not support labels + return nil +} + +func (mem *Memory) GetLabels(key string) (map[string]string, error) { + // not support labels + return nil, nil +} diff --git a/pkg/storage/driver/mock_test.go b/pkg/storage/driver/mock_test.go index c0236ece8..115eac4f9 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -34,6 +34,33 @@ import ( rspb "helm.sh/helm/v3/pkg/release" ) +type releaseInfo struct { + release *rspb.Release + // labels specifies the customized labels for release storage's (secret/configmap) + labels map[string]string +} + +func newReleaseInfo(rls *rspb.Release) *releaseInfo { + return &releaseInfo{ + release: rls, + } +} + +func wrapReleases(rlss ...*rspb.Release) []*releaseInfo { + rlsInfos := make([]*releaseInfo, 0) + for _, rls := range rlss { + rlsInfos = append(rlsInfos, newReleaseInfo(rls)) + } + return rlsInfos +} + +func newReleaseInfoWithLabels(rls *rspb.Release, lbs map[string]string) *releaseInfo { + return &releaseInfo{ + release: rls, + labels: lbs, + } +} + func releaseStub(name string, vers int, namespace string, status rspb.Status) *rspb.Release { return &rspb.Release{ Name: name, @@ -78,9 +105,9 @@ func tsFixtureMemory(t *testing.T) *Memory { // newTestFixture initializes a MockConfigMapsInterface. // ConfigMaps are created for each release provided. -func newTestFixtureCfgMaps(t *testing.T, releases ...*rspb.Release) *ConfigMaps { +func newTestFixtureCfgMaps(t *testing.T, rlsInfos ...*releaseInfo) *ConfigMaps { var mock MockConfigMapsInterface - mock.Init(t, releases...) + mock.Init(t, rlsInfos...) return NewConfigMaps(&mock) } @@ -93,13 +120,13 @@ type MockConfigMapsInterface struct { } // Init initializes the MockConfigMapsInterface with the set of releases. -func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) { +func (mock *MockConfigMapsInterface) Init(t *testing.T, rlsInfos ...*releaseInfo) { mock.objects = map[string]*v1.ConfigMap{} - for _, rls := range releases { - objkey := testKey(rls.Name, rls.Version) + for _, rlsInfo := range rlsInfos { + objkey := testKey(rlsInfo.release.Name, rlsInfo.release.Version) - cfgmap, err := newConfigMapsObject(objkey, rls, nil) + cfgmap, err := newConfigMapsObject(objkey, rlsInfo.release, rlsInfo.labels) if err != nil { t.Fatalf("Failed to create configmap: %s", err) } @@ -164,9 +191,9 @@ func (mock *MockConfigMapsInterface) Delete(_ context.Context, name string, _ me // newTestFixture initializes a MockSecretsInterface. // Secrets are created for each release provided. -func newTestFixtureSecrets(t *testing.T, releases ...*rspb.Release) *Secrets { +func newTestFixtureSecrets(t *testing.T, rlsInfos ...*releaseInfo) *Secrets { var mock MockSecretsInterface - mock.Init(t, releases...) + mock.Init(t, rlsInfos...) return NewSecrets(&mock) } @@ -179,13 +206,13 @@ type MockSecretsInterface struct { } // Init initializes the MockSecretsInterface with the set of releases. -func (mock *MockSecretsInterface) Init(t *testing.T, releases ...*rspb.Release) { +func (mock *MockSecretsInterface) Init(t *testing.T, rlsInfos ...*releaseInfo) { mock.objects = map[string]*v1.Secret{} - for _, rls := range releases { - objkey := testKey(rls.Name, rls.Version) + for _, rlsInfo := range rlsInfos { + objkey := testKey(rlsInfo.release.Name, rlsInfo.release.Version) - secret, err := newSecretsObject(objkey, rls, nil) + secret, err := newSecretsObject(objkey, rlsInfo.release, rlsInfo.labels) if err != nil { t.Fatalf("Failed to create secret: %s", err) } diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 2e8530d0c..495e2fff6 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -43,6 +43,8 @@ const SecretsDriverName = "Secret" type Secrets struct { impl corev1.SecretInterface Log func(string, ...interface{}) + // labels specifies the customized labels for release secret + labels } // NewSecrets initializes a new Secrets wrapping an implementation of @@ -148,6 +150,8 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error { var lbs labels lbs.init() + // set customized labels + lbs.fromMap(secrets.labels) lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) // create a new secret to hold the release @@ -172,7 +176,14 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error { // set labels for secrets object meta data var lbs labels + // get the existed secret to re-use it's labels + exist, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{}) + if err != nil { + return errors.Wrapf(err, "update: failed to get secret %q", key) + } + lbs.init() + lbs.fromMap(exist.Labels) lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) // create a new secret object to hold the release @@ -180,6 +191,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error { if err != nil { return errors.Wrapf(err, "update: failed to encode release %q", rls.Name) } + // push the secret object out into the kubiverse _, err = secrets.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) return errors.Wrap(err, "update: failed to update") @@ -248,3 +260,28 @@ func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, er Data: map[string][]byte{"release": []byte(s)}, }, nil } + +// SetLabels set the customized labels, must be executed before Create function +func (secrets *Secrets) SetLabels(labels map[string]string) error { + if err := validate(labels); err != nil { + return err + } + if secrets.labels == nil { + secrets.labels.init() + } + secrets.labels.fromMap(labels) + return nil +} + +// GetLabels return the customized labels +func (secrets *Secrets) GetLabels(key string) (map[string]string, 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, errors.Wrapf(err, "get: failed to get %q", key) + } + return retrieveCustomizedLabels(obj.Labels), nil +} diff --git a/pkg/storage/driver/secrets_test.go b/pkg/storage/driver/secrets_test.go index d509c7b3a..fcf23c764 100644 --- a/pkg/storage/driver/secrets_test.go +++ b/pkg/storage/driver/secrets_test.go @@ -38,7 +38,7 @@ func TestSecretGet(t *testing.T) { key := testKey(name, vers) rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) - secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) + secrets := newTestFixtureSecrets(t, wrapReleases(rel)...) // get release with key got, err := secrets.Get(key) @@ -51,7 +51,7 @@ func TestSecretGet(t *testing.T) { } } -func TestUNcompressedSecretGet(t *testing.T) { +func TestUncompressedSecretGet(t *testing.T) { vers := 1 name := "smug-pigeon" namespace := "default" @@ -84,14 +84,14 @@ func TestUNcompressedSecretGet(t *testing.T) { } func TestSecretList(t *testing.T) { - secrets := newTestFixtureSecrets(t, []*rspb.Release{ + secrets := newTestFixtureSecrets(t, wrapReleases( releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", rspb.StatusSuperseded), - }...) + )...) // list all deleted releases del, err := secrets.List(func(rel *rspb.Release) bool { @@ -131,14 +131,14 @@ func TestSecretList(t *testing.T) { } func TestSecretQuery(t *testing.T) { - secrets := newTestFixtureSecrets(t, []*rspb.Release{ + secrets := newTestFixtureSecrets(t, wrapReleases( releaseStub("key-1", 1, "default", rspb.StatusUninstalled), releaseStub("key-2", 1, "default", rspb.StatusUninstalled), releaseStub("key-3", 1, "default", rspb.StatusDeployed), releaseStub("key-4", 1, "default", rspb.StatusDeployed), releaseStub("key-5", 1, "default", rspb.StatusSuperseded), releaseStub("key-6", 1, "default", rspb.StatusSuperseded), - }...) + )...) rls, err := secrets.Query(map[string]string{"status": "deployed"}) if err != nil { @@ -180,6 +180,41 @@ func TestSecretCreate(t *testing.T) { } } +func TestSecretCreateWithLabels(t *testing.T) { + secrets := newTestFixtureSecrets(t) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + secrets.SetLabels(lbs) + + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + // store the release in a secret + if err := secrets.Create(key, rel); err != nil { + t.Fatalf("Failed to create release with key %q: %s", key, err) + } + + // get the release back + got, err := secrets.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // compare created release with original + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected {%v}, got {%v}", rel, got) + } + + // compare created secret's labels with original + gotLbs, _ := secrets.GetLabels(key) + if !reflect.DeepEqual(lbs, gotLbs) { + t.Errorf("Expected {%v}, got {%v}", lbs, gotLbs) + } +} + func TestSecretUpdate(t *testing.T) { vers := 1 name := "smug-pigeon" @@ -187,7 +222,7 @@ func TestSecretUpdate(t *testing.T) { key := testKey(name, vers) rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) - secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) + secrets := newTestFixtureSecrets(t, wrapReleases(rel)...) // modify release status code rel.Info.Status = rspb.StatusSuperseded @@ -209,6 +244,43 @@ func TestSecretUpdate(t *testing.T) { } } +func TestSecretUpdateWithLabels(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + + secrets := newTestFixtureSecrets(t, []*releaseInfo{newReleaseInfoWithLabels(rel, deepCopyStringMap(lbs))}...) + + // modify release status code + rel.Info.Status = rspb.StatusSuperseded + + // perform the update + if err := secrets.Update(key, rel); err != nil { + t.Fatalf("Failed to update release: %s", err) + } + + // fetch the updated release + got, err := secrets.Get(key) + if err != nil { + t.Fatalf("Failed to get release with key %q: %s", key, err) + } + + // check release has actually been updated by comparing modified fields + if rel.Info.Status != got.Info.Status { + t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String()) + } + + // compare created secret's labels with original + gotLbs, _ := secrets.GetLabels(key) + if !reflect.DeepEqual(lbs, gotLbs) { + t.Errorf("Expected {%v}, got {%v}", lbs, gotLbs) + } +} + func TestSecretDelete(t *testing.T) { vers := 1 name := "smug-pigeon" @@ -216,7 +288,7 @@ func TestSecretDelete(t *testing.T) { key := testKey(name, vers) rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) - secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) + secrets := newTestFixtureSecrets(t, wrapReleases(rel)...) // perform the delete on a non-existing release _, err := secrets.Delete("nonexistent") @@ -239,3 +311,32 @@ func TestSecretDelete(t *testing.T) { t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) } } + +func TestSecretSetLabels(t *testing.T) { + secrets := newTestFixtureSecrets(t) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + secrets.SetLabels(lbs) + + if !reflect.DeepEqual(lbs, secrets.labels.toMap()) { + t.Errorf("Expected {%v}, got {%v}", lbs, secrets.labels) + } +} + +func TestSecretGetLabels(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + lbs := map[string]string{"KEY_A": "VAL_A", "KEY_B": "VAL_B"} + + secrets := newTestFixtureSecrets(t, []*releaseInfo{newReleaseInfoWithLabels(rel, deepCopyStringMap(lbs))}...) + + // compare created secret's labels with original + gotLbs, _ := secrets.GetLabels(key) + if !reflect.DeepEqual(lbs, gotLbs) { + t.Errorf("Expected {%v}, got {%v}", lbs, gotLbs) + } +} diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index c8a6ae04f..cace272df 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -494,3 +494,13 @@ func (s *SQL) Delete(key string) (*rspb.Release, error) { _, err = transaction.Exec(deleteQuery, args...) return release, err } + +func (s *SQL) SetLabels(labels map[string]string) error { + // not support labels + return nil +} + +func (s *SQL) GetLabels(key string) (map[string]string, error) { + // not support labels + return nil, nil +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 370fec4b4..b39bd1a44 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -54,6 +54,11 @@ func (s *Storage) Get(name string, version int) (*rspb.Release, error) { return s.Driver.Get(makeKey(name, version)) } +// GetLabels retrieves the labels of release storage. +func (s *Storage) GetLabels(name string, version int) (map[string]string, error) { + return s.Driver.GetLabels(makeKey(name, version)) +} + // Create creates a new storage entry holding the release. An // error is returned if the storage driver fails to store the // release, or a release with an identical key already exists.