join: make Azure instance names k8s compliant (#807)

join: make Azure instance names k8s compliant
This commit is contained in:
3u13r 2022-12-23 18:59:15 +01:00 committed by GitHub
parent edd51cb137
commit d1195d1d5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 23 deletions

View File

@ -14,6 +14,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -36,6 +37,8 @@ import (
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
) )
var validHostnameRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
// configReader provides kubeconfig as []byte. // configReader provides kubeconfig as []byte.
type configReader interface { type configReader interface {
ReadKubeconfig() ([]byte, error) ReadKubeconfig() ([]byte, error)
@ -110,7 +113,11 @@ func (k *KubeWrapper) InitCluster(
if instance.VPCIP != "" { if instance.VPCIP != "" {
validIPs = append(validIPs, net.ParseIP(instance.VPCIP)) validIPs = append(validIPs, net.ParseIP(instance.VPCIP))
} }
nodeName := k8sCompliantHostname(instance.Name) nodeName, err := k8sCompliantHostname(instance.Name)
if err != nil {
return nil, fmt.Errorf("generating node name: %w", err)
}
nodeIP := instance.VPCIP nodeIP := instance.VPCIP
subnetworkPodCIDR := instance.SecondaryIPRange subnetworkPodCIDR := instance.SecondaryIPRange
if len(instance.AliasIPRanges) > 0 { if len(instance.AliasIPRanges) > 0 {
@ -278,7 +285,10 @@ func (k *KubeWrapper) JoinCluster(ctx context.Context, args *kubeadm.BootstrapTo
} }
providerID := instance.ProviderID providerID := instance.ProviderID
nodeInternalIP := instance.VPCIP nodeInternalIP := instance.VPCIP
nodeName := k8sCompliantHostname(instance.Name) nodeName, err := k8sCompliantHostname(instance.Name)
if err != nil {
return fmt.Errorf("generating node name: %w", err)
}
loadbalancerEndpoint, err := k.providerMetadata.GetLoadBalancerEndpoint(ctx) loadbalancerEndpoint, err := k.providerMetadata.GetLoadBalancerEndpoint(ctx)
if err != nil { if err != nil {
@ -401,10 +411,13 @@ func (k *KubeWrapper) setupInternalConfigMap(ctx context.Context, azureCVM strin
// k8sCompliantHostname transforms a hostname to an RFC 1123 compliant, lowercase subdomain as required by Kubernetes node names. // k8sCompliantHostname transforms a hostname to an RFC 1123 compliant, lowercase subdomain as required by Kubernetes node names.
// The following regex is used by k8s for validation: /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/ . // The following regex is used by k8s for validation: /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/ .
// Only a simple heuristic is used for now (to lowercase, replace underscores). // Only a simple heuristic is used for now (to lowercase, replace underscores).
func k8sCompliantHostname(in string) string { func k8sCompliantHostname(in string) (string, error) {
hostname := strings.ToLower(in) hostname := strings.ToLower(in)
hostname = strings.ReplaceAll(hostname, "_", "-") hostname = strings.ReplaceAll(hostname, "_", "-")
return hostname if !validHostnameRegex.MatchString(hostname) {
return "", fmt.Errorf("failed to generate a Kubernetes compliant hostname for %s", in)
}
return hostname, nil
} }
// StartKubelet starts the kubelet service. // StartKubelet starts the kubelet service.

View File

@ -10,7 +10,6 @@ import (
"context" "context"
"errors" "errors"
"net" "net"
"regexp"
"strconv" "strconv"
"testing" "testing"
@ -465,22 +464,32 @@ func TestJoinCluster(t *testing.T) {
} }
func TestK8sCompliantHostname(t *testing.T) { func TestK8sCompliantHostname(t *testing.T) {
compliantHostname := regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
testCases := map[string]struct { testCases := map[string]struct {
hostname string input string
wantHostname string expected string
wantErr bool
}{ }{
"azure scale set names work": { "no change": {
hostname: "constellation-scale-set-bootstrappers-name_0", input: "test",
wantHostname: "constellation-scale-set-bootstrappers-name-0", expected: "test",
}, },
"compliant hostname is not modified": { "uppercase": {
hostname: "abcd-123", input: "TEST",
wantHostname: "abcd-123", expected: "test",
}, },
"uppercase hostnames are lowercased": { "underscore": {
hostname: "ABCD", input: "test_node",
wantHostname: "abcd", expected: "test-node",
},
"empty": {
input: "",
expected: "",
wantErr: true,
},
"error": {
input: "test_node_",
expected: "",
wantErr: true,
}, },
} }
@ -488,10 +497,13 @@ func TestK8sCompliantHostname(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
hostname := k8sCompliantHostname(tc.hostname) actual, err := k8sCompliantHostname(tc.input)
if tc.wantErr {
assert.Equal(tc.wantHostname, hostname) assert.Error(err)
assert.Regexp(compliantHostname, hostname) return
}
assert.NoError(err)
assert.Equal(tc.expected, actual)
}) })
} }
} }

View File

@ -10,6 +10,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp"
"strings"
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
@ -85,7 +87,14 @@ func (c *Client) CreateConfigMap(ctx context.Context, configMap corev1.ConfigMap
func (c *Client) AddNodeToJoiningNodes(ctx context.Context, nodeName string, componentsHash string, isControlPlane bool) error { func (c *Client) AddNodeToJoiningNodes(ctx context.Context, nodeName string, componentsHash string, isControlPlane bool) error {
joiningNode := &unstructured.Unstructured{} joiningNode := &unstructured.Unstructured{}
objectMetadataName := nodeName compliantNodeName, err := k8sCompliantHostname(nodeName)
if err != nil {
return fmt.Errorf("failed to get k8s compliant hostname: %w", err)
}
// JoiningNodes referencing a worker node are named after the worker node.
// JoiningNodes referencing the control-plane node are named "control-plane".
objectMetadataName := compliantNodeName
deadline := metav1.NewTime(time.Now().Add(48 * time.Hour)) deadline := metav1.NewTime(time.Now().Add(48 * time.Hour))
if isControlPlane { if isControlPlane {
objectMetadataName = "control-plane" objectMetadataName = "control-plane"
@ -99,7 +108,7 @@ func (c *Client) AddNodeToJoiningNodes(ctx context.Context, nodeName string, com
"name": objectMetadataName, "name": objectMetadataName,
}, },
"spec": map[string]any{ "spec": map[string]any{
"name": nodeName, "name": compliantNodeName,
"componentshash": componentsHash, "componentshash": componentsHash,
"iscontrolplane": isControlPlane, "iscontrolplane": isControlPlane,
"deadline": deadline, "deadline": deadline,
@ -142,3 +151,16 @@ func (c *Client) AddReferenceToK8sVersionConfigMap(ctx context.Context, k8sVersi
} }
return nil return nil
} }
var validHostnameRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
// k8sCompliantHostname transforms a hostname to an RFC 1123 compliant, lowercase subdomain as required by Kubernetes node names.
// Only a simple heuristic is used for now (to lowercase, replace underscores).
func k8sCompliantHostname(in string) (string, error) {
hostname := strings.ToLower(in)
hostname = strings.ReplaceAll(hostname, "_", "-")
if !validHostnameRegex.MatchString(hostname) {
return "", fmt.Errorf("failed to generate a Kubernetes compliant hostname for %s", in)
}
return hostname, nil
}

View File

@ -0,0 +1,63 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package kubernetes
import (
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestK8sCompliantHostname(t *testing.T) {
testCases := map[string]struct {
input string
expected string
wantErr bool
}{
"no change": {
input: "test",
expected: "test",
},
"uppercase": {
input: "TEST",
expected: "test",
},
"underscore": {
input: "test_node",
expected: "test-node",
},
"empty": {
input: "",
expected: "",
wantErr: true,
},
"error": {
input: "test_node_",
expected: "",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
actual, err := k8sCompliantHostname(tc.input)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.expected, actual)
})
}
}