Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ name: "Build and push docker container"
on:
pull_request:
workflow_dispatch:


env:
GOPRIVATE: github.com/weaveworks/cluster-controller

jobs:
cluster-bootstrap-controller:
runs-on: ubuntu-latest
Expand All @@ -17,7 +20,15 @@ jobs:
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Configure git for private modules
env:
GITHUB_BUILD_USERNAME: ${{ secrets.BUILD_BOT_USER }}
GITHUB_BUILD_TOKEN: ${{ secrets.BUILD_BOT_PERSONAL_ACCESS_TOKEN }}
run: git config --global url."https://${GITHUB_BUILD_USERNAME}:${GITHUB_BUILD_TOKEN}@github.com".insteadOf "https://github.com"
- name: Build docker image
env:
GITHUB_BUILD_USERNAME: ${{ secrets.BUILD_BOT_USER }}
GITHUB_BUILD_TOKEN: ${{ secrets.BUILD_BOT_PERSONAL_ACCESS_TOKEN }}
run: |
make docker-build
- name: Login to Docker Hub
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Build the manager binary
FROM golang:1.17 as builder

ARG GITHUB_BUILD_USERNAME
ARG GITHUB_BUILD_TOKEN
RUN git config --global url."https://${GITHUB_BUILD_USERNAME}:${GITHUB_BUILD_TOKEN}@github.com".insteadOf "https://github.com"

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ run: manifests generate fmt vet ## Run a controller from your host.
go run ./main.go

docker-build: test ## Build docker image with the manager.
docker build -t ${IMG} .
docker build \
--build-arg=GITHUB_BUILD_TOKEN=$(GITHUB_BUILD_TOKEN) \
--build-arg=GITHUB_BUILD_USERNAME=$(GITHUB_BUILD_USERNAME) \
-t ${IMG} . \


docker-push: ## Push docker image with the manager.
docker push ${IMG}
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# cluster-bootstrap-controller

This is a controller that tracks [CAPI](https://github.com/kubernetes-sigs/cluster-api) [Cluster](https://cluster-api.sigs.k8s.io/developer/architecture/controllers/cluster.html) objects.
This is a controller that tracks [GitopsCluster] objects.

It provides a CR for a `ClusterBootstrapConfig` which provides a [Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/) template.

When a CAPI Cluster is "provisioned" a Job is created from the template, the template can access multiple fields.
When a GitopsCluster is "Ready" a Job is created from the template, the template can access multiple fields.

```yaml
apiVersion: capi.weave.works/v1alpha1
Expand Down Expand Up @@ -34,7 +34,7 @@ spec:
secretName: '{{ .ObjectMeta.Name }}-kubeconfig'
```

This is using Go [templating](https://pkg.go.dev/text/template) and the `Cluster` object is provided as the context, this means that expressions like `{{ .ObjectMeta.Name }}` will get the _name_ of the Cluster that has transitioned to "provisioned".
This is using Go [templating](https://pkg.go.dev/text/template) and the `GitopsCluster` object is provided as the context, this means that expressions like `{{ .ObjectMeta.Name }}` will get the _name_ of the GitopsCluster that has transitioned to "Ready".

## Annotations

Expand All @@ -55,8 +55,6 @@ e.g.

## Installation

You will need to have CAPI installed first, see the [CAPI Quick Start](https://cluster-api.sigs.k8s.io/user/quick-start.html).

Release files are available https://github.com/weaveworks/cluster-bootstrap-controller/releases

You can install these e.g.
Expand Down
6 changes: 3 additions & 3 deletions controllers/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"context"
"fmt"

gitopsv1alpha1 "github.com/weaveworks/cluster-controller/api/v1alpha1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

Expand All @@ -16,7 +16,7 @@ import (
)

// bootstrapCluster applies the jobs from a ClusterBootstrapConfig to a cluster.
func bootstrapClusterWithConfig(ctx context.Context, logger logr.Logger, c client.Client, cl *clusterv1.Cluster, bc *capiv1alpha1.ClusterBootstrapConfig) error {
func bootstrapClusterWithConfig(ctx context.Context, logger logr.Logger, c client.Client, cl *gitopsv1alpha1.GitopsCluster, bc *capiv1alpha1.ClusterBootstrapConfig) error {
job, err := renderTemplates(cl, jobFromTemplate(cl, bc.Spec.Template))
if err != nil {
return fmt.Errorf("failed to render job from template: %w", err)
Expand All @@ -31,7 +31,7 @@ func bootstrapClusterWithConfig(ctx context.Context, logger logr.Logger, c clien
return nil
}

func jobFromTemplate(cl *clusterv1.Cluster, jt capiv1alpha1.JobTemplate) *batchv1.Job {
func jobFromTemplate(cl *gitopsv1alpha1.GitopsCluster, jt capiv1alpha1.JobTemplate) *batchv1.Job {
return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
GenerateName: jt.GenerateName,
Expand Down
16 changes: 8 additions & 8 deletions controllers/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import (
"github.com/go-logr/logr"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
gitopsv1alpha1 "github.com/weaveworks/cluster-controller/api/v1alpha1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ptrutils "k8s.io/utils/pointer"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

Expand Down Expand Up @@ -76,8 +76,8 @@ func Test_bootstrapClusterWithConfig_sets_owner(t *testing.T) {

want := []metav1.OwnerReference{
{
APIVersion: "cluster.x-k8s.io/v1beta1",
Kind: "Cluster",
APIVersion: "gitops.weave.works/v1alpha1",
Kind: "GitopsCluster",
Name: testClusterName,
},
}
Expand All @@ -90,7 +90,7 @@ func Test_bootstrapClusterWithConfig_fail_to_create_job(t *testing.T) {
// This is a hacky test for making Create fail because of an unregistered
// type.
s := runtime.NewScheme()
test.AssertNoError(t, clusterv1.AddToScheme(s))
test.AssertNoError(t, gitopsv1alpha1.AddToScheme(s))
tc := fake.NewClientBuilder().WithScheme(s).Build()
bc := makeTestClusterBootstrapConfig()
cl := makeTestCluster()
Expand Down Expand Up @@ -120,13 +120,13 @@ func makeTestPodSpecWithVolumes(volumes ...corev1.Volume) corev1.PodSpec {
}
}

func makeTestCluster(opts ...func(*clusterv1.Cluster)) *clusterv1.Cluster {
c := &clusterv1.Cluster{
func makeTestCluster(opts ...func(*gitopsv1alpha1.GitopsCluster)) *gitopsv1alpha1.GitopsCluster {
c := &gitopsv1alpha1.GitopsCluster{
ObjectMeta: metav1.ObjectMeta{
Name: testClusterName,
Namespace: testNamespace,
},
Spec: clusterv1.ClusterSpec{},
Spec: gitopsv1alpha1.GitopsClusterSpec{},
}
for _, o := range opts {
o(c)
Expand Down Expand Up @@ -188,8 +188,8 @@ func makeTestClientAndScheme(t *testing.T, objs ...runtime.Object) (*runtime.Sch
s := runtime.NewScheme()
test.AssertNoError(t, clientgoscheme.AddToScheme(s))
test.AssertNoError(t, capiv1alpha1.AddToScheme(s))
test.AssertNoError(t, clusterv1.AddToScheme(s))
test.AssertNoError(t, batchv1.AddToScheme(s))
test.AssertNoError(t, gitopsv1alpha1.AddToScheme(s))
return s, fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
}

Expand Down
23 changes: 15 additions & 8 deletions controllers/clusterbootstrapconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import (
"encoding/json"
"fmt"

gitopsv1alpha1 "github.com/weaveworks/cluster-controller/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/clientcmd"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
Expand Down Expand Up @@ -131,13 +131,13 @@ func (r *ClusterBootstrapConfigReconciler) SetupWithManager(mgr ctrl.Manager) er
return ctrl.NewControllerManagedBy(mgr).
For(&capiv1alpha1.ClusterBootstrapConfig{}).
Watches(
&source.Kind{Type: &clusterv1.Cluster{}},
&source.Kind{Type: &gitopsv1alpha1.GitopsCluster{}},
handler.EnqueueRequestsFromMapFunc(r.clusterToClusterBootstrapConfig),
).
Complete(r)
}

func (r *ClusterBootstrapConfigReconciler) getClustersBySelector(ctx context.Context, ns string, ls metav1.LabelSelector) ([]*clusterv1.Cluster, error) {
func (r *ClusterBootstrapConfigReconciler) getClustersBySelector(ctx context.Context, ns string, ls metav1.LabelSelector) ([]*gitopsv1alpha1.GitopsCluster, error) {
logger := ctrl.LoggerFrom(ctx)
selector, err := metav1.LabelSelectorAsSelector(&ls)
if err != nil {
Expand All @@ -148,17 +148,24 @@ func (r *ClusterBootstrapConfigReconciler) getClustersBySelector(ctx context.Con
logger.Info("empty ClusterBootstrapConfig selector: no clusters are selected")
return nil, nil
}
clusterList := &clusterv1.ClusterList{}
clusterList := &gitopsv1alpha1.GitopsClusterList{}
if err := r.Client.List(ctx, clusterList, client.InNamespace(ns), client.MatchingLabelsSelector{Selector: selector}); err != nil {
return nil, fmt.Errorf("failed to list clusters: %w", err)
}

logger.Info("identified clusters with selector", "selector", selector, "count", len(clusterList.Items))
clusters := []*clusterv1.Cluster{}
clusters := []*gitopsv1alpha1.GitopsCluster{}
for i := range clusterList.Items {
c := &clusterList.Items[i]
if clusterv1.ClusterPhase(c.Status.Phase) != clusterv1.ClusterPhaseProvisioned {
logger.Info("cluster discarded - not provisioned", "phase", c.Status.Phase)

clusterFound := false
for _, condition := range c.Status.Conditions {
if condition.Type == "Ready" && condition.Status == metav1.ConditionTrue {
clusterFound = true
}
}
if !clusterFound {
logger.Info("cluster discarded - not provisioned", "phase", c.Status)
continue
}
if metav1.HasAnnotation(c.ObjectMeta, capiv1alpha1.BootstrappedAnnotation) {
Expand All @@ -175,7 +182,7 @@ func (r *ClusterBootstrapConfigReconciler) getClustersBySelector(ctx context.Con
// ClusterBootstrapConfig.
func (r *ClusterBootstrapConfigReconciler) clusterToClusterBootstrapConfig(o client.Object) []ctrl.Request {
result := []ctrl.Request{}
cluster, ok := o.(*clusterv1.Cluster)
cluster, ok := o.(*gitopsv1alpha1.GitopsCluster)
if !ok {
panic(fmt.Sprintf("Expected a Cluster but got a %T", o))
}
Expand Down
33 changes: 20 additions & 13 deletions controllers/clusterbootstrapconfig_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

capiv1alpha1 "github.com/weaveworks/cluster-bootstrap-controller/api/v1alpha1"
"github.com/weaveworks/cluster-bootstrap-controller/test"
gitopsv1alpha1 "github.com/weaveworks/cluster-controller/api/v1alpha1"
)

const testWaitDuration = time.Second * 55
Expand All @@ -35,9 +35,9 @@ func TestReconcile_when_cluster_not_ready(t *testing.T) {
"node-role.kubernetes.io/control-plane": "",
}, corev1.NodeCondition{Type: "Ready", Status: "False", LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletReady", Message: "kubelet is posting ready status"})

cl := makeTestCluster(func(c *clusterv1.Cluster) {
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
c.ObjectMeta.Labels = bc.Spec.ClusterSelector.MatchLabels
c.Status.Phase = string(clusterv1.ClusterPhaseProvisioned)
c.Status.Conditions = append(c.Status.Conditions, makeReadyCondition())
})
secret := makeTestSecret(types.NamespacedName{
Name: cl.GetName() + "-kubeconfig",
Expand Down Expand Up @@ -69,9 +69,9 @@ func TestReconcile_when_cluster_secret_not_available(t *testing.T) {
bc := makeTestClusterBootstrapConfig(func(c *capiv1alpha1.ClusterBootstrapConfig) {
c.Spec.RequireClusterReady = true
})
cl := makeTestCluster(func(c *clusterv1.Cluster) {
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
c.ObjectMeta.Labels = bc.Spec.ClusterSelector.MatchLabels
c.Status.Phase = string(clusterv1.ClusterPhaseProvisioned)
c.Status.Conditions = append(c.Status.Conditions, makeReadyCondition())
})
reconciler := makeTestReconciler(t, bc, cl)

Expand Down Expand Up @@ -104,9 +104,9 @@ func TestReconcile_when_cluster_ready(t *testing.T) {
}, corev1.NodeCondition{
Type: "Ready", Status: "True", LastHeartbeatTime: metav1.Now(), LastTransitionTime: metav1.Now(), Reason: "KubeletReady", Message: "kubelet is posting ready status"})

cl := makeTestCluster(func(c *clusterv1.Cluster) {
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
c.ObjectMeta.Labels = bc.Spec.ClusterSelector.MatchLabels
c.Status.Phase = string(clusterv1.ClusterPhaseProvisioned)
c.Status.Conditions = append(c.Status.Conditions, makeReadyCondition())
})
secret := makeTestSecret(types.NamespacedName{
Name: cl.GetName() + "-kubeconfig",
Expand Down Expand Up @@ -142,11 +142,11 @@ func TestReconcile_when_cluster_no_matching_labels(t *testing.T) {
bc := makeTestClusterBootstrapConfig(func(c *capiv1alpha1.ClusterBootstrapConfig) {
c.Spec.RequireClusterReady = true
})
cl := makeTestCluster(func(c *clusterv1.Cluster) {
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
c.ObjectMeta.Labels = map[string]string{
"will-not-match": "",
}
c.Status.Phase = string(clusterv1.ClusterPhaseProvisioned)
c.Status.Conditions = append(c.Status.Conditions, makeReadyCondition())
})
// This cheats by using the local client as the remote client to simplify
// getting the value from the remote client.
Expand Down Expand Up @@ -178,11 +178,11 @@ func TestReconcile_when_empty_label_selector(t *testing.T) {
}

})
cl := makeTestCluster(func(c *clusterv1.Cluster) {
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
c.ObjectMeta.Labels = map[string]string{
"will-not-match": "",
}
c.Status.Phase = string(clusterv1.ClusterPhaseProvisioned)
c.Status.Conditions = append(c.Status.Conditions, makeReadyCondition())
})
// This cheats by using the local client as the remote client to simplify
// getting the value from the remote client.
Expand Down Expand Up @@ -214,9 +214,9 @@ func TestReconcile_when_cluster_ready_and_old_label(t *testing.T) {
LastTransitionTime: metav1.Now(), Reason: "KubeletReady",
Message: "kubelet is posting ready status"})

cl := makeTestCluster(func(c *clusterv1.Cluster) {
cl := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
c.ObjectMeta.Labels = bc.Spec.ClusterSelector.MatchLabels
c.Status.Phase = string(clusterv1.ClusterPhaseProvisioned)
c.Status.Conditions = append(c.Status.Conditions, makeReadyCondition())
})
secret := makeTestSecret(types.NamespacedName{
Name: cl.GetName() + "-kubeconfig",
Expand Down Expand Up @@ -313,6 +313,13 @@ func Test_kubeConfigBytesToClient_with_invalidkubeconfig(t *testing.T) {
}
}

func makeReadyCondition() metav1.Condition {
return metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
}
}

func makeTestReconciler(t *testing.T, objs ...runtime.Object) *ClusterBootstrapConfigReconciler {
s, tc := makeTestClientAndScheme(t, objs...)
return NewClusterBootstrapConfigReconciler(tc, s)
Expand Down
6 changes: 3 additions & 3 deletions controllers/templating.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"fmt"
"text/template"

gitopsv1alpha1 "github.com/weaveworks/cluster-controller/api/v1alpha1"
batchv1 "k8s.io/api/batch/v1"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/yaml"
)

Expand All @@ -16,14 +16,14 @@ func lookup(m map[string]string) func(s string) string {
}
}

func makeFuncMap(cl *clusterv1.Cluster) template.FuncMap {
func makeFuncMap(cl *gitopsv1alpha1.GitopsCluster) template.FuncMap {
return template.FuncMap{
"annotation": lookup(cl.ObjectMeta.GetAnnotations()),
"label": lookup(cl.ObjectMeta.GetLabels()),
}
}

func renderTemplates(cl *clusterv1.Cluster, j *batchv1.Job) (*batchv1.Job, error) {
func renderTemplates(cl *gitopsv1alpha1.GitopsCluster, j *batchv1.Job) (*batchv1.Job, error) {
raw, err := yaml.Marshal(j)
if err != nil {
return nil, fmt.Errorf("failed to parse job as YAML: %w", err)
Expand Down
Loading