Skip to content

Commit 5fee3fb

Browse files
authored
Merge pull request #15 from weaveworks/trigger-on-provisioned
Trigger on provisioned
2 parents ba7d310 + f79891a commit 5fee3fb

11 files changed

+178
-37
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build the manager binary
2-
FROM golang:1.17 as builder
2+
FROM golang:1.19 as builder
33

44
ARG GITHUB_BUILD_USERNAME
55
ARG GITHUB_BUILD_TOKEN

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ IMAGE_TAG := $(shell tools/image-tag)
4040
IMG ?= $(IMAGE_TAG_BASE):$(IMAGE_TAG)
4141
CRD_OPTIONS ?= "crd"
4242
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
43-
ENVTEST_K8S_VERSION = 1.21
43+
ENVTEST_K8S_VERSION = 1.25
4444

4545
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
4646
ifeq (,$(shell go env GOBIN))

api/v1alpha1/clusterbootstrapconfig_types.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type JobTemplate struct {
4545
// guarantees (e.g. finalizers) will be honored. If this field is unset,
4646
// the Job won't be automatically deleted. If this field is set to zero,
4747
// the Job becomes eligible to be deleted immediately after it finishes.
48-
// +optional
48+
//+optional
4949
TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"`
5050

5151
// A batch/v1 Job is created with the Spec as the PodSpec.
@@ -57,6 +57,18 @@ type ClusterBootstrapConfigSpec struct {
5757
ClusterSelector metav1.LabelSelector `json:"clusterSelector"`
5858
Template JobTemplate `json:"jobTemplate"`
5959

60+
// Trigger the bootstrapping when the linked cluster has a True
61+
// "ClusterProvisioned" condition.
62+
//
63+
// A new job will not be triggered when the cluster is finally "Ready"
64+
// because it will already have the annotation that indicates the cluster
65+
// has been bootstrapped.
66+
//
67+
// Defaults to false.
68+
//+kubebuilder:default:false
69+
//+optional
70+
RequireClusterProvisioned bool `json:"requireClusterProvisioned"`
71+
6072
// Wait for the remote cluster to be "ready" before creating the jobs.
6173
// Defaults to false.
6274
//+kubebuilder:default:false

api/v1alpha1/groupversion_info.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ limitations under the License.
1515
*/
1616

1717
// Package v1alpha1 contains API Schema definitions for the capi v1alpha1 API group
18-
//+kubebuilder:object:generate=true
19-
//+groupName=capi.weave.works
18+
// +kubebuilder:object:generate=true
19+
// +groupName=capi.weave.works
2020
package v1alpha1
2121

2222
import (

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

+7
Original file line numberDiff line numberDiff line change
@@ -7196,6 +7196,13 @@ spec:
71967196
- generateName
71977197
- spec
71987198
type: object
7199+
requireClusterProvisioned:
7200+
description: "Trigger the bootstrapping when the linked cluster has
7201+
a True \"ClusterProvisioned\" condition. \n A new job will not be
7202+
triggered when the cluster is finally \"Ready\" because it will
7203+
already have the annotation that indicates the cluster has been
7204+
bootstrapped. \n Defaults to false."
7205+
type: boolean
71997206
requireClusterReady:
72007207
description: Wait for the remote cluster to be "ready" before creating
72017208
the jobs. Defaults to false.

config/default/manager_auth_proxy_patch.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ spec:
2525
- "--health-probe-bind-address=:8081"
2626
- "--metrics-bind-address=127.0.0.1:8080"
2727
- "--leader-elect"
28+
securityContext:
29+
allowPrivilegeEscalation: false
30+
capabilities:
31+
drop:
32+
- ALL

config/rbac/role.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,12 @@ rules:
6161
- patch
6262
- update
6363
- watch
64+
- apiGroups:
65+
- gitops.weave.works
66+
resources:
67+
- gitopsclusters
68+
verbs:
69+
- get
70+
- list
71+
- patch
72+
- watch

controllers/clusterbootstrapconfig_controller.go

+27-20
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"fmt"
2323

24+
"github.com/fluxcd/pkg/runtime/conditions"
2425
gitopsv1alpha1 "github.com/weaveworks/cluster-controller/api/v1alpha1"
2526
corev1 "k8s.io/api/core/v1"
2627
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -62,6 +63,7 @@ func NewClusterBootstrapConfigReconciler(c client.Client, s *runtime.Scheme) *Cl
6263
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
6364
//+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch;update;patch
6465
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
66+
// +kubebuilder:rbac:groups="gitops.weave.works",resources=gitopsclusters,verbs=get;watch;list;patch
6567

6668
// Reconcile is part of the main kubernetes reconciliation loop which aims to
6769
// move the current state of the cluster closer to the desired state.
@@ -76,15 +78,15 @@ func (r *ClusterBootstrapConfigReconciler) Reconcile(ctx context.Context, req ct
7678
}
7779
logger.Info("cluster bootstrap config loaded", "name", clusterBootstrapConfig.ObjectMeta.Name)
7880

79-
clusters, err := r.getClustersBySelector(ctx, req.Namespace, clusterBootstrapConfig.Spec.ClusterSelector)
81+
clusters, err := r.getClustersBySelector(ctx, req.Namespace, clusterBootstrapConfig.Spec)
8082
if err != nil {
8183
return ctrl.Result{}, fmt.Errorf("failed to getClustersBySelector for bootstrap config %s: %w", req, err)
8284
}
8385
logger.Info("identified clusters for reconciliation", "clusterCount", len(clusters))
8486

85-
for _, c := range clusters {
87+
for _, cluster := range clusters {
8688
if clusterBootstrapConfig.Spec.RequireClusterReady {
87-
clusterName := types.NamespacedName{Name: c.GetName(), Namespace: c.GetNamespace()}
89+
clusterName := types.NamespacedName{Name: cluster.GetName(), Namespace: cluster.GetNamespace()}
8890
clusterClient, err := r.clientForCluster(ctx, clusterName)
8991
if err != nil {
9092
if apierrors.IsNotFound(err) {
@@ -105,7 +107,7 @@ func (r *ClusterBootstrapConfigReconciler) Reconcile(ctx context.Context, req ct
105107
return ctrl.Result{RequeueAfter: clusterBootstrapConfig.ClusterReadinessRequeue()}, nil
106108
}
107109
}
108-
if err := bootstrapClusterWithConfig(ctx, logger, r.Client, c, &clusterBootstrapConfig); err != nil {
110+
if err := bootstrapClusterWithConfig(ctx, logger, r.Client, cluster, &clusterBootstrapConfig); err != nil {
109111
return ctrl.Result{}, fmt.Errorf("failed to bootstrap cluster config: %w", err)
110112
}
111113

@@ -119,8 +121,8 @@ func (r *ClusterBootstrapConfigReconciler) Reconcile(ctx context.Context, req ct
119121
if err != nil {
120122
return ctrl.Result{}, fmt.Errorf("failed to create a patch to update the cluster annotations: %w", err)
121123
}
122-
if err := r.Client.Patch(ctx, c, client.RawPatch(types.MergePatchType, mergePatch)); err != nil {
123-
return ctrl.Result{}, fmt.Errorf("failed to annotate cluster %s/%s as bootstrapped: %w", c.ObjectMeta.Name, c.ObjectMeta.Namespace, err)
124+
if err := r.Client.Patch(ctx, cluster, client.RawPatch(types.MergePatchType, mergePatch)); err != nil {
125+
return ctrl.Result{}, fmt.Errorf("failed to annotate cluster %s/%s as bootstrapped: %w", cluster.ObjectMeta.Name, cluster.ObjectMeta.Namespace, err)
124126
}
125127
}
126128
return ctrl.Result{}, nil
@@ -137,9 +139,9 @@ func (r *ClusterBootstrapConfigReconciler) SetupWithManager(mgr ctrl.Manager) er
137139
Complete(r)
138140
}
139141

140-
func (r *ClusterBootstrapConfigReconciler) getClustersBySelector(ctx context.Context, ns string, ls metav1.LabelSelector) ([]*gitopsv1alpha1.GitopsCluster, error) {
142+
func (r *ClusterBootstrapConfigReconciler) getClustersBySelector(ctx context.Context, ns string, spec capiv1alpha1.ClusterBootstrapConfigSpec) ([]*gitopsv1alpha1.GitopsCluster, error) {
141143
logger := ctrl.LoggerFrom(ctx)
142-
selector, err := metav1.LabelSelectorAsSelector(&ls)
144+
selector, err := metav1.LabelSelectorAsSelector(&spec.ClusterSelector)
143145
if err != nil {
144146
return nil, fmt.Errorf("unable to convert selector: %w", err)
145147
}
@@ -156,23 +158,24 @@ func (r *ClusterBootstrapConfigReconciler) getClustersBySelector(ctx context.Con
156158
logger.Info("identified clusters with selector", "selector", selector, "count", len(clusterList.Items))
157159
clusters := []*gitopsv1alpha1.GitopsCluster{}
158160
for i := range clusterList.Items {
159-
c := &clusterList.Items[i]
161+
cluster := &clusterList.Items[i]
160162

161-
clusterFound := false
162-
for _, condition := range c.Status.Conditions {
163-
if condition.Type == "Ready" && condition.Status == metav1.ConditionTrue {
164-
clusterFound = true
165-
}
166-
}
167-
if !clusterFound {
168-
logger.Info("cluster discarded - not provisioned", "phase", c.Status)
163+
if !conditions.IsReady(cluster) && !spec.RequireClusterProvisioned {
164+
logger.Info("cluster discarded - not ready", "phase", cluster.Status)
169165
continue
170166
}
171-
if metav1.HasAnnotation(c.ObjectMeta, capiv1alpha1.BootstrappedAnnotation) {
167+
if spec.RequireClusterProvisioned {
168+
if !isProvisioned(cluster) {
169+
logger.Info("waiting for cluster to be provisioned", "cluster", cluster.Name)
170+
continue
171+
}
172+
}
173+
174+
if metav1.HasAnnotation(cluster.ObjectMeta, capiv1alpha1.BootstrappedAnnotation) {
172175
continue
173176
}
174-
if c.DeletionTimestamp.IsZero() {
175-
clusters = append(clusters, c)
177+
if cluster.DeletionTimestamp.IsZero() {
178+
clusters = append(clusters, cluster)
176179
}
177180
}
178181
return clusters, nil
@@ -270,3 +273,7 @@ func kubeConfigBytesToClient(b []byte) (client.Client, error) {
270273
}
271274
return client, nil
272275
}
276+
277+
func isProvisioned(from conditions.Getter) bool {
278+
return conditions.IsTrue(from, gitopsv1alpha1.ClusterProvisionedCondition)
279+
}

controllers/clusterbootstrapconfig_controller_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,82 @@ func TestReconcile_when_cluster_ready(t *testing.T) {
138138
}
139139
}
140140

141+
func TestReconcile_when_cluster_provisioned(t *testing.T) {
142+
bc := makeTestClusterBootstrapConfig(func(c *capiv1alpha1.ClusterBootstrapConfig) {
143+
c.Spec.RequireClusterProvisioned = true
144+
})
145+
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
146+
c.ObjectMeta.Labels = bc.Spec.ClusterSelector.MatchLabels
147+
c.Status.Conditions = append(c.Status.Conditions, makeNotReadyCondition(), makeClusterProvisionedCondition())
148+
})
149+
secret := makeTestSecret(types.NamespacedName{
150+
Name: cl.GetName() + "-kubeconfig",
151+
Namespace: cl.GetNamespace(),
152+
}, map[string][]byte{"value": []byte("testing")})
153+
// This cheats by using the local client as the remote client to simplify
154+
// getting the value from the remote client.
155+
reconciler := makeTestReconciler(t, bc, cl, secret)
156+
reconciler.configParser = func(b []byte) (client.Client, error) {
157+
return reconciler.Client, nil
158+
}
159+
160+
result, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: types.NamespacedName{
161+
Name: bc.GetName(),
162+
Namespace: bc.GetNamespace(),
163+
}})
164+
if err != nil {
165+
t.Fatal(err)
166+
}
167+
if !result.IsZero() {
168+
t.Fatalf("want empty result, got %v", result)
169+
}
170+
var jobs batchv1.JobList
171+
if err := reconciler.List(context.TODO(), &jobs, client.InNamespace(testNamespace)); err != nil {
172+
t.Fatal(err)
173+
}
174+
if l := len(jobs.Items); l != 1 {
175+
t.Fatalf("found %d jobs, want %d", l, 1)
176+
}
177+
}
178+
179+
func TestReconcile_when_cluster_not_provisioned(t *testing.T) {
180+
bc := makeTestClusterBootstrapConfig(func(c *capiv1alpha1.ClusterBootstrapConfig) {
181+
c.Spec.RequireClusterProvisioned = true
182+
})
183+
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
184+
c.ObjectMeta.Labels = bc.Spec.ClusterSelector.MatchLabels
185+
c.Status.Conditions = append(c.Status.Conditions, makeNotReadyCondition())
186+
})
187+
secret := makeTestSecret(types.NamespacedName{
188+
Name: cl.GetName() + "-kubeconfig",
189+
Namespace: cl.GetNamespace(),
190+
}, map[string][]byte{"value": []byte("testing")})
191+
// This cheats by using the local client as the remote client to simplify
192+
// getting the value from the remote client.
193+
reconciler := makeTestReconciler(t, bc, cl, secret)
194+
reconciler.configParser = func(b []byte) (client.Client, error) {
195+
return reconciler.Client, nil
196+
}
197+
198+
result, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: types.NamespacedName{
199+
Name: bc.GetName(),
200+
Namespace: bc.GetNamespace(),
201+
}})
202+
if err != nil {
203+
t.Fatal(err)
204+
}
205+
if !result.IsZero() {
206+
t.Fatalf("want empty result, got %v", result)
207+
}
208+
var jobs batchv1.JobList
209+
if err := reconciler.List(context.TODO(), &jobs, client.InNamespace(testNamespace)); err != nil {
210+
t.Fatal(err)
211+
}
212+
if l := len(jobs.Items); l != 0 {
213+
t.Fatalf("found %d jobs, want %d", l, 1)
214+
}
215+
}
216+
141217
func TestReconcile_when_cluster_no_matching_labels(t *testing.T) {
142218
bc := makeTestClusterBootstrapConfig(func(c *capiv1alpha1.ClusterBootstrapConfig) {
143219
c.Spec.RequireClusterReady = true
@@ -320,6 +396,20 @@ func makeReadyCondition() metav1.Condition {
320396
}
321397
}
322398

399+
func makeClusterProvisionedCondition() metav1.Condition {
400+
return metav1.Condition{
401+
Type: gitopsv1alpha1.ClusterProvisionedCondition,
402+
Status: metav1.ConditionTrue,
403+
}
404+
}
405+
406+
func makeNotReadyCondition() metav1.Condition {
407+
return metav1.Condition{
408+
Type: "Ready",
409+
Status: metav1.ConditionFalse,
410+
}
411+
}
412+
323413
func makeTestReconciler(t *testing.T, objs ...runtime.Object) *ClusterBootstrapConfigReconciler {
324414
s, tc := makeTestClientAndScheme(t, objs...)
325415
return NewClusterBootstrapConfigReconciler(tc, s)

go.mod

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
module github.com/weaveworks/cluster-bootstrap-controller
22

3-
go 1.17
3+
go 1.19
44

55
require (
6+
github.com/fluxcd/pkg/runtime v0.13.2
67
github.com/go-logr/logr v1.2.2
78
github.com/google/go-cmp v0.5.7
8-
github.com/weaveworks/cluster-controller v0.0.0-20220412121721-313761dc9997
9+
github.com/weaveworks/cluster-controller v1.3.1
910
k8s.io/api v0.23.4
1011
k8s.io/apimachinery v0.23.4
1112
k8s.io/client-go v0.23.4
1213
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
13-
sigs.k8s.io/cluster-api v1.1.3
1414
sigs.k8s.io/controller-runtime v0.11.1
1515
sigs.k8s.io/yaml v1.3.0
1616
)
@@ -24,7 +24,6 @@ require (
2424
github.com/Azure/go-autorest/logger v0.2.1 // indirect
2525
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
2626
github.com/beorn7/perks v1.0.1 // indirect
27-
github.com/blang/semver v3.5.1+incompatible // indirect
2827
github.com/cespare/xxhash/v2 v2.1.2 // indirect
2928
github.com/davecgh/go-spew v1.1.1 // indirect
3029
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
@@ -43,6 +42,7 @@ require (
4342
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
4443
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
4544
github.com/modern-go/reflect2 v1.0.2 // indirect
45+
github.com/onsi/gomega v1.18.1 // indirect
4646
github.com/pkg/errors v0.9.1 // indirect
4747
github.com/prometheus/client_golang v1.12.1 // indirect
4848
github.com/prometheus/client_model v0.2.0 // indirect

0 commit comments

Comments
 (0)