Skip to content

Commit 29bbde9

Browse files
authored
Merge pull request #7 from weaveworks/wait-for-controlplane
Wait for controlplane to be ready before creating the jobs in the origin cluster.
2 parents 0c07d87 + 7997448 commit 29bbde9

12 files changed

+696
-9
lines changed

api/v1alpha1/clusterbootstrapconfig_types.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@ limitations under the License.
1717
package v1alpha1
1818

1919
import (
20+
"time"
21+
2022
corev1 "k8s.io/api/core/v1"
2123
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2224
)
2325

26+
const defaultWaitDuration = time.Second * 60
27+
2428
const BootstrappedAnnotation = "capi.weave.works/bootstrapped"
2529

2630
// JobTemplate describes a job to create
@@ -40,7 +44,16 @@ type JobTemplate struct {
4044
// ClusterBootstrapConfigSpec defines the desired state of ClusterBootstrapConfig
4145
type ClusterBootstrapConfigSpec struct {
4246
ClusterSelector metav1.LabelSelector `json:"clusterSelector"`
43-
Template JobTemplate `json:"jobTemplate,omitempty"`
47+
Template JobTemplate `json:"jobTemplate"`
48+
49+
// Wait for the remote cluster to be "ready" before creating the jobs.
50+
// Defaults to false.
51+
//+kubebuilder:default:false
52+
RequireClusterReady bool `json:"requireClusterReady"`
53+
// When checking for readiness, this is the time to wait before
54+
// checking again.
55+
//+kubebuilder:default:60s
56+
ClusterReadinessBackoff *metav1.Duration `json:"clusterReadinessBackoff,omitempty"`
4457
}
4558

4659
// ClusterBootstrapConfigStatus defines the observed state of ClusterBootstrapConfig
@@ -59,6 +72,15 @@ type ClusterBootstrapConfig struct {
5972
Status ClusterBootstrapConfigStatus `json:"status,omitempty"`
6073
}
6174

75+
// ClusterReadinessRequeue returns the configured ClusterReadinessBackoff or a default
76+
// value if not configured.
77+
func (c ClusterBootstrapConfig) ClusterReadinessRequeue() time.Duration {
78+
if v := c.Spec.ClusterReadinessBackoff; v != nil {
79+
return v.Duration
80+
}
81+
return defaultWaitDuration
82+
}
83+
6284
//+kubebuilder:object:root=true
6385

6486
// ClusterBootstrapConfigList contains a list of ClusterBootstrapConfig
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2022.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"testing"
21+
"time"
22+
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
)
25+
26+
func TestControl(t *testing.T) {
27+
cfg := ClusterBootstrapConfig{}
28+
29+
if v := cfg.ClusterReadinessRequeue(); v != defaultWaitDuration {
30+
t.Fatalf("ClusterReadinessRequeue() got %v, want %v", v, defaultWaitDuration)
31+
}
32+
33+
want := time.Second * 20
34+
cfg.Spec.ClusterReadinessBackoff = &metav1.Duration{Duration: want}
35+
if v := cfg.ClusterReadinessRequeue(); v != want {
36+
t.Fatalf("ClusterReadinessRequeue() got %v, want %v", v, want)
37+
}
38+
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 6 additions & 0 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ spec:
3737
spec:
3838
description: ClusterBootstrapConfigSpec defines the desired state of ClusterBootstrapConfig
3939
properties:
40+
clusterReadinessBackoff:
41+
description: When checking for readiness, this is the time to wait
42+
before checking again.
43+
type: string
4044
clusterSelector:
4145
description: A label selector is a label query over a set of resources.
4246
The result of matchLabels and matchExpressions are ANDed. An empty
@@ -6889,8 +6893,14 @@ spec:
68896893
- generateName
68906894
- spec
68916895
type: object
6896+
requireClusterReady:
6897+
description: Wait for the remote cluster to be "ready" before creating
6898+
the jobs. Defaults to false.
6899+
type: boolean
68926900
required:
68936901
- clusterSelector
6902+
- jobTemplate
6903+
- requireClusterReady
68946904
type: object
68956905
status:
68966906
description: ClusterBootstrapConfigStatus defines the observed state of

config/default/kustomization.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Adds namespace to all resources.
2-
namespace: cluster-bootstrap-controller-system
2+
namespace: flux-system
33

44
# Value of this field is prepended to the
55
# names of all resources, e.g. a deployment named

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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/go-logr/logr"
8+
corev1 "k8s.io/api/core/v1"
9+
"sigs.k8s.io/controller-runtime/pkg/client"
10+
"sigs.k8s.io/controller-runtime/pkg/log"
11+
)
12+
13+
const (
14+
deprecatedControlPlaneLabel = "node-role.kubernetes.io/master"
15+
controlPlaneLabel = "node-role.kubernetes.io/control-plane"
16+
)
17+
18+
// IsControlPlaneReady takes a client connected to a cluster and reports whether or
19+
// not the control-plane for the cluster is "ready".
20+
func IsControlPlaneReady(ctx context.Context, cl client.Client) (bool, error) {
21+
logger := log.FromContext(ctx)
22+
readiness := []bool{}
23+
readyNodes, err := listReadyNodesWithLabel(ctx, logger, cl, controlPlaneLabel)
24+
if err != nil {
25+
return false, err
26+
}
27+
readiness = append(readiness, readyNodes...)
28+
29+
if len(readyNodes) == 0 {
30+
readyNodes, err := listReadyNodesWithLabel(ctx, logger, cl, deprecatedControlPlaneLabel)
31+
if err != nil {
32+
return false, err
33+
}
34+
readiness = append(readiness, readyNodes...)
35+
}
36+
37+
isReady := func(bools []bool) bool {
38+
for _, v := range bools {
39+
if !v {
40+
return false
41+
}
42+
}
43+
return true
44+
}
45+
logger.Info("readiness", "len", len(readiness), "is-ready", isReady(readiness))
46+
47+
// If we have no statuses, then we really don't know if we're ready or not.
48+
return (len(readiness) > 0 && isReady(readiness)), nil
49+
}
50+
51+
func listReadyNodesWithLabel(ctx context.Context, logger logr.Logger, cl client.Client, label string) ([]bool, error) {
52+
nodes := &corev1.NodeList{}
53+
// https://github.com/kubernetes/enhancements/blob/master/keps/sig-cluster-lifecycle/kubeadm/2067-rename-master-label-taint/README.md#design-details
54+
err := cl.List(ctx, nodes, client.HasLabels([]string{label}))
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to query cluster node list: %w", err)
57+
}
58+
logger.Info("listed nodes with control plane label", "label", label, "count", len(nodes.Items))
59+
60+
readiness := []bool{}
61+
for _, node := range nodes.Items {
62+
for _, c := range node.Status.Conditions {
63+
switch c.Type {
64+
case corev1.NodeReady:
65+
readiness = append(readiness, c.Status == corev1.ConditionTrue)
66+
}
67+
}
68+
}
69+
return readiness, nil
70+
}

controllers/cluster_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
corev1 "k8s.io/api/core/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/apimachinery/pkg/types"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
13+
)
14+
15+
func TestIsControlPlaneReady(t *testing.T) {
16+
controlPlaneLabels := map[string]string{
17+
"node-role.kubernetes.io/master": "",
18+
"node-role.kubernetes.io/control-plane": "",
19+
"beta.kubernetes.io/arch": "amd64",
20+
"beta.kubernetes.io/os": "linux",
21+
"kubernetes.io/arch": "amd64",
22+
"kubernetes.io/hostname": "kind-control-plane",
23+
"kubernetes.io/os": "linux",
24+
}
25+
26+
nodeTests := []struct {
27+
name string
28+
labels map[string]string
29+
conditions []corev1.NodeCondition
30+
wantReady bool
31+
}{
32+
{
33+
name: "control plane not ready",
34+
labels: controlPlaneLabels,
35+
conditions: makeConditions(
36+
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"},
37+
),
38+
},
39+
{
40+
name: "control plane ready",
41+
labels: controlPlaneLabels,
42+
conditions: makeConditions(
43+
corev1.NodeCondition{Type: "NetworkUnavailable", Status: "False", LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "CalicoIsUp", Message: "Calico is running on this node"},
44+
corev1.NodeCondition{Type: "Ready", Status: "True", LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletReady", Message: "kubelet is posting ready status"},
45+
),
46+
wantReady: true,
47+
},
48+
{
49+
name: "no control plane",
50+
labels: map[string]string{},
51+
conditions: makeConditions(
52+
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"},
53+
),
54+
},
55+
}
56+
for _, tt := range nodeTests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
cl := makeClient(makeNode(tt.labels, tt.conditions...))
59+
60+
ready, err := IsControlPlaneReady(context.TODO(), cl)
61+
if err != nil {
62+
t.Fatal(err)
63+
}
64+
65+
if ready != tt.wantReady {
66+
t.Fatalf("IsControlPlaneReady() got %v, want %v", ready, tt.wantReady)
67+
}
68+
})
69+
}
70+
}
71+
72+
func makeNode(labels map[string]string, conds ...corev1.NodeCondition) *corev1.Node {
73+
return &corev1.Node{
74+
ObjectMeta: metav1.ObjectMeta{
75+
Name: "test-control-plane",
76+
Labels: labels,
77+
UID: types.UID("f046e20e-df55-40d0-ab3a-76ff56617575"),
78+
},
79+
Spec: corev1.NodeSpec{},
80+
Status: corev1.NodeStatus{
81+
Conditions: conds,
82+
},
83+
}
84+
}
85+
86+
func makeClient(objs ...runtime.Object) client.Client {
87+
return fake.NewClientBuilder().WithRuntimeObjects(objs...).Build()
88+
}
89+
90+
func makeConditions(conds ...corev1.NodeCondition) []corev1.NodeCondition {
91+
base := []corev1.NodeCondition{
92+
corev1.NodeCondition{Type: corev1.NodeMemoryPressure, Status: corev1.ConditionFalse, LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletHasSufficientMemory", Message: "kubelet has sufficient memory available"},
93+
corev1.NodeCondition{Type: corev1.NodeDiskPressure, Status: corev1.ConditionFalse, LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletHasNoDiskPressure", Message: "kubelet has no disk pressure"},
94+
corev1.NodeCondition{Type: corev1.NodePIDPressure, Status: corev1.ConditionFalse, LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletHasSufficientPID", Message: "kubelet has sufficient PID available"},
95+
}
96+
return append(conds, base...)
97+
}

0 commit comments

Comments
 (0)