Skip to content

Commit 2a7541c

Browse files
authored
Fix missing updates of server certificates (#219)
* merge * wip test * added test for restart * tests reorg * simplified tests
1 parent 997216f commit 2a7541c

File tree

5 files changed

+239
-149
lines changed

5 files changed

+239
-149
lines changed

pkg/controller/cluster/cluster.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/rancher/k3k/pkg/controller/cluster/agent"
1414
"github.com/rancher/k3k/pkg/controller/cluster/server"
1515
"github.com/rancher/k3k/pkg/controller/cluster/server/bootstrap"
16+
apps "k8s.io/api/apps/v1"
1617
v1 "k8s.io/api/core/v1"
1718
rbacv1 "k8s.io/api/rbac/v1"
1819
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -76,6 +77,7 @@ func Add(ctx context.Context, mgr manager.Manager, sharedAgentImage, sharedAgent
7677
WithOptions(ctrlruntimecontroller.Options{
7778
MaxConcurrentReconciles: maxConcurrentReconciles,
7879
}).
80+
Owns(&apps.StatefulSet{}).
7981
Complete(&reconciler)
8082
}
8183

@@ -212,24 +214,47 @@ func (c *ClusterReconciler) reconcileCluster(ctx context.Context, cluster *v1alp
212214
}
213215
}
214216

215-
bootstrapSecret, err := bootstrap.Generate(ctx, cluster, serviceIP, token)
216-
if err != nil {
217+
if err := c.ensureBootstrapSecret(ctx, cluster, serviceIP, token); err != nil {
217218
return err
218219
}
219220

220-
if err := c.Client.Create(ctx, bootstrapSecret); err != nil {
221-
if !apierrors.IsAlreadyExists(err) {
222-
return err
223-
}
224-
}
225-
226221
if err := c.bindNodeProxyClusterRole(ctx, cluster); err != nil {
227222
return err
228223
}
229224

230225
return nil
231226
}
232227

228+
// ensureBootstrapSecret will create or update the Secret containing the bootstrap data from the k3s server
229+
func (c *ClusterReconciler) ensureBootstrapSecret(ctx context.Context, cluster *v1alpha1.Cluster, serviceIP, token string) error {
230+
log := ctrl.LoggerFrom(ctx)
231+
log.Info("ensuring bootstrap secret")
232+
233+
bootstrapData, err := bootstrap.GenerateBootstrapData(ctx, cluster, serviceIP, token)
234+
if err != nil {
235+
return err
236+
}
237+
238+
bootstrapSecret := &v1.Secret{
239+
ObjectMeta: metav1.ObjectMeta{
240+
Name: controller.SafeConcatNameWithPrefix(cluster.Name, "bootstrap"),
241+
Namespace: cluster.Namespace,
242+
},
243+
}
244+
245+
_, err = controllerutil.CreateOrUpdate(ctx, c.Client, bootstrapSecret, func() error {
246+
if err := controllerutil.SetControllerReference(cluster, bootstrapSecret, c.Scheme); err != nil {
247+
return err
248+
}
249+
250+
bootstrapSecret.Data = map[string][]byte{
251+
"bootstrap": bootstrapData,
252+
}
253+
return nil
254+
})
255+
return err
256+
}
257+
233258
func (c *ClusterReconciler) createClusterConfigs(ctx context.Context, cluster *v1alpha1.Cluster, server *server.Server, serviceIP string) error {
234259
// create init node config
235260
initServerConfig, err := server.Config(true, serviceIP)

pkg/controller/cluster/server/bootstrap/bootstrap.go

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
1414
"github.com/rancher/k3k/pkg/controller"
1515
v1 "k8s.io/api/core/v1"
16-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1716
"k8s.io/apimachinery/pkg/types"
1817
"sigs.k8s.io/controller-runtime/pkg/client"
1918
)
@@ -35,7 +34,7 @@ type content struct {
3534
// Generate generates the bootstrap for the cluster:
3635
// 1- use the server token to get the bootstrap data from k3s
3736
// 2- save the bootstrap data as a secret
38-
func Generate(ctx context.Context, cluster *v1alpha1.Cluster, ip, token string) (*v1.Secret, error) {
37+
func GenerateBootstrapData(ctx context.Context, cluster *v1alpha1.Cluster, ip, token string) ([]byte, error) {
3938
bootstrap, err := requestBootstrap(token, ip)
4039
if err != nil {
4140
return nil, fmt.Errorf("failed to request bootstrap secret: %w", err)
@@ -45,31 +44,7 @@ func Generate(ctx context.Context, cluster *v1alpha1.Cluster, ip, token string)
4544
return nil, fmt.Errorf("failed to decode bootstrap secret: %w", err)
4645
}
4746

48-
bootstrapData, err := json.Marshal(bootstrap)
49-
if err != nil {
50-
return nil, err
51-
}
52-
return &v1.Secret{
53-
TypeMeta: metav1.TypeMeta{
54-
Kind: "Secret",
55-
APIVersion: "v1",
56-
},
57-
ObjectMeta: metav1.ObjectMeta{
58-
Name: controller.SafeConcatNameWithPrefix(cluster.Name, "bootstrap"),
59-
Namespace: cluster.Namespace,
60-
OwnerReferences: []metav1.OwnerReference{
61-
{
62-
APIVersion: cluster.APIVersion,
63-
Kind: cluster.Kind,
64-
Name: cluster.Name,
65-
UID: cluster.UID,
66-
},
67-
},
68-
},
69-
Data: map[string][]byte{
70-
"bootstrap": bootstrapData,
71-
},
72-
}, nil
47+
return json.Marshal(bootstrap)
7348

7449
}
7550

tests/cluster_test.go

Lines changed: 110 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,46 @@ package k3k_test
22

33
import (
44
"context"
5+
"crypto/x509"
6+
"errors"
57
"fmt"
6-
"strings"
78
"time"
89

910
. "github.com/onsi/ginkgo/v2"
1011
. "github.com/onsi/gomega"
1112
"github.com/rancher/k3k/k3k-kubelet/translate"
1213
"github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
13-
"github.com/rancher/k3k/pkg/controller/certs"
14-
"github.com/rancher/k3k/pkg/controller/kubeconfig"
1514
corev1 "k8s.io/api/core/v1"
1615
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17-
"k8s.io/client-go/kubernetes"
18-
"k8s.io/client-go/tools/clientcmd"
19-
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
2016
)
2117

18+
var _ = When("k3k is installed", func() {
19+
It("is in Running status", func() {
20+
21+
// check that the controller is running
22+
Eventually(func() bool {
23+
opts := v1.ListOptions{LabelSelector: "app.kubernetes.io/name=k3k"}
24+
podList, err := k8s.CoreV1().Pods("k3k-system").List(context.Background(), opts)
25+
26+
Expect(err).To(Not(HaveOccurred()))
27+
Expect(podList.Items).To(Not(BeEmpty()))
28+
29+
var isRunning bool
30+
for _, pod := range podList.Items {
31+
if pod.Status.Phase == corev1.PodRunning {
32+
isRunning = true
33+
break
34+
}
35+
}
36+
37+
return isRunning
38+
}).
39+
WithTimeout(time.Second * 10).
40+
WithPolling(time.Second).
41+
Should(BeTrue())
42+
})
43+
})
44+
2245
var _ = When("a cluster is installed", func() {
2346

2447
var namespace string
@@ -30,28 +53,29 @@ var _ = When("a cluster is installed", func() {
3053
namespace = createdNS.Name
3154
})
3255

33-
It("will be created in shared mode", func() {
56+
It("can create a nginx pod", func() {
3457
ctx := context.Background()
35-
containerIP, err := k3sContainer.ContainerIP(ctx)
36-
Expect(err).To(Not(HaveOccurred()))
37-
38-
fmt.Fprintln(GinkgoWriter, "K3s containerIP: "+containerIP)
3958

4059
cluster := v1alpha1.Cluster{
4160
ObjectMeta: v1.ObjectMeta{
4261
Name: "mycluster",
4362
Namespace: namespace,
4463
},
4564
Spec: v1alpha1.ClusterSpec{
46-
TLSSANs: []string{containerIP},
65+
TLSSANs: []string{hostIP},
4766
Expose: &v1alpha1.ExposeConfig{
4867
NodePort: &v1alpha1.NodePortConfig{
4968
Enabled: true,
5069
},
5170
},
5271
},
5372
}
54-
virtualK8sClient := CreateCluster(containerIP, cluster)
73+
74+
By(fmt.Sprintf("Creating virtual cluster %s/%s", cluster.Namespace, cluster.Name))
75+
NewVirtualCluster(cluster)
76+
77+
By("Waiting to get a kubernetes client for the virtual cluster")
78+
virtualK8sClient := NewVirtualK8sClient(cluster)
5579

5680
nginxPod := &corev1.Pod{
5781
ObjectMeta: v1.ObjectMeta{
@@ -65,7 +89,7 @@ var _ = When("a cluster is installed", func() {
6589
}},
6690
},
6791
}
68-
nginxPod, err = virtualK8sClient.CoreV1().Pods(nginxPod.Namespace).Create(ctx, nginxPod, v1.CreateOptions{})
92+
nginxPod, err := virtualK8sClient.CoreV1().Pods(nginxPod.Namespace).Create(ctx, nginxPod, v1.CreateOptions{})
6993
Expect(err).To(Not(HaveOccurred()))
7094

7195
// check that the nginx Pod is up and running in the host cluster
@@ -78,12 +102,12 @@ var _ = When("a cluster is installed", func() {
78102
resourceName := pod.Annotations[translate.ResourceNameAnnotation]
79103
resourceNamespace := pod.Annotations[translate.ResourceNamespaceAnnotation]
80104

81-
fmt.Fprintf(GinkgoWriter,
82-
"pod=%s resource=%s/%s status=%s\n",
83-
pod.Name, resourceNamespace, resourceName, pod.Status.Phase,
84-
)
85-
86105
if resourceName == nginxPod.Name && resourceNamespace == nginxPod.Namespace {
106+
fmt.Fprintf(GinkgoWriter,
107+
"pod=%s resource=%s/%s status=%s\n",
108+
pod.Name, resourceNamespace, resourceName, pod.Status.Phase,
109+
)
110+
87111
return pod.Status.Phase == corev1.PodRunning
88112
}
89113
}
@@ -94,73 +118,81 @@ var _ = When("a cluster is installed", func() {
94118
WithPolling(time.Second * 5).
95119
Should(BeTrue())
96120
})
97-
})
98121

99-
func CreateCluster(hostIP string, cluster v1alpha1.Cluster) *kubernetes.Clientset {
100-
GinkgoHelper()
122+
It("regenerates the bootstrap secret after a restart", func() {
123+
ctx := context.Background()
101124

102-
By(fmt.Sprintf("Creating virtual cluster %s/%s", cluster.Namespace, cluster.Name))
125+
cluster := v1alpha1.Cluster{
126+
ObjectMeta: v1.ObjectMeta{
127+
Name: "mycluster",
128+
Namespace: namespace,
129+
},
130+
Spec: v1alpha1.ClusterSpec{
131+
TLSSANs: []string{hostIP},
132+
Expose: &v1alpha1.ExposeConfig{
133+
NodePort: &v1alpha1.NodePortConfig{
134+
Enabled: true,
135+
},
136+
},
137+
},
138+
}
139+
140+
By(fmt.Sprintf("Creating virtual cluster %s/%s", cluster.Namespace, cluster.Name))
141+
NewVirtualCluster(cluster)
103142

104-
ctx := context.Background()
105-
err := k8sClient.Create(ctx, &cluster)
106-
Expect(err).To(Not(HaveOccurred()))
143+
By("Waiting to get a kubernetes client for the virtual cluster")
144+
virtualK8sClient := NewVirtualK8sClient(cluster)
107145

108-
By("Waiting for server and kubelet to be ready")
146+
_, err := virtualK8sClient.DiscoveryClient.ServerVersion()
147+
Expect(err).To(Not(HaveOccurred()))
109148

110-
// check that the server Pod and the Kubelet are in Ready state
111-
Eventually(func() bool {
112-
podList, err := k8s.CoreV1().Pods(cluster.Namespace).List(ctx, v1.ListOptions{})
149+
labelSelector := "cluster=" + cluster.Name + ",role=server"
150+
serverPods, err := k8s.CoreV1().Pods(namespace).List(ctx, v1.ListOptions{LabelSelector: labelSelector})
113151
Expect(err).To(Not(HaveOccurred()))
114152

115-
serverRunning := false
116-
kubeletRunning := false
153+
Expect(len(serverPods.Items)).To(Equal(1))
154+
serverPod := serverPods.Items[0]
117155

118-
for _, pod := range podList.Items {
119-
imageName := pod.Spec.Containers[0].Image
120-
imageName = strings.Split(imageName, ":")[0] // remove tag
156+
fmt.Fprintf(GinkgoWriter, "deleting pod %s/%s\n", serverPod.Namespace, serverPod.Name)
157+
// GracePeriodSeconds: ptr.To[int64](0)
158+
err = k8s.CoreV1().Pods(namespace).Delete(ctx, serverPod.Name, v1.DeleteOptions{})
159+
Expect(err).To(Not(HaveOccurred()))
121160

122-
switch imageName {
123-
case "rancher/k3s":
124-
serverRunning = pod.Status.Phase == corev1.PodRunning
125-
case "rancher/k3k-kubelet":
126-
kubeletRunning = pod.Status.Phase == corev1.PodRunning
127-
}
161+
By("Deleting server pod")
128162

129-
if serverRunning && kubeletRunning {
130-
return true
131-
}
132-
}
163+
// check that the server pods restarted
164+
Eventually(func() any {
165+
serverPods, err = k8s.CoreV1().Pods(namespace).List(ctx, v1.ListOptions{LabelSelector: labelSelector})
166+
Expect(err).To(Not(HaveOccurred()))
167+
Expect(len(serverPods.Items)).To(Equal(1))
168+
return serverPods.Items[0].DeletionTimestamp
169+
}).
170+
WithTimeout(time.Minute).
171+
WithPolling(time.Second * 5).
172+
Should(BeNil())
173+
174+
By("Server pod up and running again")
175+
176+
By("Using old k8s client configuration should fail")
177+
178+
Eventually(func() bool {
179+
_, err = virtualK8sClient.DiscoveryClient.ServerVersion()
180+
var unknownAuthorityErr x509.UnknownAuthorityError
181+
return errors.As(err, &unknownAuthorityErr)
182+
}).
183+
WithTimeout(time.Minute * 2).
184+
WithPolling(time.Second * 5).
185+
Should(BeTrue())
133186

134-
return false
135-
}).
136-
WithTimeout(time.Minute).
137-
WithPolling(time.Second * 5).
138-
Should(BeTrue())
139-
140-
By("Waiting for server to be up and running")
141-
142-
var config *clientcmdapi.Config
143-
Eventually(func() error {
144-
vKubeconfig := kubeconfig.New()
145-
vKubeconfig.AltNames = certs.AddSANs([]string{hostIP, "k3k-mycluster-kubelet"})
146-
config, err = vKubeconfig.Extract(ctx, k8sClient, &cluster, hostIP)
147-
return err
148-
}).
149-
WithTimeout(time.Minute * 2).
150-
WithPolling(time.Second * 5).
151-
Should(BeNil())
152-
153-
configData, err := clientcmd.Write(*config)
154-
Expect(err).To(Not(HaveOccurred()))
155-
156-
restcfg, err := clientcmd.RESTConfigFromKubeConfig(configData)
157-
Expect(err).To(Not(HaveOccurred()))
158-
virtualK8sClient, err := kubernetes.NewForConfig(restcfg)
159-
Expect(err).To(Not(HaveOccurred()))
160-
161-
serverVersion, err := virtualK8sClient.DiscoveryClient.ServerVersion()
162-
Expect(err).To(Not(HaveOccurred()))
163-
fmt.Fprintf(GinkgoWriter, "serverVersion: %+v\n", serverVersion)
164-
165-
return virtualK8sClient
166-
}
187+
By("Recover new config should succeed")
188+
189+
Eventually(func() error {
190+
virtualK8sClient = NewVirtualK8sClient(cluster)
191+
_, err = virtualK8sClient.DiscoveryClient.ServerVersion()
192+
return err
193+
}).
194+
WithTimeout(time.Minute * 2).
195+
WithPolling(time.Second * 5).
196+
Should(BeNil())
197+
})
198+
})

0 commit comments

Comments
 (0)