Skip to content

Commit 834b16d

Browse files
committed
Wait for the control-plane to be ready
This changes the way that jobs are created. If spec.requireClusterReady is true then the jobs will not be created until the remote cluster's control-plane is ready.
1 parent b6c379d commit 834b16d

12 files changed

+255
-58
lines changed

api/v1alpha1/clusterbootstrapconfig_types.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ type JobTemplate struct {
4343

4444
// ClusterBootstrapConfigSpec defines the desired state of ClusterBootstrapConfig
4545
type ClusterBootstrapConfigSpec struct {
46-
ClusterSelector metav1.LabelSelector `json:"clusterSelector"`
47-
Template JobTemplate `json:"jobTemplate"`
48-
ControlPlaneWaitDuration *metav1.Duration `json:"waitForControlPlane,omitempty"`
46+
ClusterSelector metav1.LabelSelector `json:"clusterSelector"`
47+
Template JobTemplate `json:"jobTemplate"`
48+
RequireClusterReady bool `json:"requireClusterReady"`
4949
}
5050

5151
// ClusterBootstrapConfigStatus defines the observed state of ClusterBootstrapConfig
@@ -64,15 +64,6 @@ type ClusterBootstrapConfig struct {
6464
Status ClusterBootstrapConfigStatus `json:"status,omitempty"`
6565
}
6666

67-
// ControlPlaneWait returns the configured ControlPlaneWaitDuration or a default
68-
// value if not configured.
69-
func (c ClusterBootstrapConfig) ControlPlaneWait() time.Duration {
70-
if v := c.Spec.ControlPlaneWaitDuration; v != nil {
71-
return v.Duration
72-
}
73-
return defaultWaitDuration
74-
}
75-
7667
//+kubebuilder:object:root=true
7768

7869
// ClusterBootstrapConfigList contains a list of ClusterBootstrapConfig

api/v1alpha1/clusterbootstrapconfig_types_test.go

Lines changed: 0 additions & 22 deletions
This file was deleted.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/capi.weave.works_clusterbootstrapconfigs.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6889,11 +6889,12 @@ spec:
68896889
- generateName
68906890
- spec
68916891
type: object
6892-
waitForControlPlane:
6893-
type: string
6892+
requireClusterReady:
6893+
type: boolean
68946894
required:
68956895
- clusterSelector
68966896
- jobTemplate
6897+
- requireClusterReady
68976898
type: object
68986899
status:
68996900
description: ClusterBootstrapConfigStatus defines the observed state of

config/manager/kustomization.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ generatorOptions:
55
disableNameSuffixHash: true
66

77
configMapGenerator:
8-
- name: manager-config
9-
files:
8+
- files:
109
- controller_manager_config.yaml
10+
name: manager-config
11+
apiVersion: kustomize.config.k8s.io/v1beta1
12+
kind: Kustomization
13+
images:
14+
- name: controller
15+
newName: bigkevmcd/cluster-bootstrap-controller
16+
newTag: ee04cdc

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ metadata:
66
creationTimestamp: null
77
name: manager-role
88
rules:
9+
- apiGroups:
10+
- ""
11+
resources:
12+
- secrets
13+
verbs:
14+
- get
15+
- list
16+
- watch
917
- apiGroups:
1018
- batch
1119
resources:

controllers/bootstrap_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
corev1 "k8s.io/api/core/v1"
1212
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1313
"k8s.io/apimachinery/pkg/runtime"
14+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
1415
ptrutils "k8s.io/utils/pointer"
1516
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1617
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -181,12 +182,18 @@ func makeTestClusterBootstrapConfig(opts ...func(*capiv1alpha1.ClusterBootstrapC
181182
}
182183

183184
func makeTestClient(t *testing.T, objs ...runtime.Object) client.Client {
185+
_, client := makeTestClientAndScheme(t, objs...)
186+
return client
187+
}
188+
189+
func makeTestClientAndScheme(t *testing.T, objs ...runtime.Object) (*runtime.Scheme, client.Client) {
184190
t.Helper()
185191
s := runtime.NewScheme()
192+
test.AssertNoError(t, clientgoscheme.AddToScheme(s))
186193
test.AssertNoError(t, capiv1alpha1.AddToScheme(s))
187194
test.AssertNoError(t, clusterv1.AddToScheme(s))
188195
test.AssertNoError(t, batchv1.AddToScheme(s))
189-
return fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
196+
return s, fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
190197
}
191198

192199
func makeTestVolume(name, secretName string) corev1.Volume {

controllers/cluster.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@ import (
66

77
corev1 "k8s.io/api/core/v1"
88
"sigs.k8s.io/controller-runtime/pkg/client"
9+
"sigs.k8s.io/controller-runtime/pkg/log"
910
)
1011

1112
// IsControlPlaneReady takes a client connected to a cluster and reports whether or
1213
// not the control-plane for the cluster is "ready".
1314
func IsControlPlaneReady(ctx context.Context, cl client.Client) (bool, error) {
15+
logger := log.FromContext(ctx)
1416
nodes := &corev1.NodeList{}
1517
// https://github.com/kubernetes/enhancements/blob/master/keps/sig-cluster-lifecycle/kubeadm/2067-rename-master-label-taint/README.md#design-details
1618
err := cl.List(ctx, nodes, client.HasLabels([]string{"node-role.kubernetes.io/control-plane"}))
1719
if err != nil {
1820
return false, fmt.Errorf("failed to query cluster node list: %w", err)
1921
}
22+
logger.Info("listed nodes", "count", len(nodes.Items))
2023

2124
readiness := []bool{}
2225
for _, node := range nodes.Items {
2326
for _, c := range node.Status.Conditions {
27+
logger.Info("node status", "type", c.Type, "status", c.Status)
2428
switch c.Type {
2529
case corev1.NodeReady:
2630
readiness = append(readiness, c.Status == corev1.ConditionTrue)
@@ -36,6 +40,7 @@ func IsControlPlaneReady(ctx context.Context, cl client.Client) (bool, error) {
3640
}
3741
return true
3842
}
43+
logger.Info("readiness", "len", len(readiness), "is-ready", isReady(readiness))
3944

4045
// If we have no statuses, then we really don't know if we're ready or not.
4146
return (len(readiness) > 0 && isReady(readiness)), nil

controllers/cluster_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1313
)
1414

15-
func TestIsMasterNodeReady(t *testing.T) {
16-
masterLabels := map[string]string{
15+
func TestIsControlPlaneReady(t *testing.T) {
16+
controlPlaneLabels := map[string]string{
1717
"node-role.kubernetes.io/master": "",
1818
"node-role.kubernetes.io/control-plane": "",
1919
"beta.kubernetes.io/arch": "amd64",
@@ -30,23 +30,23 @@ func TestIsMasterNodeReady(t *testing.T) {
3030
wantReady bool
3131
}{
3232
{
33-
name: "master node not ready",
34-
labels: masterLabels,
33+
name: "control plane not ready",
34+
labels: controlPlaneLabels,
3535
conditions: makeConditions(
3636
corev1.NodeCondition{Type: corev1.NodeReady, Status: corev1.ConditionFalse, LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletNotReady", Message: "container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized"},
3737
),
3838
},
3939
{
40-
name: "master node ready",
41-
labels: masterLabels,
40+
name: "control plane ready",
41+
labels: controlPlaneLabels,
4242
conditions: makeConditions(
4343
corev1.NodeCondition{Type: "NetworkUnavailable", Status: "False", LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "CalicoIsUp", Message: "Calico is running on this node"},
4444
corev1.NodeCondition{Type: "Ready", Status: "True", LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletReady", Message: "kubelet is posting ready status"},
4545
),
4646
wantReady: true,
4747
},
4848
{
49-
name: "no master nodes",
49+
name: "no control plane",
5050
labels: map[string]string{},
5151
conditions: makeConditions(
5252
corev1.NodeCondition{Type: corev1.NodeReady, Status: corev1.ConditionFalse, LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletNotReady", Message: "container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized"},

controllers/clusterbootstrapconfig_controller.go

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ import (
2020
"context"
2121
"encoding/json"
2222
"fmt"
23+
"time"
2324

25+
corev1 "k8s.io/api/core/v1"
2426
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2527
"k8s.io/apimachinery/pkg/labels"
2628
"k8s.io/apimachinery/pkg/runtime"
2729
"k8s.io/apimachinery/pkg/types"
30+
"k8s.io/client-go/tools/clientcmd"
2831
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2932
ctrl "sigs.k8s.io/controller-runtime"
3033
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3135
"sigs.k8s.io/controller-runtime/pkg/handler"
3236
"sigs.k8s.io/controller-runtime/pkg/log"
3337
"sigs.k8s.io/controller-runtime/pkg/source"
@@ -38,14 +42,27 @@ import (
3842
// ClusterBootstrapConfigReconciler reconciles a ClusterBootstrapConfig object
3943
type ClusterBootstrapConfigReconciler struct {
4044
client.Client
41-
Scheme *runtime.Scheme
45+
Scheme *runtime.Scheme
46+
configParser func(b []byte) (client.Client, error)
4247
}
4348

49+
func NewClusterBootstrapConfigReconciler(c client.Client, s *runtime.Scheme) *ClusterBootstrapConfigReconciler {
50+
return &ClusterBootstrapConfigReconciler{
51+
Client: c,
52+
Scheme: s,
53+
configParser: bytesToKubeConfig,
54+
}
55+
}
56+
57+
// TODO: make this configurable on the Spec
58+
var requeueAfterTime = time.Minute * 2
59+
4460
//+kubebuilder:rbac:groups=capi.weave.works,resources=clusterbootstrapconfigs,verbs=get;list;watch;create;update;patch;delete
4561
//+kubebuilder:rbac:groups=capi.weave.works,resources=clusterbootstrapconfigs/status,verbs=get;update;patch
4662
//+kubebuilder:rbac:groups=capi.weave.works,resources=clusterbootstrapconfigs/finalizers,verbs=update
4763
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
4864
//+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch;update;patch
65+
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
4966

5067
// Reconcile is part of the main kubernetes reconciliation loop which aims to
5168
// move the current state of the cluster closer to the desired state.
@@ -58,7 +75,6 @@ func (r *ClusterBootstrapConfigReconciler) Reconcile(ctx context.Context, req ct
5875
if err := r.Client.Get(ctx, req.NamespacedName, &clusterBootstrapConfig); err != nil {
5976
return ctrl.Result{}, client.IgnoreNotFound(err)
6077
}
61-
6278
logger.Info("cluster bootstrap config loaded", "name", clusterBootstrapConfig.ObjectMeta.Name)
6379

6480
clusters, err := r.getClustersBySelector(ctx, req.Namespace, clusterBootstrapConfig.Spec.ClusterSelector)
@@ -68,6 +84,22 @@ func (r *ClusterBootstrapConfigReconciler) Reconcile(ctx context.Context, req ct
6884
logger.Info("identified clusters for reconciliation", "clusterCount", len(clusters))
6985

7086
for _, c := range clusters {
87+
if clusterBootstrapConfig.Spec.RequireClusterReady {
88+
clusterName := types.NamespacedName{Name: c.GetName(), Namespace: c.GetNamespace()}
89+
clusterClient, err := r.clientForCluster(ctx, clusterName)
90+
if err != nil {
91+
return ctrl.Result{}, fmt.Errorf("failed to create client for cluster %s: %w", clusterName, err)
92+
}
93+
94+
ready, err := IsControlPlaneReady(ctx, clusterClient)
95+
if err != nil {
96+
return ctrl.Result{}, fmt.Errorf("failed to check readiness of cluster %s: %w", clusterName, err)
97+
}
98+
if !ready {
99+
logger.Info("waiting for control plane to be ready", "cluster", clusterName)
100+
return ctrl.Result{RequeueAfter: time.Minute * 2}, nil
101+
}
102+
}
71103
if err := bootstrapClusterWithConfig(ctx, logger, r.Client, c, &clusterBootstrapConfig); err != nil {
72104
return ctrl.Result{}, fmt.Errorf("failed to bootstrap cluster config: %w", err)
73105
}
@@ -170,3 +202,59 @@ func (r *ClusterBootstrapConfigReconciler) clusterToClusterBootstrapConfig(o cli
170202
}
171203
return result
172204
}
205+
206+
func (r *ClusterBootstrapConfigReconciler) clientForCluster(ctx context.Context, name types.NamespacedName) (client.Client, error) {
207+
kubeConfigBytes, err := r.getKubeConfig(ctx, name)
208+
if err != nil {
209+
return nil, err
210+
}
211+
212+
client, err := r.configParser(kubeConfigBytes)
213+
if err != nil {
214+
return nil, fmt.Errorf("getting client for cluster %s: %w", name, err)
215+
}
216+
return client, nil
217+
}
218+
219+
func (r *ClusterBootstrapConfigReconciler) getKubeConfig(ctx context.Context, cluster types.NamespacedName) ([]byte, error) {
220+
secretName := types.NamespacedName{
221+
Namespace: cluster.Namespace,
222+
Name: cluster.Name + "-kubeconfig",
223+
}
224+
225+
var secret corev1.Secret
226+
if err := r.Client.Get(ctx, secretName, &secret); err != nil {
227+
return nil, fmt.Errorf("unable to read KubeConfig secret %q error: %w", secretName, err)
228+
}
229+
230+
var kubeConfig []byte
231+
for k := range secret.Data {
232+
if k == "value" || k == "value.yaml" {
233+
kubeConfig = secret.Data[k]
234+
break
235+
}
236+
}
237+
238+
if len(kubeConfig) == 0 {
239+
return nil, fmt.Errorf("KubeConfig secret %q doesn't contain a 'value' key ", secretName)
240+
}
241+
242+
return kubeConfig, nil
243+
}
244+
245+
func bytesToKubeConfig(b []byte) (client.Client, error) {
246+
restConfig, err := clientcmd.RESTConfigFromKubeConfig(b)
247+
if err != nil {
248+
return nil, fmt.Errorf("failed to parse KubeConfig from secret: %w", err)
249+
}
250+
restMapper, err := apiutil.NewDynamicRESTMapper(restConfig)
251+
if err != nil {
252+
return nil, fmt.Errorf("failed to create RESTMapper from config: %w", err)
253+
}
254+
255+
client, err := client.New(restConfig, client.Options{Mapper: restMapper})
256+
if err != nil {
257+
return nil, fmt.Errorf("failed to create a client from config: %w", err)
258+
}
259+
return client, nil
260+
}

0 commit comments

Comments
 (0)