From 15b0f0466b524c5a59f5dc5bff4cb969ec2821ae Mon Sep 17 00:00:00 2001 From: Adrien Fillon Date: Fri, 24 May 2024 12:13:43 +0200 Subject: [PATCH] Add support for labeling a release Fixes #1323 Signed-off-by: Adrien Fillon --- .changelog/1372.txt | 3 ++ helm/resource_release.go | 75 +++++++++++++++++++++++++++++++++ helm/resource_release_test.go | 78 ++++++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 .changelog/1372.txt diff --git a/.changelog/1372.txt b/.changelog/1372.txt new file mode 100644 index 000000000..b51819869 --- /dev/null +++ b/.changelog/1372.txt @@ -0,0 +1,3 @@ +```release-note:feature +Add support for labeling a release +``` diff --git a/helm/resource_release.go b/helm/resource_release.go index 80d1833ac..e0819650c 100644 --- a/helm/resource_release.go +++ b/helm/resource_release.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "path" + "slices" "strings" "time" @@ -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" ) @@ -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, @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 +} diff --git a/helm/resource_release_test.go b/helm/resource_release_test.go index babad0c4b..40257e46a 100644 --- a/helm/resource_release_test.go +++ b/helm/resource_release_test.go @@ -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"), ), }, { @@ -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") @@ -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 @@ -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"), ), }, }, @@ -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" @@ -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" {