Skip to content

Commit c8a08df

Browse files
committed
fix: kube-vip admin.conf pre/postKubeadmCommands
1 parent 5e640ad commit c8a08df

File tree

7 files changed

+229
-22
lines changed

7 files changed

+229
-22
lines changed

charts/cluster-api-runtime-extensions-nutanix/defaultclusterclasses/nutanix-cluster-class.yaml

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,6 @@ spec:
147147
tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
148148
postKubeadmCommands:
149149
- echo export KUBECONFIG=/etc/kubernetes/admin.conf >> /root/.bashrc
150-
- |
151-
KUBERNETES_VERSION_NO_V=${KUBERNETES_VERSION#v}
152-
VERSION_TO_COMPARE=1.29.0
153-
if [ "$(printf '%s\n' "$KUBERNETES_VERSION_NO_V" "$VERSION_TO_COMPARE" | sort -V | head -n1)" != "$KUBERNETES_VERSION_NO_V" ]; then
154-
if [ -f /run/kubeadm/kubeadm.yaml ]; then
155-
sed -i 's#path: /etc/kubernetes/super-admin.conf#path: /etc/kubernetes/admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml;
156-
fi
157-
fi
158150
- echo "after kubeadm call" > /var/log/postkubeadm.log
159151
preKubeadmCommands:
160152
- echo "before kubeadm call" > /var/log/prekubeadm.log
@@ -163,14 +155,6 @@ spec:
163155
- echo "127.0.0.1 localhost" >>/etc/hosts
164156
- echo "127.0.0.1 kubernetes" >>/etc/hosts
165157
- echo "127.0.0.1 {{ ds.meta_data.hostname }}" >> /etc/hosts
166-
- |
167-
KUBERNETES_VERSION_NO_V=${KUBERNETES_VERSION#v}
168-
VERSION_TO_COMPARE=1.29.0
169-
if [ "$(printf '%s\n' "$KUBERNETES_VERSION_NO_V" "$VERSION_TO_COMPARE" | sort -V | head -n1)" != "$KUBERNETES_VERSION_NO_V" ]; then
170-
if [ -f /run/kubeadm/kubeadm.yaml ]; then
171-
sed -i 's#path: /etc/kubernetes/admin.conf#path: /etc/kubernetes/super-admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml;
172-
fi
173-
fi
174158
useExperimentalRetryJoin: true
175159
verbosity: 10
176160
---

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,16 @@ spec:
5555
owner: root:root
5656
path: /etc/kubernetes/manifests/kube-vip.yaml
5757
permissions: "0600"
58+
postKubeadmCommands:
59+
# Only added for clusters version >=v1.29.0
60+
- |-
61+
if [ -f /run/kubeadm/kubeadm.yaml ]; then
62+
sed -i 's#path: /etc/kubernetes/super-admin.conf#path: /etc/kubernetes/admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml;
63+
fi
64+
preKubeadmCommands:
65+
# Only added for clusters version >=v1.29.0
66+
- |-
67+
if [ -f /run/kubeadm/kubeadm.yaml ]; then
68+
sed -i 's#path: /etc/kubernetes/admin.conf#path: /etc/kubernetes/super-admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml;
69+
fi
5870
```

hack/examples/bases/nutanix/clusterclass/kustomization.yaml.tmpl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ patches:
4949
- op: "remove"
5050
path: "/spec/template/spec/kubeadmConfigSpec/files/0"
5151

52+
# Delete the kube-vip related pre and postKubeadmCommands.
53+
# Will be added back in the handler if enabled.
54+
# If the index of these changes upstream this will need to change, but will show up as a git diff.
55+
- target:
56+
kind: KubeadmControlPlaneTemplate
57+
patch: |-
58+
- op: "remove"
59+
path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands/6"
60+
- op: "remove"
61+
path: "/spec/template/spec/kubeadmConfigSpec/postKubeadmCommands/1"
62+
5263
# FIXME: Debug why some of the patches are needed.
5364
# When the handler runs, it sends back multiple patches for individual fields.
5465
# But CAPI fails applying them because of missing value.

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (h *ControlPlaneVirtualIP) Mutate(
7575
vars map[string]apiextensionsv1.JSON,
7676
holderRef runtimehooksv1.HolderReference,
7777
_ client.ObjectKey,
78-
_ mutation.ClusterGetter,
78+
clusterGetter mutation.ClusterGetter,
7979
) error {
8080
log := ctrl.LoggerFrom(ctx).WithValues(
8181
"holderRef", holderRef,
@@ -108,6 +108,15 @@ func (h *ControlPlaneVirtualIP) Mutate(
108108
return nil
109109
}
110110

111+
cluster, err := clusterGetter(ctx)
112+
if err != nil {
113+
log.Error(
114+
err,
115+
"failed to get cluster from extraAPIServerCertSANs mutation handler",
116+
)
117+
return err
118+
}
119+
111120
// only kube-vip is supported, but more providers can be added in the future
112121
virtualIPProvider := providers.NewKubeVIPFromConfigMapProvider(
113122
h.client,
@@ -138,6 +147,40 @@ func (h *ControlPlaneVirtualIP) Mutate(
138147
obj.Spec.Template.Spec.KubeadmConfigSpec.Files,
139148
*virtualIPProviderFile,
140149
)
150+
151+
preKubeadmCommands, postKubeadmCommands, getCommandsErr := virtualIPProvider.GetCommands(cluster)
152+
if getCommandsErr != nil {
153+
return getCommandsErr
154+
}
155+
156+
if len(preKubeadmCommands) > 0 {
157+
log.WithValues(
158+
"patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(),
159+
"patchedObjectName", client.ObjectKeyFromObject(obj),
160+
).Info(fmt.Sprintf(
161+
"adding %s preKubeadmCommands to control plane kubeadm config spec",
162+
virtualIPProvider.Name(),
163+
))
164+
obj.Spec.Template.Spec.KubeadmConfigSpec.PreKubeadmCommands = append(
165+
obj.Spec.Template.Spec.KubeadmConfigSpec.PreKubeadmCommands,
166+
preKubeadmCommands...,
167+
)
168+
}
169+
170+
if len(postKubeadmCommands) > 0 {
171+
log.WithValues(
172+
"patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(),
173+
"patchedObjectName", client.ObjectKeyFromObject(obj),
174+
).Info(fmt.Sprintf(
175+
"adding %s postKubeadmCommands to control plane kubeadm config spec",
176+
virtualIPProvider.Name(),
177+
))
178+
obj.Spec.Template.Spec.KubeadmConfigSpec.PostKubeadmCommands = append(
179+
obj.Spec.Template.Spec.KubeadmConfigSpec.PostKubeadmCommands,
180+
postKubeadmCommands...,
181+
)
182+
}
183+
141184
return nil
142185
},
143186
)

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

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@ import (
1111
"github.com/onsi/gomega"
1212
corev1 "k8s.io/api/core/v1"
1313
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
16+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
17+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1418
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
1519

1620
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
1721
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation"
1822
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest"
1923
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest/request"
2024
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/clusterconfig"
21-
nutanixclusterconfig "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/nutanix/clusterconfig"
25+
virtuialipproviders "github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/controlplanevirtualip/providers"
2226
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
2327
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/test/helpers"
2428
)
@@ -32,10 +36,11 @@ var _ = Describe("Generate ControlPlane virtual IP patches", func() {
3236
testDefs := []struct {
3337
capitest.PatchTestDef
3438
virtualIPTemplate string
39+
cluster *clusterv1.Cluster
3540
}{
3641
{
3742
PatchTestDef: capitest.PatchTestDef{
38-
Name: "host and port should be templated in a new file",
43+
Name: "host and port should be templated in a new file and no pre/post commands",
3944
Vars: []runtimehooksv1.Variable{
4045
capitest.VariableWithValue(
4146
clusterconfig.MetaVariableName,
@@ -69,24 +74,120 @@ var _ = Describe("Generate ControlPlane virtual IP patches", func() {
6974
),
7075
},
7176
},
77+
UnexpectedPatchMatchers: []capitest.JSONPatchMatcher{
78+
{
79+
Operation: "add",
80+
Path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands",
81+
ValueMatcher: gomega.ContainElements(
82+
virtuialipproviders.KubeVipPreKubeadmCommands,
83+
),
84+
},
85+
{
86+
Operation: "add",
87+
Path: "/spec/template/spec/kubeadmConfigSpec/postKubeadmCommands",
88+
ValueMatcher: gomega.ContainElements(
89+
virtuialipproviders.KubeVipPostKubeadmCommands,
90+
),
91+
},
92+
},
93+
},
94+
virtualIPTemplate: validKubeVIPTemplate,
95+
cluster: &clusterv1.Cluster{
96+
ObjectMeta: metav1.ObjectMeta{
97+
Name: request.ClusterName,
98+
Namespace: metav1.NamespaceDefault,
99+
},
100+
Spec: clusterv1.ClusterSpec{
101+
Topology: &clusterv1.Topology{
102+
Version: "v1.28.100",
103+
},
104+
},
105+
},
106+
},
107+
{
108+
PatchTestDef: capitest.PatchTestDef{
109+
Name: "host and port should be templated in a new file with pre/post commands",
110+
Vars: []runtimehooksv1.Variable{
111+
capitest.VariableWithValue(
112+
clusterconfig.MetaVariableName,
113+
v1alpha1.ControlPlaneEndpointSpec{
114+
Host: "10.20.100.10",
115+
Port: 6443,
116+
VirtualIPSpec: &v1alpha1.ControlPlaneVirtualIPSpec{},
117+
},
118+
VariableName,
119+
),
120+
},
121+
RequestItem: request.NewKubeadmControlPlaneTemplateRequestItem(
122+
"",
123+
),
124+
ExpectedPatchMatchers: []capitest.JSONPatchMatcher{
125+
{
126+
Operation: "add",
127+
Path: "/spec/template/spec/kubeadmConfigSpec/files",
128+
ValueMatcher: gomega.ContainElements(
129+
gomega.SatisfyAll(
130+
gomega.HaveKeyWithValue(
131+
"content",
132+
gomega.ContainSubstring("value: \"10.20.100.10\""),
133+
),
134+
gomega.HaveKeyWithValue(
135+
"content",
136+
gomega.ContainSubstring("value: \"6443\""),
137+
),
138+
gomega.HaveKey("owner"),
139+
gomega.HaveKeyWithValue("path", gomega.ContainSubstring("kube-vip")),
140+
gomega.HaveKey("permissions"),
141+
),
142+
),
143+
},
144+
{
145+
Operation: "add",
146+
Path: "/spec/template/spec/kubeadmConfigSpec/preKubeadmCommands",
147+
ValueMatcher: gomega.ContainElements(
148+
virtuialipproviders.KubeVipPreKubeadmCommands,
149+
),
150+
},
151+
{
152+
Operation: "add",
153+
Path: "/spec/template/spec/kubeadmConfigSpec/postKubeadmCommands",
154+
ValueMatcher: gomega.ContainElements(
155+
virtuialipproviders.KubeVipPostKubeadmCommands,
156+
),
157+
},
158+
},
72159
},
73160
virtualIPTemplate: validKubeVIPTemplate,
161+
cluster: &clusterv1.Cluster{
162+
ObjectMeta: metav1.ObjectMeta{
163+
Name: request.ClusterName,
164+
Namespace: metav1.NamespaceDefault,
165+
},
166+
Spec: clusterv1.ClusterSpec{
167+
Topology: &clusterv1.Topology{
168+
Version: "v1.29.0",
169+
},
170+
},
171+
},
74172
},
75173
}
76174

77175
// create test node for each case
78-
for _, tt := range testDefs {
176+
for idx := range testDefs {
177+
tt := testDefs[idx]
79178
It(tt.Name, func() {
179+
clientScheme := runtime.NewScheme()
180+
utilruntime.Must(clientgoscheme.AddToScheme(clientScheme))
181+
utilruntime.Must(clusterv1.AddToScheme(clientScheme))
80182
// Always initialize the testEnv variable in the closure.
81183
// This will allow ginkgo to initialize testEnv variable during test execution time.
82184
testEnv := helpers.TestEnv
83185
// use direct client instead of controller client. This will allow the patch handler to read k8s object
84186
// that are written by the tests.
85187
// Test cases writes credentials secret that the mutator handler reads.
86188
// Using direct client will enable reading it immediately.
87-
client, err := testEnv.GetK8sClient()
189+
client, err := testEnv.GetK8sClientWithScheme(clientScheme)
88190
gomega.Expect(err).To(gomega.BeNil())
89-
90191
// setup a test ConfigMap to be used by the handler
91192
cm := &corev1.ConfigMap{
92193
TypeMeta: metav1.TypeMeta{
@@ -104,6 +205,15 @@ var _ = Describe("Generate ControlPlane virtual IP patches", func() {
104205
err = client.Create(context.Background(), cm)
105206
gomega.Expect(err).ToNot(gomega.HaveOccurred())
106207

208+
if tt.cluster != nil {
209+
err = client.Create(context.Background(), tt.cluster)
210+
gomega.Expect(err).To(gomega.BeNil())
211+
defer func() {
212+
err = client.Delete(context.Background(), tt.cluster)
213+
gomega.Expect(err).To(gomega.BeNil())
214+
}()
215+
}
216+
107217
cfg := &Config{
108218
GlobalOptions: options.NewGlobalOptions(),
109219
defaultKubeVipConfigMapName: cm.Name,

pkg/handlers/generic/mutation/controlplanevirtualip/providers/kubevip.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,26 @@ import (
77
"context"
88
"fmt"
99

10+
"github.com/blang/semver/v4"
1011
corev1 "k8s.io/api/core/v1"
12+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1113
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
1214
"sigs.k8s.io/controller-runtime/pkg/client"
1315

1416
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
1517
)
1618

19+
var (
20+
//nolint:lll // for readability prefer to keep the long line
21+
KubeVipPreKubeadmCommands = []string{`if [ -f /run/kubeadm/kubeadm.yaml ]; then
22+
sed -i 's#path: /etc/kubernetes/admin.conf#path: /etc/kubernetes/super-admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml;
23+
fi`}
24+
//nolint:lll // for readability prefer to keep the long line
25+
KubeVipPostKubeadmCommands = []string{`if [ -f /run/kubeadm/kubeadm.yaml ]; then
26+
sed -i 's#path: /etc/kubernetes/super-admin.conf#path: /etc/kubernetes/admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml;
27+
fi`}
28+
)
29+
1730
type kubeVIPFromConfigMapProvider struct {
1831
client client.Reader
1932

@@ -61,6 +74,29 @@ func (p *kubeVIPFromConfigMapProvider) GetFile(
6174
}, nil
6275
}
6376

77+
//nolint:gocritic // No need for named return values
78+
func (p *kubeVIPFromConfigMapProvider) GetCommands(cluster *clusterv1.Cluster) ([]string, []string, error) {
79+
// The kube-vip static Pod uses admin.conf on the host to connect to the API server.
80+
// But, starting with Kubernetes 1.29, admin.conf first gets created with no RBAC permissions.
81+
// At the same time, 'kubeadm init' command waits for the API server to be reachable on the kube-vip IP.
82+
// And since the kube-vip Pod is crashlooping with a permissions error, 'kubeadm init' fails.
83+
// To work around this:
84+
// 1. return a preKubeadmCommand to change the kube-vip Pod to use the new super-admin.conf file.
85+
// 2. return a postKubeadmCommand to change the kube-vip Pod back to use admin.conf,
86+
// after kubeadm has assigned it the necessary RBAC permissions.
87+
//
88+
// See https://github.com/kube-vip/kube-vip/issues/684
89+
needCommands, err := needHackCommands(cluster)
90+
if err != nil {
91+
return nil, nil, fmt.Errorf("failed to determine if kube-vip commands are needed: %w", err)
92+
}
93+
if !needCommands {
94+
return nil, nil, nil
95+
}
96+
97+
return KubeVipPreKubeadmCommands, KubeVipPostKubeadmCommands, nil
98+
}
99+
64100
type multipleKeysError struct {
65101
configMapKey client.ObjectKey
66102
}
@@ -101,3 +137,12 @@ func getTemplateFromConfigMap(
101137

102138
return "", emptyValuesError{configMapKey: configMapKey}
103139
}
140+
141+
func needHackCommands(cluster *clusterv1.Cluster) (bool, error) {
142+
version, err := semver.ParseTolerant(cluster.Spec.Topology.Version)
143+
if err != nil {
144+
return false, fmt.Errorf("failed to parse version from cluster %w", err)
145+
}
146+
147+
return version.Minor >= 29, nil
148+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"text/template"
1111

12+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1213
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
1314

1415
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
@@ -24,6 +25,7 @@ const (
2425
type Provider interface {
2526
Name() string
2627
GetFile(ctx context.Context, spec v1alpha1.ControlPlaneEndpointSpec) (*bootstrapv1.File, error)
28+
GetCommands(cluster *clusterv1.Cluster) ([]string, []string, error)
2729
}
2830

2931
func templateValues(controlPlaneEndpoint v1alpha1.ControlPlaneEndpointSpec, text string) (string, error) {

0 commit comments

Comments
 (0)