Skip to content

Commit e926d8d

Browse files
authored
feat: Virtual IP configuration to set different address/port (#986)
**What problem does this PR solve?**: New configuration to support setting different virutalIP address from the external controlPlaneEndpoint. This is useful for Clusters like a Nutanix Cluster in a VPC where the virtual IP needs to be some IP from same Subnet as the VMs, but the conrtol-plane endpoint is a floating IP mapped to the virtual IP accessible from outside the VPC. ``` spec: topology: variables: - name: clusterConfig value: nutanix: controlPlaneEndpoint: host: x.x.x.x port: 6443 virtualIP: configuration: address: y.y.y.y ``` The "configuration" keyword comes from an existing API here https://github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/blob/8e2b30770c64d6976a7990274988d53f9626f169/api/v1alpha1/addon_types.go#L260 **Which issue(s) this PR fixes**: Fixes # **How Has This Been Tested?**: <!-- Please describe the tests that you ran to verify your changes. Provide output from the tests and any manual steps needed to replicate the tests. --> * New unit tests * Created a Nutanix Cluster inside a VPC Assigned a floating IP `10.22.198.34` to an internal IP `172.16.0.101` <img width="1123" alt="image" src="https://github.com/user-attachments/assets/1b099de8-1b88-4820-93fc-e4c2ea3e7586"> One of the control-plane VMs got the internal VIP assigned. <img width="547" alt="image" src="https://github.com/user-attachments/assets/bcc17ce8-f2c8-45ab-9a87-a1261a2c0bf0"> And the Cluster is accessible outside of the VPC. ``` kubectl get nodes NAME STATUS ROLES AGE VERSION dkoshkin-floating-ip-k9d74-d8jxp Ready control-plane 122m v1.30.5 dkoshkin-floating-ip-k9d74-fb5pb Ready control-plane 125m v1.30.5 dkoshkin-floating-ip-k9d74-wtl5w Ready control-plane 123m v1.30.5 dkoshkin-floating-ip-md-0-qkg7q-pm5z9-6zf8f Ready <none> 124m v1.30.5 ``` **Special notes for your reviewer**: <!-- Use this to provide any additional information to the reviewers. This may include: - Best way to review the PR. - Where the author wants the most review attention on. - etc. -->
1 parent 582133f commit e926d8d

File tree

12 files changed

+337
-18
lines changed

12 files changed

+337
-18
lines changed

api/v1alpha1/common_types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ type ControlPlaneVirtualIPSpec struct {
4747
// +kubebuilder:validation:Enum=KubeVIP
4848
// +kubebuilder:default=KubeVIP
4949
Provider string `json:"provider,omitempty"`
50+
51+
// Configuration for the chosen control-plane virtual IP provider.
52+
// +kubebuilder:validation:Optional
53+
Configuration *ControlPlaneVirtualIPConfiguration `json:"configuration,omitempty"`
54+
}
55+
56+
type ControlPlaneVirtualIPConfiguration struct {
57+
// The virtual IP on which the API server is serving.
58+
// If left empty, the value from controlPlaneEndpoint.host will be used.
59+
// +kubebuilder:validation:Optional
60+
// +kubebuilder:validation:Format=ipv4
61+
Address string `json:"address,omitempty"`
62+
63+
// The port on which the API server is serving.
64+
// If left empty, the value from controlPlaneEndpoint.port will be used.
65+
// +kubebuilder:validation:Optional
66+
// +kubebuilder:validation:Minimum=1
67+
// +kubebuilder:validation:Maximum=65535
68+
Port int32 `json:"port,omitempty"`
5069
}
5170

5271
// LocalObjectReference contains enough information to let you locate the

api/v1alpha1/crds/caren.nutanix.com_nutanixclusterconfigs.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,24 @@ spec:
613613
virtualIP:
614614
description: Configuration for the virtual IP provider.
615615
properties:
616+
configuration:
617+
description: Configuration for the chosen control-plane virtual IP provider.
618+
properties:
619+
address:
620+
description: |-
621+
The virtual IP on which the API server is serving.
622+
If left empty, the value from controlPlaneEndpoint.host will be used.
623+
format: ipv4
624+
type: string
625+
port:
626+
description: |-
627+
The port on which the API server is serving.
628+
If left empty, the value from controlPlaneEndpoint.port will be used.
629+
format: int32
630+
maximum: 65535
631+
minimum: 1
632+
type: integer
633+
type: object
616634
provider:
617635
default: KubeVIP
618636
description: Virtual IP provider to deploy.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/cluster-api-runtime-extensions-nutanix/addons/ccm/nutanix/values-template.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ prismCentralInsecure: {{ .PrismCentralInsecure }}
44
{{- with .PrismCentralAdditionalTrustBundle }}
55
prismCentralAdditionalTrustBundle: "{{ . }}"
66
{{- end }}
7-
{{- with .ControlPlaneEndpointHost }}
8-
ignoredNodeIPs: [ {{ printf "%q" . }} ]
9-
{{- end }}
7+
{{- with .IPsToIgnore }}
8+
ignoredNodeIPs: [ {{ joinQuoted . }} ]
9+
{{- end }}
1010

1111
# The Secret containing the credentials will be created by the handler.
1212
createSecret: false

charts/cluster-api-runtime-extensions-nutanix/templates/virtual-ip/kube-vip/manifests/kube-vip-configmap.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ data:
2222
- name: vip_arp
2323
value: "true"
2424
- name: port
25-
value: '{{ `{{ .ControlPlaneEndpoint.Port }}` }}'
25+
value: '{{ `{{ .Port }}` }}'
2626
- name: vip_nodename
2727
valueFrom:
2828
fieldRef:
@@ -46,7 +46,7 @@ data:
4646
- name: vip_retryperiod
4747
value: "2"
4848
- name: address
49-
value: '{{ `{{ .ControlPlaneEndpoint.Host }}` }}'
49+
value: '{{ `{{ .Address }}` }}'
5050
- name: prometheus_server
5151
image: ghcr.io/kube-vip/kube-vip:v0.8.3
5252
imagePullPolicy: IfNotPresent

docs/content/customization/nutanix/control-plane-endpoint.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title = "Control Plane Endpoint"
33
+++
44

5-
Configure Control Plane Endpoint. Defines the host IP and port of the CAPX Kubernetes cluster.
5+
Configure Control Plane Endpoint. Defines the host IP and port of the Nutanix Kubernetes cluster.
66

77
## Examples
88

@@ -51,6 +51,93 @@ spec:
5151
name: kube-vip
5252
namespace: kube-system
5353
spec:
54+
containers:
55+
- name: kube-vip
56+
args:
57+
- manager
58+
env:
59+
- name: port
60+
value: '6443'
61+
- name: address
62+
value: 'x.x.x.x'
63+
...
64+
owner: root:root
65+
path: /etc/kubernetes/manifests/kube-vip.yaml
66+
permissions: "0600"
67+
postKubeadmCommands:
68+
# Only added for clusters version >=v1.29.0
69+
- |-
70+
if [ -f /run/kubeadm/kubeadm.yaml ]; then
71+
sed -i 's#path: /etc/kubernetes/super-admin.conf#path: ...
72+
fi
73+
preKubeadmCommands:
74+
# Only added for clusters version >=v1.29.0
75+
- |-
76+
if [ -f /run/kubeadm/kubeadm.yaml ]; then
77+
sed -i 's#path: /etc/kubernetes/admin.conf#path: ...
78+
fi
79+
```
80+
81+
### Set Control Plane Endpoint and a Different Virtual IP
82+
83+
It is also possible to set a separate virtual IP to be used by kube-vip from the control plane endpoint.
84+
This is useful in VPC setups or other instances
85+
when you have an external floating IP already associated with the virtual IP.
86+
87+
```yaml
88+
apiVersion: cluster.x-k8s.io/v1beta1
89+
kind: Cluster
90+
metadata:
91+
name: <NAME>
92+
spec:
93+
topology:
94+
variables:
95+
- name: clusterConfig
96+
value:
97+
nutanix:
98+
controlPlaneEndpoint:
99+
host: x.x.x.x
100+
port: 6443
101+
virtualIP:
102+
configuration:
103+
address: y.y.y.y
104+
```
105+
106+
Applying this configuration will result in the following value being set:
107+
108+
- `NutanixCluster`:
109+
110+
```yaml
111+
spec:
112+
template:
113+
spec:
114+
controlPlaneEndpoint:
115+
host: x.x.x.x
116+
port: 6443
117+
```
118+
119+
- `KubeadmControlPlaneTemplate`
120+
121+
```yaml
122+
spec:
123+
kubeadmConfigSpec:
124+
files:
125+
- content: |
126+
apiVersion: v1
127+
kind: Pod
128+
metadata:
129+
name: kube-vip
130+
namespace: kube-system
131+
spec:
132+
containers:
133+
- name: kube-vip
134+
args:
135+
- manager
136+
env:
137+
- name: port
138+
value: '6443'
139+
- name: address
140+
value: 'y.y.y.y'
54141
...
55142
owner: root:root
56143
path: /etc/kubernetes/manifests/kube-vip.yaml

hack/addons/update-kube-vip-manifests.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ docker container run --rm ghcr.io/kube-vip/kube-vip:"${KUBE_VIP_VERSION}" \
3333
gojq --yaml-input --yaml-output \
3434
'del(.metadata.creationTimestamp, .status) |
3535
.spec.containers[].imagePullPolicy |= "IfNotPresent" |
36-
(.spec.containers[0].env[] | select(.name == "port").value) |= "{{ `{{ .ControlPlaneEndpoint.Port }}` }}" |
37-
(.spec.containers[0].env[] | select(.name == "address").value) |= "{{ `{{ .ControlPlaneEndpoint.Host }}` }}"
36+
(.spec.containers[0].env[] | select(.name == "port").value) |= "{{ `{{ .Port }}` }}" |
37+
(.spec.containers[0].env[] | select(.name == "address").value) |= "{{ `{{ .Address }}` }}"
3838
' >"${ASSETS_DIR}/${FILE_NAME}"
3939

4040
kubectl create configmap "{{ .Values.hooks.virtualIP.kubeVip.defaultTemplateConfigMap.name }}" --dry-run=client --output yaml \

pkg/handlers/generic/lifecycle/ccm/nutanix/handler.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11+
"strings"
1112
"text/template"
1213

1314
"github.com/go-logr/logr"
@@ -140,7 +141,15 @@ func templateValuesFunc(
140141
nutanixConfig *v1alpha1.NutanixSpec,
141142
) func(*clusterv1.Cluster, string) (string, error) {
142143
return func(_ *clusterv1.Cluster, valuesTemplate string) (string, error) {
143-
helmValuesTemplate, err := template.New("").Parse(valuesTemplate)
144+
joinQuoted := template.FuncMap{
145+
"joinQuoted": func(items []string) string {
146+
for i, item := range items {
147+
items[i] = fmt.Sprintf("%q", item)
148+
}
149+
return strings.Join(items, ", ")
150+
},
151+
}
152+
helmValuesTemplate, err := template.New("").Funcs(joinQuoted).Parse(valuesTemplate)
144153
if err != nil {
145154
return "", fmt.Errorf("failed to parse Helm values template: %w", err)
146155
}
@@ -150,7 +159,7 @@ func templateValuesFunc(
150159
PrismCentralPort uint16
151160
PrismCentralInsecure bool
152161
PrismCentralAdditionalTrustBundle string
153-
ControlPlaneEndpointHost string
162+
IPsToIgnore []string
154163
}
155164

156165
address, port, err := nutanixConfig.PrismCentralEndpoint.ParseURL()
@@ -162,7 +171,7 @@ func templateValuesFunc(
162171
PrismCentralPort: port,
163172
PrismCentralInsecure: nutanixConfig.PrismCentralEndpoint.Insecure,
164173
PrismCentralAdditionalTrustBundle: nutanixConfig.PrismCentralEndpoint.AdditionalTrustBundle,
165-
ControlPlaneEndpointHost: nutanixConfig.ControlPlaneEndpoint.Host,
174+
IPsToIgnore: ipsToIgnore(nutanixConfig),
166175
}
167176

168177
var b bytes.Buffer
@@ -174,3 +183,17 @@ func templateValuesFunc(
174183
return b.String(), nil
175184
}
176185
}
186+
187+
func ipsToIgnore(nutanixConfig *v1alpha1.NutanixSpec) []string {
188+
toIgnore := []string{nutanixConfig.ControlPlaneEndpoint.Host}
189+
// Also ignore the virtual IP if it is set.
190+
if nutanixConfig.ControlPlaneEndpoint.VirtualIPSpec != nil &&
191+
nutanixConfig.ControlPlaneEndpoint.VirtualIPSpec.Configuration != nil &&
192+
nutanixConfig.ControlPlaneEndpoint.VirtualIPSpec.Configuration.Address != "" {
193+
toIgnore = append(
194+
toIgnore,
195+
nutanixConfig.ControlPlaneEndpoint.VirtualIPSpec.Configuration.Address,
196+
)
197+
}
198+
return toIgnore
199+
}

pkg/handlers/generic/lifecycle/ccm/nutanix/handler_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ prismCentralPort: 9440
3939
prismCentralInsecure: true
4040
ignoredNodeIPs: [ "1.2.3.4" ]
4141
42+
# The Secret containing the credentials will be created by the handler.
43+
createSecret: false
44+
secretName: nutanix-ccm-credentials
45+
`
46+
47+
expectedWithVirtualIPSet = `prismCentralEndPoint: prism-central.nutanix.com
48+
prismCentralPort: 9440
49+
prismCentralInsecure: true
50+
ignoredNodeIPs: [ "1.2.3.4", "5.6.7.8" ]
51+
4252
# The Secret containing the credentials will be created by the handler.
4353
createSecret: false
4454
secretName: nutanix-ccm-credentials
@@ -127,6 +137,41 @@ func Test_templateValues(t *testing.T) {
127137
in: valuesTemplate,
128138
expected: expectedWithoutAdditionalTrustBundle,
129139
},
140+
{
141+
name: "With VirtualIP Set",
142+
clusterConfig: &apivariables.ClusterConfigSpec{
143+
Addons: &apivariables.Addons{
144+
GenericAddons: v1alpha1.GenericAddons{
145+
CCM: &v1alpha1.CCM{
146+
Credentials: &v1alpha1.CCMCredentials{
147+
SecretRef: v1alpha1.LocalObjectReference{
148+
Name: "creds",
149+
},
150+
},
151+
},
152+
},
153+
},
154+
Nutanix: &v1alpha1.NutanixSpec{
155+
PrismCentralEndpoint: v1alpha1.NutanixPrismCentralEndpointSpec{
156+
URL: fmt.Sprintf(
157+
"https://prism-central.nutanix.com:%d",
158+
v1alpha1.DefaultPrismCentralPort,
159+
),
160+
Insecure: true,
161+
},
162+
ControlPlaneEndpoint: v1alpha1.ControlPlaneEndpointSpec{
163+
Host: "1.2.3.4",
164+
VirtualIPSpec: &v1alpha1.ControlPlaneVirtualIPSpec{
165+
Configuration: &v1alpha1.ControlPlaneVirtualIPConfiguration{
166+
Address: "5.6.7.8",
167+
},
168+
},
169+
},
170+
},
171+
},
172+
in: valuesTemplate,
173+
expected: expectedWithVirtualIPSet,
174+
},
130175
}
131176
for idx := range tests {
132177
tt := tests[idx]

pkg/handlers/generic/mutation/controlplanevirtualip/inject_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ spec:
264264
- name: vip_arp
265265
value: "true"
266266
- name: address
267-
value: "{{ .ControlPlaneEndpoint.Host }}"
267+
value: "{{ .Address }}"
268268
- name: port
269-
value: "{{ .ControlPlaneEndpoint.Port }}"
269+
value: "{{ .Port }}"
270270
`

0 commit comments

Comments
 (0)