Skip to content

Commit 012fb6a

Browse files
committed
refactor: add kube-vip static Pod to KCP in the handler
1 parent 01d3752 commit 012fb6a

File tree

8 files changed

+576
-60
lines changed

8 files changed

+576
-60
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2023 D2iQ, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package virtualip
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"io"
10+
"strings"
11+
12+
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
13+
14+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
15+
)
16+
17+
type fromReaderProvider struct {
18+
template string
19+
}
20+
21+
func NewFromReaderProvider(reader io.Reader) (*fromReaderProvider, error) {
22+
buf := new(strings.Builder)
23+
_, err := io.Copy(buf, reader)
24+
if err != nil {
25+
return nil, fmt.Errorf("failed to copy content from reader: %w", err)
26+
}
27+
28+
return &fromReaderProvider{
29+
template: buf.String(),
30+
}, nil
31+
}
32+
33+
func (p *fromReaderProvider) GetFile(
34+
_ context.Context,
35+
spec v1alpha1.ControlPlaneEndpointSpec,
36+
) (*bootstrapv1.File, error) {
37+
virtualIPStaticPod, err := templateValues(spec, p.template)
38+
if err != nil {
39+
return nil, fmt.Errorf("failed templating static Pod: %w", err)
40+
}
41+
42+
return &bootstrapv1.File{
43+
Content: virtualIPStaticPod,
44+
Owner: kubeVIPFileOwner,
45+
Path: kubeVIPFilePath,
46+
Permissions: kubeVIPFilePermissions,
47+
}, nil
48+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2023 D2iQ, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package virtualip
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/spf13/pflag"
11+
corev1 "k8s.io/api/core/v1"
12+
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
15+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
16+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/handlers/options"
17+
)
18+
19+
type KubeVIPFromConfigMapConfig struct {
20+
*options.GlobalOptions
21+
22+
defaultConfigMapName string
23+
}
24+
25+
func (c *KubeVIPFromConfigMapConfig) AddFlags(prefix string, flags *pflag.FlagSet) {
26+
flags.StringVar(
27+
&c.defaultConfigMapName,
28+
prefix+".default-kube-vip-template-configmap-name",
29+
"default-kube-vip-template",
30+
"default ConfigMap name that holds the kube-vip template used for the control-plane virtual IP",
31+
)
32+
}
33+
34+
type kubeVIPFromConfigMapProvider struct {
35+
client client.Reader
36+
37+
configMapKey client.ObjectKey
38+
}
39+
40+
func NewKubeVIPFromConfigMapProvider(
41+
cl client.Reader,
42+
config *KubeVIPFromConfigMapConfig,
43+
) *kubeVIPFromConfigMapProvider {
44+
return &kubeVIPFromConfigMapProvider{
45+
client: cl,
46+
configMapKey: client.ObjectKey{
47+
Name: config.defaultConfigMapName,
48+
Namespace: config.DefaultsNamespace(),
49+
},
50+
}
51+
}
52+
53+
// GetFile reads the kube-vip template from the ConfigMap
54+
// and returns the content a File, templating the required variables.
55+
func (p *kubeVIPFromConfigMapProvider) GetFile(
56+
ctx context.Context,
57+
spec v1alpha1.ControlPlaneEndpointSpec,
58+
) (*bootstrapv1.File, error) {
59+
data, err := getTemplateFromConfigMap(ctx, p.client, p.configMapKey)
60+
if err != nil {
61+
return nil, fmt.Errorf("failed getting template data: %w", err)
62+
}
63+
64+
kubeVIPStaticPod, err := templateValues(spec, data)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed templating static Pod: %w", err)
67+
}
68+
69+
return &bootstrapv1.File{
70+
Content: kubeVIPStaticPod,
71+
Owner: kubeVIPFileOwner,
72+
Path: kubeVIPFilePath,
73+
Permissions: kubeVIPFilePermissions,
74+
}, nil
75+
}
76+
77+
type multipleKeysError struct {
78+
configMapKey client.ObjectKey
79+
}
80+
81+
func (e multipleKeysError) Error() string {
82+
return fmt.Sprintf("found multiple keys in ConfigMap %q, when only 1 is expected", e.configMapKey)
83+
}
84+
85+
type emptyValuesError struct {
86+
configMapKey client.ObjectKey
87+
}
88+
89+
func (e emptyValuesError) Error() string {
90+
return fmt.Sprintf("could not find any keys with non-empty vaules in ConfigMap %q", e.configMapKey)
91+
}
92+
93+
func getTemplateFromConfigMap(
94+
ctx context.Context,
95+
cl client.Reader,
96+
configMapKey client.ObjectKey,
97+
) (string, error) {
98+
configMap := &corev1.ConfigMap{}
99+
err := cl.Get(ctx, configMapKey, configMap)
100+
if err != nil {
101+
return "", fmt.Errorf("failed to get template ConfigMap %q: %w", configMapKey, err)
102+
}
103+
104+
if len(configMap.Data) > 1 {
105+
return "", multipleKeysError{configMapKey: configMapKey}
106+
}
107+
108+
// at this point there should only be 1 key ConfigMap, return on the first non-empty value
109+
for _, data := range configMap.Data {
110+
if data != "" {
111+
return data, nil
112+
}
113+
}
114+
115+
return "", emptyValuesError{configMapKey: configMapKey}
116+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2023 D2iQ, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package virtualip
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
corev1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"sigs.k8s.io/controller-runtime/pkg/client"
15+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
16+
17+
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/api/v1alpha1"
18+
)
19+
20+
func Test_GetFile(t *testing.T) {
21+
t.Parallel()
22+
23+
tests := []struct {
24+
name string
25+
controlPlaneEndpointSpec v1alpha1.ControlPlaneEndpointSpec
26+
configMap *corev1.ConfigMap
27+
expectedContent string
28+
expectedErr error
29+
}{
30+
{
31+
name: "should return templated data with both host and port",
32+
controlPlaneEndpointSpec: v1alpha1.ControlPlaneEndpointSpec{
33+
Host: "10.20.100.10",
34+
Port: 6443,
35+
},
36+
configMap: &corev1.ConfigMap{
37+
ObjectMeta: metav1.ObjectMeta{
38+
Name: "default-kube-vip-template",
39+
Namespace: "default",
40+
},
41+
Data: map[string]string{
42+
"data": validKubeVIPTemplate,
43+
},
44+
},
45+
expectedContent: expectedKubeVIPPod,
46+
},
47+
}
48+
49+
for idx := range tests {
50+
tt := tests[idx] // Capture range variable
51+
t.Run(tt.name, func(t *testing.T) {
52+
t.Parallel()
53+
fakeClient := fake.NewClientBuilder().WithObjects(tt.configMap).Build()
54+
55+
provider := kubeVIPFromConfigMapProvider{
56+
client: fakeClient,
57+
configMapKey: client.ObjectKeyFromObject(tt.configMap),
58+
}
59+
60+
file, err := provider.GetFile(context.TODO(), tt.controlPlaneEndpointSpec)
61+
require.Equal(t, tt.expectedErr, err)
62+
assert.Equal(t, tt.expectedContent, file.Content)
63+
assert.NotEmpty(t, file.Path)
64+
assert.NotEmpty(t, file.Owner)
65+
assert.NotEmpty(t, file.Permissions)
66+
})
67+
}
68+
}
69+
70+
func Test_getTemplateFromConfigMap(t *testing.T) {
71+
t.Parallel()
72+
73+
tests := []struct {
74+
name string
75+
configMap *corev1.ConfigMap
76+
expectedData string
77+
expectedErr error
78+
}{
79+
{
80+
name: "should return data from the only key",
81+
configMap: &corev1.ConfigMap{
82+
ObjectMeta: metav1.ObjectMeta{
83+
Name: "default-kube-vip-template",
84+
Namespace: "default",
85+
},
86+
Data: map[string]string{
87+
"data": "kube-vip-template",
88+
},
89+
},
90+
expectedData: "kube-vip-template",
91+
},
92+
{
93+
name: "should fail with multipleKeysError",
94+
configMap: &corev1.ConfigMap{
95+
ObjectMeta: metav1.ObjectMeta{
96+
Name: "default-kube-vip-template",
97+
Namespace: "default",
98+
},
99+
Data: map[string]string{
100+
"data": "kube-vip-template",
101+
"unexpected-key": "unexpected-value",
102+
},
103+
},
104+
expectedErr: multipleKeysError{
105+
configMapKey: client.ObjectKey{
106+
Name: "default-kube-vip-template",
107+
Namespace: "default",
108+
},
109+
},
110+
},
111+
{
112+
name: "should fail with emptyValuesError",
113+
configMap: &corev1.ConfigMap{
114+
ObjectMeta: metav1.ObjectMeta{
115+
Name: "default-kube-vip-template",
116+
Namespace: "default",
117+
},
118+
Data: map[string]string{
119+
"data": "",
120+
},
121+
},
122+
expectedErr: emptyValuesError{
123+
configMapKey: client.ObjectKey{
124+
Name: "default-kube-vip-template",
125+
Namespace: "default",
126+
},
127+
},
128+
},
129+
}
130+
131+
for idx := range tests {
132+
tt := tests[idx] // Capture range variable
133+
t.Run(tt.name, func(t *testing.T) {
134+
t.Parallel()
135+
fakeClient := fake.NewClientBuilder().WithObjects(tt.configMap).Build()
136+
137+
data, err := getTemplateFromConfigMap(
138+
context.TODO(),
139+
fakeClient,
140+
client.ObjectKeyFromObject(tt.configMap),
141+
)
142+
require.Equal(t, tt.expectedErr, err)
143+
assert.Equal(t, tt.expectedData, data)
144+
})
145+
}
146+
}
147+
148+
var (
149+
validKubeVIPTemplate = `
150+
apiVersion: v1
151+
kind: Pod
152+
metadata:
153+
name: kube-vip
154+
namespace: kube-system
155+
spec:
156+
containers:
157+
- name: kube-vip
158+
image: ghcr.io/kube-vip/kube-vip:v1.1.1
159+
imagePullPolicy: IfNotPresent
160+
args:
161+
- manager
162+
env:
163+
- name: vip_arp
164+
value: "true"
165+
- name: address
166+
value: "{{ .ControlPlaneEndpoint.Host }}"
167+
- name: port
168+
value: "{{ .ControlPlaneEndpoint.Port }}"
169+
`
170+
171+
expectedKubeVIPPod = `
172+
apiVersion: v1
173+
kind: Pod
174+
metadata:
175+
name: kube-vip
176+
namespace: kube-system
177+
spec:
178+
containers:
179+
- name: kube-vip
180+
image: ghcr.io/kube-vip/kube-vip:v1.1.1
181+
imagePullPolicy: IfNotPresent
182+
args:
183+
- manager
184+
env:
185+
- name: vip_arp
186+
value: "true"
187+
- name: address
188+
value: "10.20.100.10"
189+
- name: port
190+
value: "6443"
191+
`
192+
)

0 commit comments

Comments
 (0)