mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 12:16:44 +02:00
cmd/k8s-operator: move TruncateLabelValue to shared k8s-operator package
Move the label truncation helper to k8s-operator/utils.go so it can be reused by other components that need to produce valid Kubernetes labels. Signed-off-by: Daniel Pañeda <daniel.paneda@clickhouse.com> Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
This commit is contained in:
parent
4b12f9c6af
commit
37dda42f75
@ -7,8 +7,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
@ -21,6 +19,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
kube "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/kube/kubetypes"
|
||||
)
|
||||
@ -229,13 +228,13 @@ func metricsResourceLabels(opts *metricsOpts) map[string]string {
|
||||
kubetypes.LabelManaged: "true",
|
||||
labelMetricsTarget: opts.proxyStsName,
|
||||
labelPromProxyType: opts.proxyType,
|
||||
labelPromProxyParentName: truncateLabelValue(opts.proxyLabels[LabelParentName]),
|
||||
labelPromProxyParentName: kube.TruncateLabelValue(opts.proxyLabels[LabelParentName]),
|
||||
}
|
||||
// Include namespace label for proxies created for a namespaced type.
|
||||
if isNamespacedProxyType(opts.proxyType) {
|
||||
lbls[labelPromProxyParentNamespace] = truncateLabelValue(opts.proxyLabels[LabelParentNamespace])
|
||||
lbls[labelPromProxyParentNamespace] = kube.TruncateLabelValue(opts.proxyLabels[LabelParentNamespace])
|
||||
}
|
||||
lbls[labelPromJob] = truncateLabelValue(promJobName(opts))
|
||||
lbls[labelPromJob] = kube.TruncateLabelValue(promJobName(opts))
|
||||
return lbls
|
||||
}
|
||||
|
||||
@ -252,11 +251,11 @@ func promJobName(opts *metricsOpts) string {
|
||||
func metricsSvcSelector(proxyLabels map[string]string, proxyType string) map[string]string {
|
||||
sel := map[string]string{
|
||||
labelPromProxyType: proxyType,
|
||||
labelPromProxyParentName: truncateLabelValue(proxyLabels[LabelParentName]),
|
||||
labelPromProxyParentName: kube.TruncateLabelValue(proxyLabels[LabelParentName]),
|
||||
}
|
||||
// Include namespace label for proxies created for a namespaced type.
|
||||
if isNamespacedProxyType(proxyType) {
|
||||
sel[labelPromProxyParentNamespace] = truncateLabelValue(proxyLabels[LabelParentNamespace])
|
||||
sel[labelPromProxyParentNamespace] = kube.TruncateLabelValue(proxyLabels[LabelParentNamespace])
|
||||
}
|
||||
return sel
|
||||
}
|
||||
@ -287,20 +286,6 @@ func isNamespacedProxyType(typ string) bool {
|
||||
return typ == proxyTypeIngressResource || typ == proxyTypeIngressService
|
||||
}
|
||||
|
||||
// truncateLabelValue truncates a Kubernetes label value to fit within the
|
||||
// 63-character limit. If the value exceeds the limit, it is truncated and a
|
||||
// short hash suffix is appended to preserve uniqueness.
|
||||
func truncateLabelValue(val string) string {
|
||||
const maxLen = 63
|
||||
if len(val) <= maxLen {
|
||||
return val
|
||||
}
|
||||
hash := sha256.Sum256([]byte(val))
|
||||
suffix := hex.EncodeToString(hash[:4]) // 8 hex chars
|
||||
truncated := val[:maxLen-len(suffix)-1]
|
||||
return truncated + "-" + suffix
|
||||
}
|
||||
|
||||
func mergeMapKeys(a, b map[string]string) map[string]string {
|
||||
m := make(map[string]string, len(a)+len(b))
|
||||
maps.Copy(m, b)
|
||||
|
||||
@ -4,75 +4,3 @@
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTruncateLabelValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string // empty means expect input unchanged
|
||||
}{
|
||||
{
|
||||
name: "short value unchanged",
|
||||
input: "my-service",
|
||||
},
|
||||
{
|
||||
name: "exactly 63 chars unchanged",
|
||||
input: strings.Repeat("a", 63),
|
||||
},
|
||||
{
|
||||
name: "64 chars gets truncated",
|
||||
input: strings.Repeat("a", 64),
|
||||
},
|
||||
{
|
||||
name: "very long value gets truncated",
|
||||
input: "tailscale-nginx-clickhouse-o11y-server-https-with-extra-long-suffix-that-exceeds-limit",
|
||||
},
|
||||
{
|
||||
name: "253 chars (max k8s resource name)",
|
||||
input: strings.Repeat("x", 253),
|
||||
},
|
||||
{
|
||||
name: "empty string unchanged",
|
||||
input: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := truncateLabelValue(tt.input)
|
||||
if len(got) > 63 {
|
||||
t.Errorf("truncateLabelValue(%q) = %q (len %d), exceeds 63 chars", tt.input, got, len(got))
|
||||
}
|
||||
if len(tt.input) <= 63 && got != tt.input {
|
||||
t.Errorf("truncateLabelValue(%q) = %q, want unchanged input", tt.input, got)
|
||||
}
|
||||
if len(tt.input) > 63 && got == tt.input {
|
||||
t.Errorf("truncateLabelValue(%q) was not truncated", tt.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateLabelValueDeterministic(t *testing.T) {
|
||||
input := strings.Repeat("a", 100)
|
||||
first := truncateLabelValue(input)
|
||||
for i := 0; i < 10; i++ {
|
||||
got := truncateLabelValue(input)
|
||||
if got != first {
|
||||
t.Fatalf("non-deterministic: got %q, want %q", got, first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateLabelValueUniqueness(t *testing.T) {
|
||||
// Two inputs sharing a long prefix but differing at the end should produce different outputs.
|
||||
a := strings.Repeat("a", 100) + "-one"
|
||||
b := strings.Repeat("a", 100) + "-two"
|
||||
if truncateLabelValue(a) == truncateLabelValue(b) {
|
||||
t.Errorf("collision: %q and %q produce the same truncated label", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
@ -50,3 +52,17 @@ func CapVerFromFileName(name string) (tailcfg.CapabilityVersion, error) {
|
||||
_, err := fmt.Sscanf(name, "cap-%d.hujson", &cap)
|
||||
return cap, err
|
||||
}
|
||||
|
||||
// TruncateLabelValue truncates a Kubernetes label value to fit within the
|
||||
// 63-character limit. If the value exceeds the limit, it is truncated and a
|
||||
// short hash suffix is appended to preserve uniqueness.
|
||||
func TruncateLabelValue(val string) string {
|
||||
const maxLen = 63
|
||||
if len(val) <= maxLen {
|
||||
return val
|
||||
}
|
||||
hash := sha256.Sum256([]byte(val))
|
||||
suffix := hex.EncodeToString(hash[:4]) // 8 hex chars
|
||||
truncated := val[:maxLen-len(suffix)-1]
|
||||
return truncated + "-" + suffix
|
||||
}
|
||||
|
||||
78
k8s-operator/utils_test.go
Normal file
78
k8s-operator/utils_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package kube
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTruncateLabelValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string // empty means expect input unchanged
|
||||
}{
|
||||
{
|
||||
name: "short value unchanged",
|
||||
input: "my-service",
|
||||
},
|
||||
{
|
||||
name: "exactly 63 chars unchanged",
|
||||
input: strings.Repeat("a", 63),
|
||||
},
|
||||
{
|
||||
name: "64 chars gets truncated",
|
||||
input: strings.Repeat("a", 64),
|
||||
},
|
||||
{
|
||||
name: "very long value gets truncated",
|
||||
input: "tailscale-nginx-clickhouse-o11y-server-https-with-extra-long-suffix-that-exceeds-limit",
|
||||
},
|
||||
{
|
||||
name: "253 chars (max k8s resource name)",
|
||||
input: strings.Repeat("x", 253),
|
||||
},
|
||||
{
|
||||
name: "empty string unchanged",
|
||||
input: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := TruncateLabelValue(tt.input)
|
||||
if len(got) > 63 {
|
||||
t.Errorf("TruncateLabelValue(%q) = %q (len %d), exceeds 63 chars", tt.input, got, len(got))
|
||||
}
|
||||
if len(tt.input) <= 63 && got != tt.input {
|
||||
t.Errorf("TruncateLabelValue(%q) = %q, want unchanged input", tt.input, got)
|
||||
}
|
||||
if len(tt.input) > 63 && got == tt.input {
|
||||
t.Errorf("TruncateLabelValue(%q) was not truncated", tt.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateLabelValueDeterministic(t *testing.T) {
|
||||
input := strings.Repeat("a", 100)
|
||||
first := TruncateLabelValue(input)
|
||||
for i := 0; i < 10; i++ {
|
||||
got := TruncateLabelValue(input)
|
||||
if got != first {
|
||||
t.Fatalf("non-deterministic: got %q, want %q", got, first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateLabelValueUniqueness(t *testing.T) {
|
||||
// Two inputs sharing a long prefix but differing at the end should produce different outputs.
|
||||
a := strings.Repeat("a", 100) + "-one"
|
||||
b := strings.Repeat("a", 100) + "-two"
|
||||
if TruncateLabelValue(a) == TruncateLabelValue(b) {
|
||||
t.Errorf("collision: %q and %q produce the same truncated label", a, b)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user