diff --git a/api/v1alpha1/automatedclusterdiscovery_types.go b/api/v1alpha1/automatedclusterdiscovery_types.go index 3b3421a..c009816 100644 --- a/api/v1alpha1/automatedclusterdiscovery_types.go +++ b/api/v1alpha1/automatedclusterdiscovery_types.go @@ -26,17 +26,6 @@ type AKS struct { // SubscriptionID is the Azure subscription ID // +required SubscriptionID string `json:"subscriptionID"` - - Filter AKSFilter `json:"filter,omitempty"` - - // Exclude is the list of clusters to exclude - Exclude []string `json:"exclude,omitempty"` -} - -// Filter criteria for AKS clusters -type AKSFilter struct { - // Location is the location of the AKS clusters - Location string `json:"location,omitempty"` } // AutomatedClusterDiscoverySpec defines the desired state of AutomatedClusterDiscovery @@ -58,6 +47,11 @@ type AutomatedClusterDiscoverySpec struct { // AutomatedClusterDiscovery. // +optional Suspend bool `json:"suspend,omitempty"` + + // Labels to add to all generated resources. + CommonLabels map[string]string `json:"commonLabels,omitempty"` + // Annotations to add to all generated resources. + CommonAnnotations map[string]string `json:"commonAnnotations,omitempty"` } // AutomatedClusterDiscoveryStatus defines the observed state of AutomatedClusterDiscovery diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5b8eccf..15f7120 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -28,12 +28,6 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AKS) DeepCopyInto(out *AKS) { *out = *in - out.Filter = in.Filter - if in.Exclude != nil { - in, out := &in.Exclude, &out.Exclude - *out = make([]string, len(*in)) - copy(*out, *in) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AKS. @@ -46,21 +40,6 @@ func (in *AKS) DeepCopy() *AKS { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AKSFilter) DeepCopyInto(out *AKSFilter) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AKSFilter. -func (in *AKSFilter) DeepCopy() *AKSFilter { - if in == nil { - return nil - } - out := new(AKSFilter) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AutomatedClusterDiscovery) DeepCopyInto(out *AutomatedClusterDiscovery) { *out = *in @@ -126,9 +105,23 @@ func (in *AutomatedClusterDiscoverySpec) DeepCopyInto(out *AutomatedClusterDisco if in.AKS != nil { in, out := &in.AKS, &out.AKS *out = new(AKS) - (*in).DeepCopyInto(*out) + **out = **in } out.Interval = in.Interval + if in.CommonLabels != nil { + in, out := &in.CommonLabels, &out.CommonLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.CommonAnnotations != nil { + in, out := &in.CommonAnnotations, &out.CommonAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutomatedClusterDiscoverySpec. diff --git a/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml b/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml index 6b6fed8..1c2dd5d 100644 --- a/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml +++ b/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml @@ -46,24 +46,22 @@ spec: aks: description: AKS defines the desired state of AKS properties: - exclude: - description: Exclude is the list of clusters to exclude - items: - type: string - type: array - filter: - description: Filter criteria for AKS clusters - properties: - location: - description: Location is the location of the AKS clusters - type: string - type: object subscriptionID: description: SubscriptionID is the Azure subscription ID type: string required: - subscriptionID type: object + commonAnnotations: + additionalProperties: + type: string + description: Annotations to add to all generated resources. + type: object + commonLabels: + additionalProperties: + type: string + description: Labels to add to all generated resources. + type: object interval: description: The interval at which to run the discovery type: string diff --git a/internal/controller/automatedclusterdiscovery_controller.go b/internal/controller/automatedclusterdiscovery_controller.go index 2f4dcba..732d42b 100644 --- a/internal/controller/automatedclusterdiscovery_controller.go +++ b/internal/controller/automatedclusterdiscovery_controller.go @@ -188,7 +188,7 @@ func (r *AutomatedClusterDiscoveryReconciler) SetupWithManager(mgr ctrl.Manager) Complete(r) } -func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Context, clusters []*providers.ProviderCluster, currentClusterID string, cd *clustersv1alpha1.AutomatedClusterDiscovery) ([]clustersv1alpha1.ResourceRef, error) { +func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Context, clusters []*providers.ProviderCluster, currentClusterID string, acd *clustersv1alpha1.AutomatedClusterDiscovery) ([]clustersv1alpha1.ResourceRef, error) { logger := log.FromContext(ctx) logger.Info("reconciling clusters", "count", len(clusters)) @@ -220,7 +220,7 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont gitopsCluster := newGitopsCluster(secretName, types.NamespacedName{ Name: cluster.Name, - Namespace: cd.Namespace, + Namespace: acd.Namespace, }) clusterRef, err := clustersv1alpha1.ResourceRefFromObject(gitopsCluster) @@ -228,15 +228,17 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont return inventoryResources, err } - if isExistingRef(clusterRef, cd.Status.Inventory) { + if isExistingRef(clusterRef, acd.Status.Inventory) { existingClusters = append(existingClusters, clusterRef) } logger.Info("creating gitops cluster", "name", gitopsCluster.GetName()) - if err := controllerutil.SetOwnerReference(cd, gitopsCluster, r.Scheme); err != nil { + if err := controllerutil.SetOwnerReference(acd, gitopsCluster, r.Scheme); err != nil { return inventoryResources, fmt.Errorf("failed to set ownership on created GitopsCluster: %w", err) } - gitopsCluster.SetLabels(labelsForResource(*cd)) + 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{ @@ -254,7 +256,7 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont secret := newSecret(types.NamespacedName{ Name: secretName, - Namespace: cd.Namespace, + Namespace: acd.Namespace, }) secretRef, err := clustersv1alpha1.ResourceRefFromObject(secret) @@ -263,11 +265,13 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont } logger.Info("creating secret", "name", secret.GetName()) - if err := controllerutil.SetOwnerReference(cd, secret, r.Scheme); err != nil { + if err := controllerutil.SetOwnerReference(acd, secret, r.Scheme); err != nil { return inventoryResources, fmt.Errorf("failed to set ownership on created Secret: %w", err) } - secret.SetLabels(labelsForResource(*cd)) + 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 { @@ -284,9 +288,9 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont inventoryResources = append(inventoryResources, secretRef) } - if cd.Status.Inventory != nil { + if acd.Status.Inventory != nil { clustersToDelete := []client.Object{} - for _, item := range cd.Status.Inventory.Entries { + for _, item := range acd.Status.Inventory.Entries { obj, err := unstructuredFromResourceRef(item) if err != nil { return inventoryResources, err @@ -320,7 +324,7 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont } secretToUpdate := &corev1.Secret{} - if err := r.Client.Get(ctx, types.NamespacedName{Name: existingCluster.Spec.SecretRef.Name, Namespace: cd.GetNamespace()}, secretToUpdate); err != nil { + if err := r.Client.Get(ctx, types.NamespacedName{Name: existingCluster.Spec.SecretRef.Name, Namespace: acd.GetNamespace()}, secretToUpdate); err != nil { // TODO: don't error, create a new secret! return inventoryResources, fmt.Errorf("failed to get the secret to update: %w", err) } @@ -403,10 +407,28 @@ func clustersToMapping(clusters []*providers.ProviderCluster) map[string]*provid } func labelsForResource(acd clustersv1alpha1.AutomatedClusterDiscovery) map[string]string { - return map[string]string{ + appliedLabels := map[string]string{ k8sManagedByLabel: "cluster-reflector-controller", "clusters.weave.works/origin-name": acd.GetName(), "clusters.weave.works/origin-namespace": acd.GetNamespace(), "clusters.weave.works/origin-type": acd.Spec.Type, } + + return mergeMaps(acd.Spec.CommonLabels, appliedLabels) +} + +func mergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { + result := map[K]V{} + + for _, map_ := range maps { + if map_ == nil { + continue + } + + for k, v := range map_ { + result[k] = v + } + } + + return result } diff --git a/internal/controller/automatedclusterdiscovery_controller_test.go b/internal/controller/automatedclusterdiscovery_controller_test.go index da2b761..640426b 100644 --- a/internal/controller/automatedclusterdiscovery_controller_test.go +++ b/internal/controller/automatedclusterdiscovery_controller_test.go @@ -153,6 +153,91 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { assertHasOwnerReference(t, secret, clusterRef) }) + t.Run("Reconcile with common labels and annotations", func(t *testing.T) { + wantAnnotations := map[string]string{ + "test.example.com/annotation": "test", + "example.com/test": "annotation", + } + + 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}, + CommonLabels: map[string]string{ + "example.com/label": "test", + }, + CommonAnnotations: wantAnnotations, + }, + } + + testProvider := stubProvider{ + response: []*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), + }, + }, + }, + }, + }, + } + + reconciler := &AutomatedClusterDiscoveryReconciler{ + Client: k8sClient, + Scheme: scheme, + AKSProvider: func(providerID string) providers.Provider { + return &testProvider + }, + } + + assert.NoError(t, reconciler.SetupWithManager(mgr)) + + ctx := context.TODO() + key := types.NamespacedName{Name: aksCluster.Name, Namespace: aksCluster.Namespace} + err = k8sClient.Create(ctx, aksCluster) + assert.NoError(t, err) + defer deleteClusterDiscoveryAndInventory(t, k8sClient, aksCluster) + + result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: key}) + assert.NoError(t, err) + assert.Equal(t, ctrl.Result{RequeueAfter: time.Minute}, result) + + wantLabels := map[string]string{ + "app.kubernetes.io/managed-by": "cluster-reflector-controller", + "clusters.weave.works/origin-name": "test-aks", + "clusters.weave.works/origin-namespace": "default", + "clusters.weave.works/origin-type": "aks", + "example.com/label": "test", + } + + gitopsCluster := &gitopsv1alpha1.GitopsCluster{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: "cluster-1", Namespace: aksCluster.Namespace}, gitopsCluster) + assert.NoError(t, err) + assert.Equal(t, gitopsv1alpha1.GitopsClusterSpec{ + SecretRef: &meta.LocalObjectReference{Name: "cluster-1-kubeconfig"}, + }, gitopsCluster.Spec) + assertHasLabels(t, gitopsCluster, wantLabels) + assertHasAnnotations(t, gitopsCluster, wantAnnotations) + + secret := &corev1.Secret{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: "cluster-1-kubeconfig", Namespace: aksCluster.Namespace}, secret) + assert.NoError(t, err) + assertHasLabels(t, secret, wantLabels) + assertHasAnnotations(t, secret, wantAnnotations) + }) + t.Run("Reconcile when executing in cluster and cluster matches reflector cluster", func(t *testing.T) { aksCluster := &clustersv1alpha1.AutomatedClusterDiscovery{ ObjectMeta: metav1.ObjectMeta{ @@ -362,6 +447,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { assert.NoError(t, err) assert.Equal(t, value, secret.Data["value"]) }) + t.Run("Reconcile suspended cluster discovery resource", func(t *testing.T) { ctx := context.TODO() aksCluster := &clustersv1alpha1.AutomatedClusterDiscovery{ @@ -745,6 +831,18 @@ func assertHasLabels(t *testing.T, o client.Object, want map[string]string) { } } +func assertHasAnnotations(t *testing.T, o client.Object, want map[string]string) { + annotations := o.GetAnnotations() + for k, v := range want { + kv, ok := annotations[k] + if !ok { + t.Errorf("%s %s/%s is missing annotation %q with value %q", o.GetObjectKind().GroupVersionKind().Kind, o.GetNamespace(), o.GetName(), k, v) + continue + } + assert.Equal(t, v, kv) + } +} + func isOwnerReferenceEqual(a, b metav1.OwnerReference) bool { return (a.APIVersion == b.APIVersion) && (a.Kind == b.Kind) &&