diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 94c6205..8ad03ba 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -12,6 +12,13 @@ rules: - get - list - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - "" resources: diff --git a/internal/controller/automatedclusterdiscovery_controller.go b/internal/controller/automatedclusterdiscovery_controller.go index 732d42b..0b85b00 100644 --- a/internal/controller/automatedclusterdiscovery_controller.go +++ b/internal/controller/automatedclusterdiscovery_controller.go @@ -44,20 +44,31 @@ import ( const k8sManagedByLabel = "app.kubernetes.io/managed-by" +type eventRecorder interface { + Event(object runtime.Object, eventtype, reason, message string) +} + // AutomatedClusterDiscoveryReconciler reconciles a AutomatedClusterDiscovery object type AutomatedClusterDiscoveryReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + EventRecorder eventRecorder AKSProvider func(string) providers.Provider } +// event emits a Kubernetes event and forwards the event to the event recorder +func (r *AutomatedClusterDiscoveryReconciler) event(obj *clustersv1alpha1.AutomatedClusterDiscovery, eventtype, reason, message string) { + r.EventRecorder.Event(obj, eventtype, reason, message) +} + //+kubebuilder:rbac:groups=clusters.weave.works,resources=automatedclusterdiscoveries,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=clusters.weave.works,resources=automatedclusterdiscoveries/status,verbs=get;update;patch //+kubebuilder:rbac:groups=clusters.weave.works,resources=automatedclusterdiscoveries/finalizers,verbs=update //+kubebuilder:rbac:groups=gitops.weave.works,resources=gitopsclusters,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -238,7 +249,6 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont } gitopsCluster.SetLabels(labelsForResource(*acd)) gitopsCluster.SetAnnotations(acd.Spec.CommonAnnotations) - _, err = controllerutil.CreateOrPatch(ctx, r.Client, gitopsCluster, func() error { gitopsCluster.Spec = gitopsv1alpha1.GitopsClusterSpec{ SecretRef: &meta.LocalObjectReference{ @@ -269,9 +279,11 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont return inventoryResources, fmt.Errorf("failed to set ownership on created Secret: %w", err) } + // publish event for ClusterCreated + r.event(acd, corev1.EventTypeNormal, "ClusterCreated", fmt.Sprintf("Cluster %s created", cluster.Name)) + secret.SetLabels(labelsForResource(*acd)) secret.SetAnnotations(acd.Spec.CommonAnnotations) - _, err = controllerutil.CreateOrPatch(ctx, r.Client, secret, func() error { value, err := clientcmd.Write(*cluster.KubeConfig) if err != nil { @@ -309,6 +321,9 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont if err := r.Client.Delete(ctx, cluster); err != nil { return inventoryResources, fmt.Errorf("failed to delete cluster: %w", err) } + + // publish event for ClusterRemoved + r.event(acd, corev1.EventTypeNormal, "ClusterRemoved", fmt.Sprintf("Cluster %s removed", cluster.GetName())) } } diff --git a/internal/controller/automatedclusterdiscovery_controller_test.go b/internal/controller/automatedclusterdiscovery_controller_test.go index 640426b..e323117 100644 --- a/internal/controller/automatedclusterdiscovery_controller_test.go +++ b/internal/controller/automatedclusterdiscovery_controller_test.go @@ -97,6 +97,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &testProvider }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -200,6 +201,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &testProvider }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -279,6 +281,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &testProvider }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -350,6 +353,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &testProvider }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -422,6 +426,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &testProvider }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -488,6 +493,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &testProvider }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -552,6 +558,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &testProvider }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -642,6 +649,87 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { assert.Equal(t, "testing", aksCluster.Annotations[meta.ReconcileRequestAnnotation]) assert.Equal(t, "testing", aksCluster.Status.LastHandledReconcileAt) }) + + t.Run("Reconcile publishes events on cluster creation and removal", func(t *testing.T) { + ctx := context.TODO() + aksCluster := &clustersv1alpha1.AutomatedClusterDiscovery{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-aks", + Namespace: "default", + }, + Spec: clustersv1alpha1.AutomatedClusterDiscoverySpec{ + Type: "aks", + AKS: &clustersv1alpha1.AKS{ + SubscriptionID: "subscription-123", + }, + Interval: metav1.Duration{Duration: time.Minute}, + }, + } + + err := k8sClient.Create(ctx, aksCluster) + assert.NoError(t, err) + defer deleteClusterDiscoveryAndInventory(t, k8sClient, aksCluster) + + cluster := &providers.ProviderCluster{ + Name: "cluster-1", + KubeConfig: &kubeconfig.Config{ + APIVersion: "v1", + Clusters: map[string]*kubeconfig.Cluster{ + "cluster-1": { + Server: "https://cluster-prod.example.com/", + CertificateAuthorityData: []uint8(testCAData), + }, + }, + }, + } + + testProvider := stubProvider{ + response: []*providers.ProviderCluster{ + cluster, + }, + } + + mockEventRecorder := &mockEventRecorder{} + + reconciler := &AutomatedClusterDiscoveryReconciler{ + Client: k8sClient, + Scheme: scheme, + AKSProvider: func(providerID string) providers.Provider { + return &testProvider + }, + EventRecorder: mockEventRecorder, + } + assert.NoError(t, reconciler.SetupWithManager(mgr)) + + _, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(aksCluster)}) + assert.NoError(t, err) + + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(aksCluster), aksCluster) + assert.NoError(t, err) + + secret := newSecret(types.NamespacedName{Name: "cluster-1-kubeconfig", Namespace: aksCluster.GetNamespace()}) + gitopsCluster := newGitopsCluster(secret.GetName(), types.NamespacedName{Name: "cluster-1", Namespace: aksCluster.GetNamespace()}) + assertInventoryHasItems(t, aksCluster, secret, gitopsCluster) + + assert.Equal(t, "Normal", mockEventRecorder.CapturedType) + assert.Equal(t, "ClusterCreated", mockEventRecorder.CapturedReason) + assert.Equal(t, "Cluster cluster-1 created", mockEventRecorder.CapturedMessage) + + testProvider.response = []*providers.ProviderCluster{} + + _, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(aksCluster)}) + assert.NoError(t, err) + + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(aksCluster), aksCluster) + assert.NoError(t, err) + + assertInventoryHasNoItems(t, aksCluster) + + assert.Equal(t, "Normal", mockEventRecorder.CapturedType) + assert.Equal(t, "ClusterRemoved", mockEventRecorder.CapturedReason) + assert.Equal(t, "Cluster cluster-1 removed", mockEventRecorder.CapturedMessage) + }) + } func TestReconcilingWithAnnotationChange(t *testing.T) { @@ -698,6 +786,7 @@ func TestReconcilingWithAnnotationChange(t *testing.T) { AKSProvider: func(providerID string) providers.Provider { return &stubProvider{} }, + EventRecorder: &mockEventRecorder{}, } assert.NoError(t, reconciler.SetupWithManager(mgr)) @@ -726,6 +815,20 @@ func TestReconcilingWithAnnotationChange(t *testing.T) { assert.Equal(t, aksCluster.Status.LastHandledReconcileAt, "testing") } +type mockEventRecorder struct { + CapturedObj runtime.Object + CapturedType string + CapturedReason string + CapturedMessage string +} + +func (m *mockEventRecorder) Event(object runtime.Object, eventtype, reason, message string) { + m.CapturedObj = object + m.CapturedType = eventtype + m.CapturedReason = reason + m.CapturedMessage = message +} + type stubProvider struct { response []*providers.ProviderCluster clusterID string