diff --git a/.envrc.sample b/.envrc.sample index e334dd89a..0c42c57b4 100644 --- a/.envrc.sample +++ b/.envrc.sample @@ -4,6 +4,7 @@ export KUBECONFIG=$PWD/.mgt-cluster-kubeconfig.yaml export HCLOUD_TOKEN=... export HCLOUD_SSH_KEY=test export HCLOUD_REGION=fsn1 +export CAPH_LATEST_VERSION=v1.0.0-beta.35 export CONTROL_PLANE_MACHINE_COUNT=1 export WORKER_MACHINE_COUNT=1 export KUBERNETES_VERSION=v1.29.4 @@ -13,5 +14,7 @@ export HCLOUD_WORKER_MACHINE_TYPE=cpx31 export SSH_KEY=$HOME/.ssh/id_rsa.pub export HETZNER_SSH_PUB_PATH=$HOME/.ssh/id_rsa.pub export HETZNER_SSH_PRIV_PATH=$HOME/.ssh/id_rsa +export HETZNER_SSH_PUB=$(cat ${HETZNER_SSH_PUB_PATH} | base64 --wrap 0) +export HETZNER_SSH_PRIV=$(cat ${HETZNER_SSH_PRIV_PATH} | base64 --wrap 0) export HETZNER_ROBOT_USER= export HETZNER_ROBOT_PASSWORD= diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index 229e31ed0..f12fabc8a 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -179,6 +179,13 @@ type LoadBalancerSpec struct { // Region contains the name of the HCloud location where the load balancer is running. Region Region `json:"region,omitempty"` + + // UseIPv6Endpoint defines whether to use the LoadBalancer's IPv6 address as + // the cluster endpoint instead of IPv4. This is useful if nodes are provisioned + // without IPv4 address. Defaults to 'false'. + // +optional + // +kubebuilder:default=false + UseIPv6Endpoint bool `json:"useIPv6Endpoint,omitempty"` } // LoadBalancerServiceSpec defines a load balancer Target. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml index 717913133..6746dbc1a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml @@ -167,6 +167,13 @@ spec: - lb21 - lb31 type: string + useIPv6Endpoint: + default: false + description: UseIPv6Endpoint defines whether to use the LoadBalancer's + IPv6 address as the cluster endpoint instead of IPv4. This is + useful if nodes are provisioned without IPv4 address. Defaults + to 'false'. + type: boolean type: object controlPlaneRegions: description: |- diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml index 692139b74..4450e9858 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml @@ -197,6 +197,13 @@ spec: - lb21 - lb31 type: string + useIPv6Endpoint: + default: false + description: UseIPv6Endpoint defines whether to use the + LoadBalancer's IPv6 address as the cluster endpoint + instead of IPv4. This is useful if nodes are provisioned + without IPv4 address. Defaults to 'false'. + type: boolean type: object controlPlaneRegions: description: |- diff --git a/controllers/controllers_suite_test.go b/controllers/controllers_suite_test.go index c1002d207..9630409b4 100644 --- a/controllers/controllers_suite_test.go +++ b/controllers/controllers_suite_test.go @@ -154,9 +154,10 @@ func getDefaultHetznerClusterSpec() infrav1.HetznerClusterSpec { Protocol: "tcp", }, }, - Port: 6443, - Region: "fsn1", - Type: "lb11", + Port: 6443, + Region: "fsn1", + Type: "lb11", + UseIPv6Endpoint: false, }, ControlPlaneEndpoint: &clusterv1.APIEndpoint{}, ControlPlaneRegions: []infrav1.Region{"fsn1"}, diff --git a/controllers/hetznercluster_controller.go b/controllers/hetznercluster_controller.go index 8a1e7f89e..6350e65ad 100644 --- a/controllers/hetznercluster_controller.go +++ b/controllers/hetznercluster_controller.go @@ -267,8 +267,13 @@ func (r *HetznerClusterReconciler) reconcileNormal(ctx context.Context, clusterS func processControlPlaneEndpoint(hetznerCluster *infrav1.HetznerCluster) { if hetznerCluster.Spec.ControlPlaneLoadBalancer.Enabled { - if hetznerCluster.Status.ControlPlaneLoadBalancer.IPv4 != "" { - defaultHost := hetznerCluster.Status.ControlPlaneLoadBalancer.IPv4 + ip := hetznerCluster.Status.ControlPlaneLoadBalancer.IPv4 + if hetznerCluster.Spec.ControlPlaneLoadBalancer.UseIPv6Endpoint { + ip = hetznerCluster.Status.ControlPlaneLoadBalancer.IPv6 + } + + if ip != "" { + defaultHost := ip defaultPort := int32(hetznerCluster.Spec.ControlPlaneLoadBalancer.Port) if hetznerCluster.Spec.ControlPlaneEndpoint == nil { diff --git a/controllers/hetznercluster_controller_test.go b/controllers/hetznercluster_controller_test.go index ab0528851..741faf058 100644 --- a/controllers/hetznercluster_controller_test.go +++ b/controllers/hetznercluster_controller_test.go @@ -1208,4 +1208,178 @@ func TestSetControlPlaneEndpoint(t *testing.T) { t.Fatalf("return value should be true") } }) + + t.Run("return false if load balancer is enabled with UseIPv6Endpoint and IPv6 is 'nil'. ControlPlaneEndpoint should not change", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + }, + ControlPlaneEndpoint: nil, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv4: "xyz", + IPv6: "", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint != nil { + t.Fatalf("ControlPlaneEndpoint should not change. It should remain nil") + } + + if hetznerCluster.Status.Ready != false { + t.Fatalf("return value should be false") + } + + if !conditions.Has(hetznerCluster, infrav1.ControlPlaneEndpointSetCondition) { + t.Fatalf("ControlPlaneEndpointSetCondition should exist") + } + + condition := conditions.Get(hetznerCluster, infrav1.ControlPlaneEndpointSetCondition) + if condition.Status != corev1.ConditionFalse { + t.Fatalf("condition status should be false") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endpoint, IPv6 is not nil and ControlPlaneEndpoint is nil. Values of ControlPlaneEndpoint.Host and ControlPlaneEndpoint.Port will get updated", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: nil, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "abc" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'abc'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 11 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 11", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endopint, IPv6 is not nil, ControlPlaneEndpoint.Host is an empty string and ControlPlaneEndpoint.Port is 0. Values of ControlPlaneEndpoint.Host and ControlPlaneEndpoint.Port should update", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: &clusterv1.APIEndpoint{ + Host: "", + Port: 0, + }, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "abc" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'abc'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 11 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 11", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endpoint, IPv6 is not nil, ControlPlaneEndpoint.Host is an empty string and ControlPlaneEndpoint.Port is 21. Value of ControlPlaneEndpoint.Host will change and ControlPlaneEndpoint.Port should remain same", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: &clusterv1.APIEndpoint{ + Host: "", + Port: 21, + }, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "abc" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'abc'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 21 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 21", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endpoint, IPv6 is not nil, ControlPlaneEndpoint.Host is 'xyz' and ControlPlaneEndpoint.Port is 21. Value of ControlPlaneEndpoint.Host and ControlPlaneEndpoint.Port should remain unchanged", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: &clusterv1.APIEndpoint{ + Host: "xyz", + Port: 21, + }, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "xyz" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'xyz'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 21 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 21", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) } diff --git a/hack/kind-dev.sh b/hack/kind-dev.sh index 7b2932be8..26b510c50 100755 --- a/hack/kind-dev.sh +++ b/hack/kind-dev.sh @@ -45,8 +45,9 @@ kindV1Alpha4Cluster: - role: control-plane image: ghcr.io/fluxcd/kindest/node:${CLUSTER_VERSION}-amd64 networking: - podSubnet: "10.244.0.0/16" - serviceSubnet: "10.96.0.0/12" + podSubnet: "10.244.0.0/16,fd00:10:244::/56" + serviceSubnet: "10.96.0.0/12,fd00:10:96::/112" + ipFamily: dual EOF } diff --git a/templates/cluster-templates/bases/hcloud-hetznerCluster-network-ipv6-only.yaml b/templates/cluster-templates/bases/hcloud-hetznerCluster-network-ipv6-only.yaml new file mode 100644 index 000000000..f5b599fbd --- /dev/null +++ b/templates/cluster-templates/bases/hcloud-hetznerCluster-network-ipv6-only.yaml @@ -0,0 +1,24 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: HetznerCluster +metadata: + name: "${CLUSTER_NAME}" +spec: + hcloudNetwork: + enabled: true + controlPlaneRegions: + - "${HCLOUD_REGION}" + controlPlaneEndpoint: + host: "" + port: 443 + controlPlaneLoadBalancer: + region: "${HCLOUD_REGION}" + useIPv6Endpoint: true + sshKeys: + hcloud: + - name: "${HCLOUD_SSH_KEY}" + hetznerSecretRef: + name: hetzner + key: + hcloudToken: hcloud + hetznerRobotPassword: robot-password + hetznerRobotUser: robot-user diff --git a/templates/cluster-templates/bases/hcloud-mt-control-plane-ubuntu-ipv6-only.yaml b/templates/cluster-templates/bases/hcloud-mt-control-plane-ubuntu-ipv6-only.yaml new file mode 100644 index 000000000..6f6b0d60d --- /dev/null +++ b/templates/cluster-templates/bases/hcloud-mt-control-plane-ubuntu-ipv6-only.yaml @@ -0,0 +1,12 @@ +kind: HCloudMachineTemplate +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +metadata: + name: "${CLUSTER_NAME}-control-plane" +spec: + template: + spec: + type: "${HCLOUD_CONTROL_PLANE_MACHINE_TYPE}" + imageName: "ubuntu-22.04" + publicNetwork: + enableIPv4: false + enableIPv6: true diff --git a/templates/cluster-templates/bases/hcloud-mt-md-0-ubuntu-ipv6-only.yaml b/templates/cluster-templates/bases/hcloud-mt-md-0-ubuntu-ipv6-only.yaml new file mode 100644 index 000000000..22b17fb05 --- /dev/null +++ b/templates/cluster-templates/bases/hcloud-mt-md-0-ubuntu-ipv6-only.yaml @@ -0,0 +1,12 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: HCloudMachineTemplate +metadata: + name: "${CLUSTER_NAME}-md-0" +spec: + template: + spec: + type: "${HCLOUD_WORKER_MACHINE_TYPE}" + imageName: "ubuntu-22.04" + publicNetwork: + enableIPv4: false + enableIPv6: true diff --git a/templates/cluster-templates/hcloud-network-ipv6-only/kustomization.yaml b/templates/cluster-templates/hcloud-network-ipv6-only/kustomization.yaml new file mode 100644 index 000000000..980f718ea --- /dev/null +++ b/templates/cluster-templates/hcloud-network-ipv6-only/kustomization.yaml @@ -0,0 +1,14 @@ +bases: + - ../bases/capi-cluster-kubeadm.yaml + - ../bases/hcloud-hetznerCluster-network-ipv6-only.yaml + - ../bases/hcloud-kcp-ubuntu.yaml + - ../bases/hcloud-mt-control-plane-ubuntu-ipv6-only.yaml + - ../bases/hcloud-mhc-control-plane.yaml + - ../bases/hcloud-md-0-kubeadm.yaml + - ../bases/kct-md-0-ubuntu.yaml + - ../bases/hcloud-mt-md-0-ubuntu-ipv6-only.yaml + - ../bases/hcloud-mhc-md-0.yaml +patchesStrategicMerge: + - ../bases/hcloud-hetznerCluster-placementGroup_patch.yaml + - ../bases/hcloud-mt-control-plane-placementGroup_patch.yaml + - ../bases/hcloud-mt-md-0-placementGroup_patch.yaml