From b1f94675e97b60e8f10e0d973c02ac36e10bb578 Mon Sep 17 00:00:00 2001
From: The Magician <magic-modules@google.com>
Date: Mon, 27 Apr 2020 20:34:07 -0400
Subject: [PATCH] Add validation to label keys for cloud functions (#3431)
 (#2009)

Signed-off-by: Modular Magician <magic-modules@google.com>
---
 .changelog/3431.txt                           |  3 ++
 .../resource_cloudfunctions_function.go       | 24 +++++++++-
 .../resource_cloudfunctions_function_test.go  | 45 +++++++++++++++++++
 .../r/cloudfunctions_function.html.markdown   |  4 +-
 4 files changed, 72 insertions(+), 4 deletions(-)
 create mode 100644 .changelog/3431.txt

diff --git a/.changelog/3431.txt b/.changelog/3431.txt
new file mode 100644
index 0000000000..5df2a21df1
--- /dev/null
+++ b/.changelog/3431.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+cloudfunctions: Added validation to label keys for `google_cloudfunctions_function` as API errors aren't useful.
+```
diff --git a/google-beta/resource_cloudfunctions_function.go b/google-beta/resource_cloudfunctions_function.go
index 6fd92aa760..cc778c31aa 100644
--- a/google-beta/resource_cloudfunctions_function.go
+++ b/google-beta/resource_cloudfunctions_function.go
@@ -1,6 +1,8 @@
 package google
 
 import (
+	"regexp"
+
 	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
 	"google.golang.org/api/cloudfunctions/v1"
@@ -50,6 +52,23 @@ func (s *cloudFunctionId) cloudFunctionId() string {
 	return fmt.Sprintf("projects/%s/locations/%s/functions/%s", s.Project, s.Region, s.Name)
 }
 
+// matches all international lower case letters, number, underscores and dashes.
+var labelKeyRegex = regexp.MustCompile(`^[\p{Ll}0-9_-]+$`)
+
+func labelKeyValidator(val interface{}, key string) (warns []string, errs []error) {
+	if val == nil {
+		return
+	}
+
+	m := val.(map[string]interface{})
+	for k := range m {
+		if !labelKeyRegex.MatchString(k) {
+			errs = append(errs, fmt.Errorf("%q is an invalid label key. See https://cloud.google.com/resource-manager/docs/creating-managing-labels#requirements", k))
+		}
+	}
+	return
+}
+
 func (s *cloudFunctionId) locationId() string {
 	return fmt.Sprintf("projects/%s/locations/%s", s.Project, s.Region)
 }
@@ -193,8 +212,9 @@ func resourceCloudFunctionsFunction() *schema.Resource {
 			},
 
 			"labels": {
-				Type:     schema.TypeMap,
-				Optional: true,
+				Type:         schema.TypeMap,
+				ValidateFunc: labelKeyValidator,
+				Optional:     true,
 			},
 
 			"runtime": {
diff --git a/google-beta/resource_cloudfunctions_function_test.go b/google-beta/resource_cloudfunctions_function_test.go
index e75c20508f..3b04875c33 100644
--- a/google-beta/resource_cloudfunctions_function_test.go
+++ b/google-beta/resource_cloudfunctions_function_test.go
@@ -71,6 +71,51 @@ func TestCloudFunctionsFunction_nameValidator(t *testing.T) {
 	}
 }
 
+func TestValidLabelKeys(t *testing.T) {
+	testCases := []struct {
+		labelKey string
+		valid    bool
+	}{
+		{
+			"test-label", true,
+		},
+		{
+			"test_label", true,
+		},
+		{
+			"MixedCase", false,
+		},
+		{
+			"number-09-dash", true,
+		},
+		{
+			"", false,
+		},
+		{
+			"test-label", true,
+		},
+		{
+			"mixed*symbol", false,
+		},
+		{
+			"intérnätional", true,
+		},
+	}
+
+	for _, tc := range testCases {
+		labels := make(map[string]interface{})
+		labels[tc.labelKey] = "test value"
+
+		_, errs := labelKeyValidator(labels, "")
+		if tc.valid && len(errs) > 0 {
+			t.Errorf("Validation failure, key: '%s' should be valid but actual errors were %q", tc.labelKey, errs)
+		}
+		if !tc.valid && len(errs) < 1 {
+			t.Errorf("Validation failure, key: '%s' should fail but actual errors were %q", tc.labelKey, errs)
+		}
+	}
+}
+
 func TestAccCloudFunctionsFunction_basic(t *testing.T) {
 	t.Parallel()
 
diff --git a/website/docs/r/cloudfunctions_function.html.markdown b/website/docs/r/cloudfunctions_function.html.markdown
index 13aa62af6a..d2766fa4bd 100644
--- a/website/docs/r/cloudfunctions_function.html.markdown
+++ b/website/docs/r/cloudfunctions_function.html.markdown
@@ -125,7 +125,7 @@ Eg. `"nodejs8"`, `"nodejs10"`, `"python37"`, `"go111"`.
 
 * `ingress_settings` - (Optional) String value that controls what traffic can reach the function. Allowed values are ALLOW_ALL and ALLOW_INTERNAL_ONLY. Changes to this field will recreate the cloud function.
 
-* `labels` - (Optional) A set of key/value label pairs to assign to the function.
+* `labels` - (Optional) A set of key/value label pairs to assign to the function. Label keys must follow the requirements at https://cloud.google.com/resource-manager/docs/creating-managing-labels#requirements.
 
 * `service_account_email` - (Optional) If provided, the self-provided service account to run the function with.
 
@@ -147,7 +147,7 @@ Eg. `"nodejs8"`, `"nodejs10"`, `"python37"`, `"go111"`.
 The `event_trigger` block supports:
 
 * `event_type` - (Required) The type of event to observe. For example: `"google.storage.object.finalize"`.
-See the documentation on [calling Cloud Functions](https://cloud.google.com/functions/docs/calling/) for a 
+See the documentation on [calling Cloud Functions](https://cloud.google.com/functions/docs/calling/) for a
 full reference of accepted triggers.
 
 * `resource` - (Required) Required. The name or partial URI of the resource from