Skip to content

Commit fe2cad3

Browse files
authored
Merge pull request #1678 from srm09/automated-cherry-pick-of-#1675-release-1.3
🐛 Automated cherry pick of #1675: Adds label validation & sanitization logic
2 parents 6ae3166 + da37d85 commit fe2cad3

File tree

3 files changed

+197
-1
lines changed

3 files changed

+197
-1
lines changed

controllers/vspheremachine_controller.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3030
"k8s.io/apimachinery/pkg/runtime"
3131
apitypes "k8s.io/apimachinery/pkg/types"
32+
"k8s.io/apimachinery/pkg/util/validation"
3233
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
3334
clusterutilv1 "sigs.k8s.io/cluster-api/util"
3435
"sigs.k8s.io/cluster-api/util/annotations"
@@ -59,6 +60,8 @@ import (
5960
"sigs.k8s.io/cluster-api-provider-vsphere/pkg/util"
6061
)
6162

63+
const hostInfoErrStr = "host info cannot be used as a label value"
64+
6265
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=vspheremachines,verbs=get;list;watch;create;update;patch;delete
6366
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=vspheremachines/status,verbs=get;update;patch
6467
// +kubebuilder:rbac:groups=vmware.infrastructure.cluster.x-k8s.io,resources=vspheremachines,verbs=get;list;watch;create;update;patch;delete
@@ -345,14 +348,22 @@ func (r *machineReconciler) patchMachineLabelsWithHostInfo(ctx context.MachineCo
345348
return err
346349
}
347350

351+
info := util.SanitizeHostInfoLabel(hostInfo)
352+
errs := validation.IsValidLabelValue(info)
353+
if len(errs) > 0 {
354+
err := errors.Errorf("%s: %s", hostInfoErrStr, strings.Join(errs, ","))
355+
r.Logger.Error(err, hostInfoErrStr, "info", hostInfo)
356+
return err
357+
}
358+
348359
machine := ctx.GetMachine()
349360
patchHelper, err := patch.NewHelper(machine, r.Client)
350361
if err != nil {
351362
return err
352363
}
353364

354365
labels := machine.GetLabels()
355-
labels[constants.ESXiHostInfoLabel] = hostInfo
366+
labels[constants.ESXiHostInfoLabel] = info
356367
machine.Labels = labels
357368

358369
return patchHelper.Patch(r, machine)

pkg/util/label.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package util
18+
19+
import (
20+
"fmt"
21+
"net"
22+
"strings"
23+
24+
"k8s.io/apimachinery/pkg/util/validation"
25+
)
26+
27+
// SanitizeHostInfoLabel ensures that the ESXi host information passed as a parameter confirms to
28+
// the label value constraints documented at
29+
// https://k8s.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
30+
//
31+
// The expected inputs for the object are IP addresses or FQDNs of the ESXi hosts.
32+
func SanitizeHostInfoLabel(info string) string {
33+
updatedInfo := stripZoneInfo(info)
34+
ip := net.ParseIP(updatedInfo)
35+
if ip != nil {
36+
if ipv4 := ip.To4(); ipv4 == nil {
37+
// In case of an IPv6 address, replace `:` with the acceptable `-` character.
38+
// The size for the string would never exceed 52 (8 * 4 (address) + 7 (dashes) + 13 (suffix) = 52) characters.
39+
return fmt.Sprintf("%s.ipv6-literal", strings.ReplaceAll(updatedInfo, ":", "-"))
40+
}
41+
return updatedInfo
42+
}
43+
return truncateLabelLength(info)
44+
}
45+
46+
// stripZoneInfo removes the zone info from an IPv6 address.
47+
// This might not be exactly relevant since zone is used for link-local addresses and
48+
// would not be meaningful outside the host.
49+
// TODO (srm09): consider removing it in the future.
50+
func stripZoneInfo(info string) string {
51+
idx := strings.LastIndex(info, "%")
52+
if idx == -1 {
53+
return info
54+
}
55+
return info[:idx]
56+
}
57+
58+
func truncateLabelLength(inputURL string) string {
59+
if len(inputURL) <= validation.LabelValueMaxLength {
60+
return inputURL
61+
}
62+
63+
for {
64+
pos := strings.LastIndex(inputURL, ".")
65+
if pos == -1 {
66+
return inputURL[:validation.LabelValueMaxLength]
67+
}
68+
inputURL = inputURL[0:pos]
69+
if len(inputURL) <= validation.LabelValueMaxLength {
70+
break
71+
}
72+
}
73+
return inputURL
74+
}

pkg/util/label_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package util
18+
19+
import (
20+
"testing"
21+
22+
"github.com/onsi/gomega"
23+
)
24+
25+
func TestSanitizeHostInfoLabel(t *testing.T) {
26+
t.Run("for IP addresses", testIPAddressLogic)
27+
t.Run("for DNS entries", testDNSLogic)
28+
}
29+
30+
func testIPAddressLogic(t *testing.T) {
31+
tests := []struct {
32+
name, input, expected string
33+
}{
34+
{
35+
name: "for valid IPv4 address",
36+
input: "1.2.3.4",
37+
expected: "1.2.3.4",
38+
},
39+
{
40+
name: "for valid IPv6 address",
41+
input: "2620:124:6020:c003:0:69ff:fe59:80ac",
42+
expected: "2620-124-6020-c003-0-69ff-fe59-80ac.ipv6-literal",
43+
},
44+
{
45+
name: "for a shorthand valid IPv6 address",
46+
input: "2620::c003:0:69ff:fe59:80ac",
47+
expected: "2620--c003-0-69ff-fe59-80ac.ipv6-literal",
48+
},
49+
{
50+
name: "for a valid IPv6 address with zone index",
51+
input: "2620:124:6020:c003:0:69ff:fe59:80ac%3",
52+
expected: "2620-124-6020-c003-0-69ff-fe59-80ac.ipv6-literal",
53+
},
54+
}
55+
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
g := gomega.NewGomegaWithT(t)
59+
g.Expect(SanitizeHostInfoLabel(tt.input)).To(gomega.Equal(tt.expected))
60+
})
61+
}
62+
}
63+
64+
func testDNSLogic(t *testing.T) {
65+
tests := []struct {
66+
name, input, expected string
67+
}{
68+
{
69+
name: "for DNS entry with less than 63 characters",
70+
input: "foo-1.bar.com",
71+
expected: "foo-1.bar.com",
72+
},
73+
{
74+
name: "for DNS entry with 63 characters",
75+
input: "esx13-r09.p01.1d91f0ee4f14b7e83bd42.australiaeast.avs.belch.com",
76+
expected: "esx13-r09.p01.1d91f0ee4f14b7e83bd42.australiaeast.avs.belch.com",
77+
},
78+
{
79+
name: "for DNS entry with > 63 characters",
80+
input: "esx13-r09.p01.1d91f0ee4f14b7e83bd420.australiaeast.avs.belch.com",
81+
expected: "esx13-r09.p01.1d91f0ee4f14b7e83bd420.australiaeast.avs.belch",
82+
},
83+
{
84+
name: "for DNS entry with > 63 characters with multiple segments dropped",
85+
input: "esx13-r09.p01.1d91f0ee4f14b7e83bd420.australiaeast.avs.belch.com.us",
86+
expected: "esx13-r09.p01.1d91f0ee4f14b7e83bd420.australiaeast.avs.belch",
87+
},
88+
{
89+
name: "for DNS entry with > 63 characters with length = 63 after truncation",
90+
input: "esx13-r09.p01.1d91f0ee4f14b7e83bd420az.australiaeast.avs.belch.us",
91+
expected: "esx13-r09.p01.1d91f0ee4f14b7e83bd420az.australiaeast.avs.belch",
92+
},
93+
{
94+
name: "for DNS entry with > 63 characters with first segment > 63 characters",
95+
input: "esx-zcvU3CecjX8Tr5qXQgztj9ZKCp369p3hLFdzAu8VwEyWGq4hzkLTNZq089TI.p01.1d91f0ee4f14b7e83bd420.australiaeast.avs.belch.com",
96+
expected: "esx-zcvU3CecjX8Tr5qXQgztj9ZKCp369p3hLFdzAu8VwEyWGq4hzkLTNZq089T",
97+
},
98+
{
99+
name: "for DNS entry with > 63 characters with first segment = 63 characters",
100+
input: "esx-zcvU3CecjX8Tr5qXQgztj9ZKCp369p3hLFdzAu8VwEyWGq4hzkLTNZq089T.p01.1d91f0ee4f14b7e83bd420.australiaeast.avs.belch.com",
101+
expected: "esx-zcvU3CecjX8Tr5qXQgztj9ZKCp369p3hLFdzAu8VwEyWGq4hzkLTNZq089T",
102+
},
103+
}
104+
105+
for _, tt := range tests {
106+
t.Run(tt.name, func(t *testing.T) {
107+
g := gomega.NewGomegaWithT(t)
108+
g.Expect(SanitizeHostInfoLabel(tt.input)).To(gomega.Equal(tt.expected))
109+
})
110+
}
111+
}

0 commit comments

Comments
 (0)