Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Nelm is production-ready: as the werf deployment engine, it was battle-tested ac
- [Advanced resource lifecycle capabilities](#advanced-resource-lifecycle-capabilities)
- [Resource state tracking](#resource-state-tracking)
- [Printing logs and events during deploy](#printing-logs-and-events-during-deploy)
- [Release planning and two-stage deployment support](#release-planning-and-two-stage-deployment-workflow-support)
- [Release planning and two-stage deployment workflow support](#release-planning-and-two-stage-deployment-workflow-support)
- [Encrypted values and encrypted files](#encrypted-values-and-encrypted-files)
- [Improved CRD management](#improved-crd-management)
- [Usage](#usage)
Expand All @@ -45,7 +45,6 @@ Nelm is production-ready: as the werf deployment engine, it was battle-tested ac
- [`werf.io/weight` annotation](#werfioweight-annotation)
- [`werf.io/deploy-dependency-<id>` annotation](#werfiodeploy-dependency-id-annotation)
- [`werf.io/delete-dependency-<id>` annotation](#werfiodelete-dependency-id-annotation)

- [`werf.io/ownership` annotation](#werfioownership-annotation)
- [`werf.io/deploy-on` annotation](#werfiodeploy-on-annotation)
- [`werf.io/delete-policy` annotation](#werfiodelete-policy-annotation)
Expand Down
1,005 changes: 701 additions & 304 deletions docs/reference.md

Large diffs are not rendered by default.

Binary file not shown.
25 changes: 25 additions & 0 deletions pkg/helm/pkg/storage/driver/cfgmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,31 @@ func (cfgmaps *ConfigMaps) Update(key string, rel release.Releaser) error {
return nil
}

// UpdateLabels merges the given custom labels into the ConfigMap holding the
// release named by key without creating a new revision. System labels in the
// map are ignored to avoid corrupting release metadata.
func (cfgmaps *ConfigMaps) UpdateLabels(key string, lbls map[string]string) error {
obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return ErrReleaseNotFound
}
return fmt.Errorf("update labels: failed to get %q: %w", key, err)
}

if obj.Labels == nil {
obj.Labels = map[string]string{}
}
for k, v := range filterSystemLabels(lbls) {
obj.Labels[k] = v
}

if _, err := cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("update labels: failed to update %q: %w", key, 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
Expand Down
10 changes: 10 additions & 0 deletions pkg/helm/pkg/storage/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ type Queryor interface {
Query(labels map[string]string) ([]release.Releaser, error)
}

// LabelUpdator is the interface that wraps the UpdateLabels method.
//
// UpdateLabels merges the given custom labels into the stored release named by
// key without creating a new revision, or returns ErrReleaseNotFound if the
// release does not exist. Existing labels not present in the map are preserved.
type LabelUpdator interface {
UpdateLabels(key string, labels 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,
Expand All @@ -101,6 +110,7 @@ type Driver interface {
Updator
Deletor
Queryor
LabelUpdator
Name() string
}

Expand Down
31 changes: 31 additions & 0 deletions pkg/helm/pkg/storage/driver/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,37 @@ func (mem *Memory) Update(key string, rel release.Releaser) error {
return ErrReleaseNotFound
}

// UpdateLabels merges the given custom labels into the stored release named by
// key without creating a new revision. Returns ErrReleaseNotFound if the
// release does not exist.
func (mem *Memory) UpdateLabels(key string, lbls map[string]string) error {
defer unlock(mem.wlock())

keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.")
elems := strings.Split(keyWithoutPrefix, ".v")
if len(elems) != 2 {
return ErrInvalidKey
}
name := elems[0]

recs, ok := mem.cache[mem.namespace][name]
if !ok {
return ErrReleaseNotFound
}
r := recs.Get(key)
if r == nil {
return ErrReleaseNotFound
}

if r.rls.Labels == nil {
r.rls.Labels = map[string]string{}
}
for k, v := range filterSystemLabels(lbls) {
r.rls.Labels[k] = v
}
return nil
}

// Delete deletes a release or returns ErrReleaseNotFound.
func (mem *Memory) Delete(key string) (release.Releaser, error) {
defer unlock(mem.wlock())
Expand Down
29 changes: 29 additions & 0 deletions pkg/helm/pkg/storage/driver/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,35 @@ func TestMemoryUpdate(t *testing.T) {
}
}

func TestMemoryUpdateLabels(t *testing.T) {
ts := tsFixtureMemory(t)
ts.SetNamespace("default")

key := testKey("rls-a", 4)
if err := ts.UpdateLabels(key, map[string]string{"owned-by": "operator", "key1": "changed"}); err != nil {
t.Fatalf("Failed to update labels: %s\n", err)
}

r, err := ts.Get(key)
if err != nil {
t.Fatalf("Failed to get: %s\n", err)
}
rls := r.(*rspb.Release)
if rls.Labels["owned-by"] != "operator" {
t.Fatalf("Expected merged label owned-by=operator, got %q\n", rls.Labels["owned-by"])
}
if rls.Labels["key1"] != "changed" {
t.Fatalf("Expected overwritten label key1=changed, got %q\n", rls.Labels["key1"])
}
if rls.Labels["key2"] != "val2" {
t.Fatalf("Expected preserved label key2=val2, got %q\n", rls.Labels["key2"])
}

if err := ts.UpdateLabels(testKey("rls-missing", 1), map[string]string{"owned-by": "operator"}); err != ErrReleaseNotFound {
t.Fatalf("Expected ErrReleaseNotFound, got %v\n", err)
}
}

func TestMemoryDelete(t *testing.T) {
var tests = []struct {
desc string
Expand Down
25 changes: 25 additions & 0 deletions pkg/helm/pkg/storage/driver/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,31 @@ func (secrets *Secrets) Update(key string, rel release.Releaser) error {
return nil
}

// UpdateLabels merges the given custom labels into the Secret holding the
// release named by key without creating a new revision. System labels in the
// map are ignored to avoid corrupting release metadata.
func (secrets *Secrets) UpdateLabels(key string, lbls map[string]string) error {
obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return ErrReleaseNotFound
}
return fmt.Errorf("update labels: failed to get %q: %w", key, err)
}

if obj.Labels == nil {
obj.Labels = map[string]string{}
}
for k, v := range filterSystemLabels(lbls) {
obj.Labels[k] = v
}

if _, err := secrets.impl.Update(context.Background(), obj, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("update labels: failed to update %q: %w", key, 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
Expand Down
34 changes: 34 additions & 0 deletions pkg/helm/pkg/storage/driver/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,40 @@ func TestSecretUpdate(t *testing.T) {
}
}

func TestSecretUpdateLabels(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}...)

if err := secrets.UpdateLabels(key, map[string]string{"owned-by": "operator", "key1": "changed"}); err != nil {
t.Fatalf("Failed to update labels: %s", err)
}

goti, err := secrets.Get(key)
if err != nil {
t.Fatalf("Failed to get release with key %q: %s", key, err)
}
got := convertReleaserToV1(t, goti)

if got.Labels["owned-by"] != "operator" {
t.Errorf("Expected merged label owned-by=operator, got %q", got.Labels["owned-by"])
}
if got.Labels["key1"] != "changed" {
t.Errorf("Expected overwritten label key1=changed, got %q", got.Labels["key1"])
}
if got.Labels["key2"] != "val2" {
t.Errorf("Expected preserved label key2=val2, got %q", got.Labels["key2"])
}

if err := secrets.UpdateLabels("nonexistent", map[string]string{"owned-by": "operator"}); !errors.Is(err, ErrReleaseNotFound) {
t.Fatalf("Expected ErrReleaseNotFound, got %v", err)
}
}

func TestSecretDelete(t *testing.T) {
vers := 1
name := "smug-pigeon"
Expand Down
73 changes: 73 additions & 0 deletions pkg/helm/pkg/storage/driver/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,79 @@ func (s *SQL) Update(key string, rel release.Releaser) error {
return nil
}

// UpdateLabels merges the given custom labels into the release named by key
// without creating a new revision, by upserting rows in the custom labels
// table. System labels in the map are ignored to avoid corrupting release
// metadata. Returns ErrReleaseNotFound if the release does not exist.
func (s *SQL) UpdateLabels(key string, lbls map[string]string) error {
selectQuery, selectArgs, err := s.statementBuilder.
Select(sqlReleaseTableKeyColumn).
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 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: %v", err)
}

var existingKey string
if err := transaction.Get(&existingKey, selectQuery, selectArgs...); err != nil {
transaction.Rollback()
return ErrReleaseNotFound
}

for k, v := range filterSystemLabels(lbls) {
deleteQuery, deleteArgs, err := s.statementBuilder.
Delete(sqlCustomLabelsTableName).
Where(sq.Eq{
sqlCustomLabelsTableReleaseKeyColumn: key,
sqlCustomLabelsTableReleaseNamespaceColumn: s.namespace,
sqlCustomLabelsTableKeyColumn: k,
}).
ToSql()
if err != nil {
transaction.Rollback()
s.Logger().Debug("failed to build delete labels query", slog.Any("error", err))
return err
}
if _, err := transaction.Exec(deleteQuery, deleteArgs...); err != nil {
transaction.Rollback()
s.Logger().Debug("failed to delete existing label", slog.Any("error", err))
return err
}

insertQuery, insertArgs, err := s.statementBuilder.
Insert(sqlCustomLabelsTableName).
Columns(
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableValueColumn,
).
Values(key, s.namespace, k, v).
ToSql()
if err != nil {
transaction.Rollback()
s.Logger().Debug("failed to build insert labels query", slog.Any("error", err))
return err
}
if _, err := transaction.Exec(insertQuery, insertArgs...); err != nil {
transaction.Rollback()
s.Logger().Debug("failed to insert label", slog.Any("error", err))
return err
}
}

return transaction.Commit()
}

// Delete deletes a release or returns ErrReleaseNotFound.
func (s *SQL) Delete(key string) (release.Releaser, error) {
transaction, err := s.db.Beginx()
Expand Down
55 changes: 55 additions & 0 deletions pkg/helm/pkg/storage/driver/sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,61 @@ func TestSqlUpdate(t *testing.T) {
}
}

func TestSqlUpdateLabels(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)

sqlDriver, mock := newTestFixtureSQL(t)

selectQuery := fmt.Sprintf(
"SELECT %s FROM %s WHERE %s = $1 AND %s = $2",
sqlReleaseTableKeyColumn,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
)
deleteQuery := fmt.Sprintf(
"DELETE FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3",
sqlCustomLabelsTableName,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
)
insertQuery := fmt.Sprintf(
"INSERT INTO %s (%s,%s,%s,%s) VALUES ($1,$2,$3,$4)",
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableValueColumn,
)

mock.ExpectBegin()
mock.
ExpectQuery(regexp.QuoteMeta(selectQuery)).
WithArgs(key, namespace).
WillReturnRows(sqlmock.NewRows([]string{sqlReleaseTableKeyColumn}).AddRow(key))
mock.
ExpectExec(regexp.QuoteMeta(deleteQuery)).
WithArgs("owned-by", key, namespace).
WillReturnResult(sqlmock.NewResult(0, 0))
mock.
ExpectExec(regexp.QuoteMeta(insertQuery)).
WithArgs(key, namespace, "owned-by", "operator").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()

if err := sqlDriver.UpdateLabels(key, map[string]string{"owned-by": "operator"}); err != nil {
t.Fatalf("failed to update labels with key %s: %v", key, err)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("sql expectations weren't met: %v", err)
}
}

func TestSqlQuery(t *testing.T) {
// Reflect actual use cases in ../storage.go
labelSetUnknown := map[string]string{
Expand Down
8 changes: 8 additions & 0 deletions pkg/helm/pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ func (s *Storage) Update(rls release.Releaser) error {
return s.Driver.Update(makeKey(rac.Name(), rac.Version()), rls)
}

// UpdateLabels merges the given custom labels into the stored release identified
// by name and version without creating a new revision. An error is returned if
// the storage backend fails or the release does not exist.
func (s *Storage) UpdateLabels(name string, version int, labels map[string]string) error {
s.Logger().Debug("updating release labels", "key", makeKey(name, version))
return s.Driver.UpdateLabels(makeKey(name, version), labels)
}

// Delete deletes the release from storage. An error is returned if
// the storage backend fails to delete the release or if the release
// does not exist.
Expand Down
3 changes: 3 additions & 0 deletions pkg/helm/pkg/storage/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ func (d *MaxHistoryMockDriver) Create(key string, rls release.Releaser) error {
func (d *MaxHistoryMockDriver) Update(key string, rls release.Releaser) error {
return d.Driver.Update(key, rls)
}
func (d *MaxHistoryMockDriver) UpdateLabels(key string, labels map[string]string) error {
return d.Driver.UpdateLabels(key, labels)
}
func (d *MaxHistoryMockDriver) Delete(_ string) (release.Releaser, error) {
return nil, errMaxHistoryMockDriverSomethingHappened
}
Expand Down
Loading
Loading