Skip to content

Commit

Permalink
Add support for labeling a release
Browse files Browse the repository at this point in the history
Fixes #1323

Signed-off-by: Adrien Fillon <[email protected]>
  • Loading branch information
adrien-f committed May 24, 2024
1 parent b2a325a commit 15b0f04
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .changelog/1372.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
Add support for labeling a release
```
75 changes: 75 additions & 0 deletions helm/resource_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/url"
"os"
"path"
"slices"
"strings"
"time"

Expand All @@ -26,7 +27,9 @@ import (
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/strvals"
utilValidation "k8s.io/apimachinery/pkg/util/validation"
"sigs.k8s.io/yaml"
)

Expand Down Expand Up @@ -347,6 +350,15 @@ func resourceRelease() *schema.Resource {
return new == ""
},
},
"labels": {
Type: schema.TypeMap,
Optional: true,
Description: "Labels that would be added to release metadata",
Elem: &schema.Schema{
Type: schema.TypeString,
},
ValidateFunc: validateLabels,
},
"create_namespace": {
Type: schema.TypeBool,
Optional: true,
Expand Down Expand Up @@ -609,6 +621,12 @@ func resourceReleaseCreate(ctx context.Context, d *schema.ResourceData, meta int
client.Description = d.Get("description").(string)
client.CreateNamespace = d.Get("create_namespace").(bool)

labels := map[string]string{}
for k, v := range d.Get("labels").(map[string]interface{}) {
labels[k] = v.(string)
}
client.Labels = labels

if cmd := d.Get("postrender.0.binary_path").(string); cmd != "" {
av := d.Get("postrender.0.args")
var args []string
Expand Down Expand Up @@ -732,6 +750,21 @@ func resourceReleaseUpdate(ctx context.Context, d *schema.ResourceData, meta int
client.CleanupOnFail = d.Get("cleanup_on_fail").(bool)
client.Description = d.Get("description").(string)

labels := map[string]string{}
oldLabels, newLabels := d.GetChange("labels")
for k := range oldLabels.(map[string]interface{}) {
if _, ok := newLabels.(map[string]interface{})[k]; !ok {
// https://github.com/helm/helm/blob/691f313442d84112c3c9b700e156eef7509f6614/pkg/action/upgrade.go#L630
labels[k] = "null"
}
}

for k, v := range newLabels.(map[string]interface{}) {
labels[k] = v.(string)
}

client.Labels = labels

if cmd := d.Get("postrender.0.binary_path").(string); cmd != "" {
av := d.Get("postrender.0.args")
var args []string
Expand Down Expand Up @@ -1006,6 +1039,12 @@ func resourceDiff(ctx context.Context, d *schema.ResourceDiff, meta interface{})
upgrade.Description = d.Get("description").(string)
upgrade.PostRenderer = postRenderer

labels := map[string]string{}
for k, v := range d.Get("labels").(map[string]interface{}) {
labels[k] = v.(string)
}
upgrade.Labels = labels

values, err := getValues(d)
if err != nil {
return fmt.Errorf("error getting values for a diff: %v", err)
Expand Down Expand Up @@ -1070,6 +1109,19 @@ func setReleaseAttributes(d *schema.ResourceData, r *release.Release, meta inter
values = string(v)
}

labels := map[string]string{}
systemLabels := driver.GetSystemLabels()
for k, v := range r.Labels {
if slices.Contains(systemLabels, k) {
continue
}
labels[k] = v
}

if err := d.Set("labels", labels); err != nil {
return err
}

m := meta.(*Meta)
if m.ExperimentEnabled("manifest") {
jsonManifest, err := convertYAMLManifestToJSON(r.Manifest)
Expand Down Expand Up @@ -1533,3 +1585,26 @@ func useChartVersion(chart string, repo string) bool {

return false
}

func validateLabels(value interface{}, key string) (ws []string, es []error) {
systemLabels := driver.GetSystemLabels()
m := value.(map[string]interface{})
for k, v := range m {
for _, msg := range utilValidation.IsQualifiedName(k) {
es = append(es, fmt.Errorf("%s (%q) %s", key, k, msg))
}
val, isString := v.(string)
if !isString {
es = append(es, fmt.Errorf("%s.%s (%#v): Expected value to be string", key, k, v))
return
}
for _, msg := range utilValidation.IsValidLabelValue(val) {
es = append(es, fmt.Errorf("%s (%q) %s", key, val, msg))
}

if slices.Contains(systemLabels, k) {
es = append(es, fmt.Errorf("%s (%q) is a system reserved label and cannot be set", key, k))
}
}
return
}
78 changes: 77 additions & 1 deletion helm/resource_release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestAccResourceRelease_basic(t *testing.T) {
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.chart", "test-chart"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "1.2.3"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.app_version", "1.19.5"),
resource.TestCheckResourceAttr("helm_release.test", "labels.foo", "bar"),
),
},
{
Expand All @@ -73,6 +74,36 @@ func TestAccResourceRelease_basic(t *testing.T) {
})
}

func TestAccResourceRelease_validationLabels(t *testing.T) {
name := randName("validation-labels")
namespace := createRandomNamespace(t)
defer deleteNamespace(t, namespace)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: map[string]func() (*schema.Provider, error){
"helm": func() (*schema.Provider, error) {
return Provider(), nil
},
},
CheckDestroy: testAccCheckHelmReleaseDestroy(namespace),
Steps: []resource.TestStep{
{
Config: testAccHelmReleaseConfigBasicWithLabel(testResourceName, namespace, name, "1.2.3", "version", `"1.2.3"`),
ExpectError: regexp.MustCompile(`.*labels \("version"\) is a system reserved label and cannot be set.*`),
},
{
Config: testAccHelmReleaseConfigBasicWithLabel(testResourceName, namespace, name, "1.2.3", "foo", `{ foo = "bar" }`),
ExpectError: regexp.MustCompile(`.*Inappropriate value for attribute "labels": element "foo": string required.*`),
},
{
Config: testAccHelmReleaseConfigBasicWithLabel(testResourceName, namespace, name, "1.2.3", "&&&", `"bar"`),
ExpectError: regexp.MustCompile(`.*labels \("&&&"\) name part must consist of alphanumeric characters.*`),
},
},
})
}

// NOTE this is a regression test for: https://github.com/hashicorp/terraform-provider-helm/issues/1236
func TestAccResourceRelease_emptyVersion(t *testing.T) {
name := randName("basic")
Expand Down Expand Up @@ -139,6 +170,7 @@ func TestAccResourceRelease_import(t *testing.T) {
resource.TestCheckResourceAttr("helm_release.imported", "metadata.0.version", "1.2.0"),
resource.TestCheckResourceAttr("helm_release.imported", "status", release.StatusDeployed.String()),
resource.TestCheckResourceAttr("helm_release.imported", "description", "Test"),
resource.TestCheckResourceAttr("helm_release.imported", "labels.foo", "bar"),
resource.TestCheckNoResourceAttr("helm_release.imported", "repository"),

// Default values
Expand Down Expand Up @@ -302,15 +334,28 @@ func TestAccResourceRelease_update(t *testing.T) {
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "1.2.3"),
resource.TestCheckResourceAttr("helm_release.test", "status", release.StatusDeployed.String()),
resource.TestCheckResourceAttr("helm_release.test", "version", "1.2.3"),
resource.TestCheckResourceAttr("helm_release.test", "labels.foo", "bar"),
),
},
{
Config: testAccHelmReleaseConfigBasic(testResourceName, namespace, name, "2.0.0"),
Config: testAccHelmReleaseConfigBasicWithLabel(testResourceName, namespace, name, "2.0.0", "foo", `"baz"`),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.revision", "2"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "2.0.0"),
resource.TestCheckResourceAttr("helm_release.test", "status", release.StatusDeployed.String()),
resource.TestCheckResourceAttr("helm_release.test", "version", "2.0.0"),
resource.TestCheckResourceAttr("helm_release.test", "labels.foo", "baz"),
),
},
{
Config: testAccHelmReleaseConfigBasicWithLabel(testResourceName, namespace, name, "2.0.0", "bar", `"foo"`),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.revision", "3"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "2.0.0"),
resource.TestCheckResourceAttr("helm_release.test", "status", release.StatusDeployed.String()),
resource.TestCheckResourceAttr("helm_release.test", "version", "2.0.0"),
resource.TestCheckResourceAttr("helm_release.test", "labels.bar", "foo"),
resource.TestCheckNoResourceAttr("helm_release.test", "labels.foo"),
),
},
},
Expand Down Expand Up @@ -894,6 +939,10 @@ func testAccHelmReleaseConfigBasic(resource, ns, name, version string) string {
chart = "test-chart"
version = %q
labels = {
"foo" = "bar"
}
set {
name = "foo"
value = "bar"
Expand All @@ -907,6 +956,33 @@ func testAccHelmReleaseConfigBasic(resource, ns, name, version string) string {
`, resource, name, ns, testRepositoryURL, version)
}

func testAccHelmReleaseConfigBasicWithLabel(resource, ns, name, version, labelKey string, labelValue any) string {
return fmt.Sprintf(`
resource "helm_release" "%s" {
name = %q
namespace = %q
description = "Test"
repository = %q
chart = "test-chart"
version = %q
labels = {
"%s" = %v
}
set {
name = "foo"
value = "bar"
}
set {
name = "fizz"
value = 1337
}
}
`, resource, name, ns, testRepositoryURL, version, labelKey, labelValue)
}

func testAccHelmReleaseConfigEmptyVersion(resource, ns, name string) string {
return fmt.Sprintf(`
resource "helm_release" "%s" {
Expand Down

0 comments on commit 15b0f04

Please sign in to comment.