Repository: kubernetes-csi/external-provisioner (go)

Issue:
distributed provisioner: managed-by label can fail if node name too long
The value of the `csi.storage.k8s.io/managed-by` label when the external provisioner had enableNodeDeployment set can be too large if the node name is too long. The value is calculated [here](https://github.com/kubernetes-csi/external-provisioner/blob/master/cmd/csi-provisioner/csi-provisioner.go#L447-L450) and then passed to the capacity controller. The capacity controller sets that value as a label on the CsiStorageCapacity object as is [here](https://github.com/kubernetes-csi/external-provisioner/blob/master/pkg/capacity/capacity.go#L622). Labels have a maximum length of 63 characters and if the node name is long enough, combined with the `external-provisioner` prefix on the label value, it will be too large and the lister will fail.

My suggestion would be to hash the value and put that as the label value, and then use the same hash function in the [check](https://github.com/kubernetes-csi/external-provisioner/blob/master/pkg/capacity/capacity.go#L833) that confirms the right managed by label is set.

Pull request description:
[release-3.1] backport 'fix managed-by label being too long when the node name is long.'
Signed-off-by: Alexander Wels <awels@redhat.com>

<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Follow the instructions for writing a release note: https://git.k8s.io/community/contributors/guide/release-notes.md
3. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

**What type of PR is this?**
/kind bug

**What this PR does / why we need it**:
Cherry-pick of #717 for the release-3.1 branch, as discussed in https://github.com/kubernetes-csi/external-provisioner/pull/717#issuecomment-1126920943.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #707

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note
storage capacity: managed-by label potentially too long with a long node name and enableNodeDeployment set to true
```


Patch:
diff --git a/cmd/csi-provisioner/csi-provisioner.go b/cmd/csi-provisioner/csi-provisioner.go
index 8b14a80da..d70a818ff 100644
--- a/cmd/csi-provisioner/csi-provisioner.go
+++ b/cmd/csi-provisioner/csi-provisioner.go
@@ -37,6 +37,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	validation "k8s.io/apimachinery/pkg/util/validation"
 	utilfeature "k8s.io/apiserver/pkg/util/feature"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes"
@@ -444,7 +445,7 @@ func main() {
 
 		managedByID := "external-provisioner"
 		if *enableNodeDeployment {
-			managedByID += "-" + node
+			managedByID = getNameWithMaxLength(managedByID, node, validation.DNS1035LabelMaxLength)
 		}
 
 		// We only need objects from our own namespace. The normal factory would give
diff --git a/cmd/csi-provisioner/util.go b/cmd/csi-provisioner/util.go
new file mode 100644
index 000000000..0b5924183
--- /dev/null
+++ b/cmd/csi-provisioner/util.go
@@ -0,0 +1,75 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"hash/fnv"
+)
+
+// getNameWithMaxLength returns a name given a base ("deployment-5") and a suffix ("deploy")
+// It will first attempt to join them with a dash. If the resulting name is longer
+// than maxLength: if the suffix is too long, it will truncate the base name and add
+// an 8-character hash of the [base]-[suffix] string.  If the suffix is not too long,
+// it will truncate the base, add the hash of the base and return [base]-[hash]-[suffix]
+// copied from https://github.com/openshift/library-go/blob/master/pkg/build/naming/namer.go
+func getNameWithMaxLength(base, suffix string, maxLength int) string {
+	name := fmt.Sprintf("%s-%s", base, suffix)
+	if len(name) <= maxLength {
+		return name
+	}
+
+	baseLength := maxLength - 10 /*length of -hash-*/ - len(suffix)
+
+	// if the suffix is too long, ignore it
+	if baseLength < 0 {
+		prefix := base[0:min(len(base), max(0, maxLength-9))]
+		// Calculate hash on initial base-suffix string
+		shortName := fmt.Sprintf("%s-%s", prefix, hash(name))
+		return shortName[:min(maxLength, len(shortName))]
+	}
+
+	prefix := base[0:baseLength]
+	// Calculate hash on initial base-suffix string
+	return fmt.Sprintf("%s-%s-%s", prefix, hash(base), suffix)
+}
+
+// hash calculates the hexadecimal representation (8-chars)
+// of the hash of the passed in string using the FNV-a algorithm
+func hash(s string) string {
+	hash := fnv.New32a()
+	hash.Write([]byte(s))
+	intHash := hash.Sum32()
+	result := fmt.Sprintf("%08x", intHash)
+	return result
+}
+
+// max returns the greater of its 2 inputs
+func max(a, b int) int {
+	if b > a {
+		return b
+	}
+	return a
+}
+
+// min returns the lesser of its 2 inputs
+func min(a, b int) int {
+	if b < a {
+		return b
+	}
+	return a
+}


Test patch:
diff --git a/cmd/csi-provisioner/util_test.go b/cmd/csi-provisioner/util_test.go
new file mode 100644
index 000000000..b43f775ea
--- /dev/null
+++ b/cmd/csi-provisioner/util_test.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+const (
+	externalProvisioner = "external-provisioner"
+)
+
+func TestGetNameWithMaxLength(t *testing.T) {
+	testcases := map[string]struct {
+		expected string
+		nodeName string
+	}{
+		"unchanged": {
+			expected: fmt.Sprintf("%s-%s", externalProvisioner, "node01"),
+			nodeName: "node01",
+		},
+		"exactly63": {
+			// 20 (external-provisioner) + 1 (-) + 4 (node) + 38 = 63
+			expected: fmt.Sprintf("%s-%s", externalProvisioner, fmt.Sprintf("node%s", strings.Repeat("a", 38))),
+			nodeName: fmt.Sprintf("node%s", strings.Repeat("a", 38)),
+		},
+		"one over": {
+			expected: fmt.Sprintf("%s-%s-%s", "external-p", "53f40b57", fmt.Sprintf("node%s", strings.Repeat("a", 39))),
+			nodeName: fmt.Sprintf("node%s", strings.Repeat("a", 39)),
+		},
+		"very long, ignore suffix": {
+			expected: fmt.Sprintf("%s-%s", externalProvisioner, "df38e37f"),
+			nodeName: fmt.Sprintf("node%s", strings.Repeat("a", 63)),
+		},
+	}
+
+	for name, c := range testcases {
+		t.Run(name, func(t *testing.T) {
+			expected := c.expected
+			res := getNameWithMaxLength(externalProvisioner, c.nodeName, 63)
+			if expected != res {
+				t.Errorf("Expected: %s, does not match result: %s", expected, res)
+			}
+		})
+	}
+}