diff --git a/api/v1alpha1/automatedclusterdiscovery_types.go b/api/v1alpha1/automatedclusterdiscovery_types.go index 05f8038..8b9c2ce 100644 --- a/api/v1alpha1/automatedclusterdiscovery_types.go +++ b/api/v1alpha1/automatedclusterdiscovery_types.go @@ -34,7 +34,7 @@ type AutomatedClusterDiscoverySpec struct { Name string `json:"name,omitempty"` // Type is the provider type. - // +kubebuilder:validation:Enum=aks + // +kubebuilder:validation:Enum=aks;capi Type string `json:"type"` // If DisableTags is true, labels will not be applied to the generated @@ -42,6 +42,7 @@ type AutomatedClusterDiscoverySpec struct { // +optional DisableTags bool `json:"disableTags"` + // AKS configures discovery of AKS clusters from Azure. AKS *AKS `json:"aks,omitempty"` // The interval at which to run the discovery diff --git a/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml b/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml index 18772e9..176fcd4 100644 --- a/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml +++ b/config/crd/bases/clusters.weave.works_automatedclusterdiscoveries.yaml @@ -80,6 +80,7 @@ spec: description: Type is the provider type. enum: - aks + - capi type: string required: - interval diff --git a/config/samples/capi_discovery.yaml b/config/samples/capi_discovery.yaml new file mode 100644 index 0000000..0d36039 --- /dev/null +++ b/config/samples/capi_discovery.yaml @@ -0,0 +1,8 @@ +apiVersion: clusters.weave.works/v1alpha1 +kind: AutomatedClusterDiscovery +metadata: + name: capi-cluster-discovery + namespace: default +spec: + type: capi + interval: 10m diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 1be6f99..c84cdc5 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,5 @@ ## Append samples of your project ## resources: -- aks_discovery.yaml + - aks_discovery.yaml + - capi_discovery.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/go.mod b/go.mod index d7324ef..2f91ab9 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( k8s.io/apimachinery v0.27.5 k8s.io/client-go v0.27.5 sigs.k8s.io/cli-utils v0.35.0 + sigs.k8s.io/cluster-api v1.5.2 sigs.k8s.io/controller-runtime v0.15.2 sigs.k8s.io/yaml v1.3.0 ) @@ -31,6 +32,8 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.10.0 // indirect @@ -66,7 +69,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -98,7 +100,6 @@ require ( k8s.io/component-base v0.27.4 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect - k8s.io/kubectl v0.27.2 // indirect k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.2 // indirect diff --git a/go.sum b/go.sum index 067e414..ca879a9 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,10 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -190,7 +194,6 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -425,11 +428,12 @@ k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= k8s.io/kubectl v0.27.2 h1:sSBM2j94MHBFRWfHIWtEXWCicViQzZsb177rNsKBhZg= -k8s.io/kubectl v0.27.2/go.mod h1:GCOODtxPcrjh+EC611MqREkU8RjYBh10ldQCQ6zpFKw= k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU= k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/cli-utils v0.35.0 h1:dfSJaF1W0frW74PtjwiyoB4cwdRygbHnC7qe7HF0g/Y= sigs.k8s.io/cli-utils v0.35.0/go.mod h1:ITitykCJxP1vaj1Cew/FZEaVJ2YsTN9Q71m02jebkoE= +sigs.k8s.io/cluster-api v1.5.2 h1:pCsyEHwTBb7n+U5Z2OA5STxdJ1EuSpJv8FLBx4lii3s= +sigs.k8s.io/cluster-api v1.5.2/go.mod h1:EGJUNpFWi7dF426tO8MG/jE+w7T0UO5KyMnOwQ5riUY= sigs.k8s.io/controller-runtime v0.15.2 h1:9V7b7SDQSJ08IIsJ6CY1CE85Okhp87dyTMNDG0FS7f4= sigs.k8s.io/controller-runtime v0.15.2/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/internal/controller/automatedclusterdiscovery_controller.go b/internal/controller/automatedclusterdiscovery_controller.go index c096ec6..964a6a4 100644 --- a/internal/controller/automatedclusterdiscovery_controller.go +++ b/internal/controller/automatedclusterdiscovery_controller.go @@ -54,7 +54,8 @@ type AutomatedClusterDiscoveryReconciler struct { Scheme *runtime.Scheme EventRecorder eventRecorder - AKSProvider func(string) providers.Provider + AKSProvider func(string) providers.Provider + CAPIProvider func(client.Client, string) providers.Provider } // event emits a Kubernetes event and forwards the event to the event recorder @@ -173,6 +174,19 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileResources(ctx context.Con return nil, err } + } else if cd.Spec.Type == "capi" { + logger.Info("reconciling CAPI cluster reflector", + "name", cd.Spec.Name, + ) + + capiProvider := r.CAPIProvider(r.Client, cd.Namespace) + + clusters, err = capiProvider.ListClusters(ctx) + if err != nil { + logger.Error(err, "failed to list CAPI clusters") + return nil, err + } + } // TODO: Fix this so that we record the inventoryRefs even if we get an @@ -230,7 +244,7 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont } secretName := fmt.Sprintf("%s-kubeconfig", cluster.Name) - gitopsCluster := newGitopsCluster(secretName, types.NamespacedName{ + gitopsCluster := newGitopsCluster(types.NamespacedName{ Name: cluster.Name, Namespace: acd.Namespace, }) @@ -259,12 +273,19 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont gitopsCluster.SetAnnotations(mergeMaps(acd.Spec.CommonAnnotations, map[string]string{ gitopsv1alpha1.GitOpsClusterNoSecretFinalizerAnnotation: "true", })) - _, err = controllerutil.CreateOrPatch(ctx, r.Client, gitopsCluster, func() error { - gitopsCluster.Spec = gitopsv1alpha1.GitopsClusterSpec{ - SecretRef: &meta.LocalObjectReference{ - Name: secretName, - }, + if acd.Spec.Type == "aks" { + gitopsCluster.Spec = gitopsv1alpha1.GitopsClusterSpec{ + SecretRef: &meta.LocalObjectReference{ + Name: secretName, + }, + } + } else if acd.Spec.Type == "capi" { + gitopsCluster.Spec = gitopsv1alpha1.GitopsClusterSpec{ + CAPIClusterRef: &meta.LocalObjectReference{ + Name: cluster.Name, + }, + } } return nil @@ -275,40 +296,18 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont inventoryResources = append(inventoryResources, clusterRef) - secret := newSecret(types.NamespacedName{ - Name: secretName, - Namespace: acd.Namespace, - }) - - secretRef, err := clustersv1alpha1.ResourceRefFromObject(secret) - if err != nil { - return inventoryResources, err - } + if cluster.KubeConfig != nil { + secretRef, err := r.createSecret(ctx, secretName, acd.Namespace, gitopsCluster, acd, cluster) + if err != nil { + return inventoryResources, err + } - logger.Info("creating secret", "name", secret.GetName()) - if err := controllerutil.SetOwnerReference(gitopsCluster, secret, r.Scheme); err != nil { - return inventoryResources, fmt.Errorf("failed to set ownership on created Secret: %w", err) + inventoryResources = append(inventoryResources, *secretRef) } // 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 { - return err - } - secret.Data["value"] = value - - return nil - }) - if err != nil { - return inventoryResources, err - } - - inventoryResources = append(inventoryResources, secretRef) } if acd.Status.Inventory != nil { @@ -349,21 +348,23 @@ func (r *AutomatedClusterDiscoveryReconciler) reconcileClusters(ctx context.Cont return inventoryResources, fmt.Errorf("failed to load GitopsCluster for update: %w", err) } - secretToUpdate := &corev1.Secret{} - 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) - } + if existingCluster.Spec.SecretRef != nil { + secretToUpdate := &corev1.Secret{} + 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) + } - cluster := clusterMapping[existingCluster.GetName()] - value, err := clientcmd.Write(*cluster.KubeConfig) - if err != nil { - return inventoryResources, err - } - secretToUpdate.Data["value"] = value - // TODO: Patch! - if err := r.Client.Update(ctx, secretToUpdate); err != nil { - return inventoryResources, err + cluster := clusterMapping[existingCluster.GetName()] + value, err := clientcmd.Write(*cluster.KubeConfig) + if err != nil { + return inventoryResources, err + } + secretToUpdate.Data["value"] = value + // TODO: Patch! + if err := r.Client.Update(ctx, secretToUpdate); err != nil { + return inventoryResources, err + } } } @@ -382,7 +383,47 @@ func (r *AutomatedClusterDiscoveryReconciler) patchStatus(ctx context.Context, r return r.Status().Patch(ctx, &set, patch) } -func newGitopsCluster(secretName string, name types.NamespacedName) *gitopsv1alpha1.GitopsCluster { +func (r *AutomatedClusterDiscoveryReconciler) createSecret(ctx context.Context, secretName, namespace string, gitopsCluster *gitopsv1alpha1.GitopsCluster, acd *clustersv1alpha1.AutomatedClusterDiscovery, cluster *providers.ProviderCluster) (*clustersv1alpha1.ResourceRef, error) { + logger := log.FromContext(ctx) + secret := newSecret(types.NamespacedName{ + Name: secretName, + Namespace: namespace, + }) + + secretRef, err := clustersv1alpha1.ResourceRefFromObject(secret) + if err != nil { + return nil, err + } + + logger.Info("creating secret", "name", secret.GetName()) + if err := controllerutil.SetOwnerReference(gitopsCluster, secret, r.Scheme); err != nil { + logger.Error(err, "failed to set ownership on created Secret") + return nil, fmt.Errorf("failed to set ownership on created Secret: %w", err) + } + + secret.SetLabels(labelsForResource(*acd)) + secret.SetAnnotations(acd.Spec.CommonAnnotations) + + // publish event for ClusterCreated + r.event(acd, corev1.EventTypeNormal, "ClusterCreated", fmt.Sprintf("Cluster %s created", cluster.Name)) + _, err = controllerutil.CreateOrPatch(ctx, r.Client, secret, func() error { + value, err := clientcmd.Write(*cluster.KubeConfig) + if err != nil { + return err + } + + secret.Data["value"] = value + + return nil + }) + if err != nil { + return nil, err + } + + return &secretRef, nil +} + +func newGitopsCluster(name types.NamespacedName) *gitopsv1alpha1.GitopsCluster { return &gitopsv1alpha1.GitopsCluster{ TypeMeta: metav1.TypeMeta{ Kind: "GitopsCluster", diff --git a/internal/controller/automatedclusterdiscovery_controller_test.go b/internal/controller/automatedclusterdiscovery_controller_test.go index b397a14..6084a7d 100644 --- a/internal/controller/automatedclusterdiscovery_controller_test.go +++ b/internal/controller/automatedclusterdiscovery_controller_test.go @@ -26,6 +26,7 @@ import ( gitopsv1alpha1 "github.com/weaveworks/cluster-controller/api/v1alpha1" clustersv1alpha1 "github.com/weaveworks/cluster-reflector-controller/api/v1alpha1" "github.com/weaveworks/cluster-reflector-controller/pkg/providers" + capiclusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) const testCAData = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJWVZjd0NTS1ZRVVV3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TWpBMk1qTXlNRE0zTlRoYUZ3MHlNekEyTWpNeU1ETTRNREJhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTMyZHBtRmlMdVFMSlZYUUYKdS9FamhuendPZmVSK3lvazkvMTJENzlEZ1U1OVVjTUVZQ2R1QzZhODVicUhxRXFiVnU2UUdrSFdmUHE4N0FIVApqMnQvM1kxVVprZ0hEOUtBWDZtdldlWnlBV2tRRUlKaHB2UjRtQ3J1T00zTXdUTHpkZlcrS01xVVBHeUZZM1Y0CjZFTmpCQ3RtdVBSaVZ3ZitZZGVwUkpGRHBLZ3MwOHJoMWUvZ3M5VlJWRlBrakIwVVIraHFMLy9KSmJ1S2NyUlgKUE5nWnE2K2N0ajZVaE9lWlZqVXc3WFNXQXZQNjIwMDZmV1V2K2dJYTVaMTRCUUZCN0Q4amhmWlRHNTE0bE04SwpIOXVsWTZJUktpaXcwcjdwdDZZV091VTdBQ3pWdjFkV3ozVUF0WWluSk15RVVOUFpneHp2VFpWNk1jSVB0QisvCkNCMExEd0lEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JUYXVnaWZneTMyYldGbWh5RjRyZlFRNkp2Ugp0akFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBRlVVV3IzS2tNcTBvaVBZcUE5UnVxNWdrTTEwM2hBV2FiQVBGCld5bjRWUmlkZGh2czgwbzR1bWJIN0xZSDdqSlBwdDRxZmR1VlNwLzdWRHFGakNvUkozQUtxd2EveU9vSDF2ZUIKYkVFQW1YQkM5clZEOUtjbVdrVzhtcC9xbFFqOFFvK1I3WFVCN3JxQTR2anJZVUQrYTg5NWFGb1oxTS9HWXpmTwptenNmaWJ6Y2o3RkZwWCtHOG94ZGkwWnY5eUx2WVFuTmU2aDFhWDgveGgzSmkyYlBjR3Y2aDR3RTFuaDdnV1JaClRHcTZzUFJyenlWSzdBc1Z2bk0wQ0tJTEpJN3k4cFB1S1BmajdBMTh6Uit5RDhvOXA2NmYxS2V0VnVaOUlOL1EKN1FoNUJRSXQwWk0xMi9iZ2ZoZDFxTWNrb2RoazF1eFFQSmVzZll1RzAxQ2dya3ZaZVE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" @@ -50,6 +51,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { assert.NoError(t, clustersv1alpha1.AddToScheme(scheme)) assert.NoError(t, gitopsv1alpha1.AddToScheme(scheme)) assert.NoError(t, clientgoscheme.AddToScheme(scheme)) + assert.NoError(t, capiclusterv1.AddToScheme(scheme)) k8sClient, err := client.New(cfg, client.Options{Scheme: scheme}) assert.NoError(t, err) @@ -141,7 +143,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { assertInventoryHasItems(t, aksCluster, newSecret(client.ObjectKeyFromObject(secret)), - newGitopsCluster(secret.GetName(), client.ObjectKeyFromObject(gitopsCluster))) + newGitopsCluster(client.ObjectKeyFromObject(gitopsCluster))) assertAutomatedClusterDiscoveryCondition(t, aksCluster, meta.ReadyCondition, "1 clusters discovered") discoveryRef := metav1.OwnerReference{ @@ -503,7 +505,6 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { defer deleteClusterDiscoveryAndInventory(t, k8sClient, aksCluster) gitopsCluster := newGitopsCluster( - "cluster-1-kubeconfig", types.NamespacedName{Name: "cluster-1", Namespace: "default"}, ) @@ -574,7 +575,6 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { defer deleteClusterDiscoveryAndInventory(t, k8sClient, aksCluster) gitopsCluster := newGitopsCluster( - "cluster-1-kubeconfig", types.NamespacedName{Name: "cluster-1", Namespace: "default"}, ) @@ -746,7 +746,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { 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()}) + gitopsCluster := newGitopsCluster(types.NamespacedName{Name: "cluster-1", Namespace: aksCluster.GetNamespace()}) assertInventoryHasItems(t, aksCluster, secret, gitopsCluster) assertAutomatedClusterDiscoveryCondition(t, aksCluster, meta.ReadyCondition, "1 clusters discovered") @@ -885,7 +885,7 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { 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()}) + gitopsCluster := newGitopsCluster(types.NamespacedName{Name: "cluster-1", Namespace: aksCluster.GetNamespace()}) assertInventoryHasItems(t, aksCluster, secret, gitopsCluster) assert.Equal(t, "Normal", mockEventRecorder.CapturedType) @@ -905,6 +905,108 @@ func TestAutomatedClusterDiscoveryReconciler(t *testing.T) { assert.Equal(t, "Normal", mockEventRecorder.CapturedType) assert.Equal(t, "ClusterRemoved", mockEventRecorder.CapturedReason) assert.Equal(t, "Cluster cluster-1 removed", mockEventRecorder.CapturedMessage) + + if err := k8sClient.Delete(ctx, secret); err != nil { + t.Fatal(err) + } + assert.NoError(t, client.IgnoreNotFound(k8sClient.Get(ctx, client.ObjectKeyFromObject(secret), secret))) + + }) + + t.Run("Reconcile with CAPI", func(t *testing.T) { + capiCluster := &clustersv1alpha1.AutomatedClusterDiscovery{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-capi", + Namespace: "default", + }, + Spec: clustersv1alpha1.AutomatedClusterDiscoverySpec{ + Type: "capi", + Interval: metav1.Duration{Duration: time.Minute}, + }, + } + + testProvider := stubProvider{ + response: []*providers.ProviderCluster{ + { + Name: "cluster-1", + KubeConfig: &kubeconfig.Config{ + APIVersion: "v1", + Clusters: map[string]*kubeconfig.Cluster{ + "cluster-1": {}, + }, + }, + }, + }, + } + + reconciler := &AutomatedClusterDiscoveryReconciler{ + Client: k8sClient, + Scheme: scheme, + CAPIProvider: func(capiclient client.Client, namespace string) providers.Provider { + return &testProvider + }, + EventRecorder: &mockEventRecorder{}, + } + + assert.NoError(t, reconciler.SetupWithManager(mgr)) + + ctx := context.TODO() + key := types.NamespacedName{Name: capiCluster.Name, Namespace: capiCluster.Namespace} + err = k8sClient.Create(ctx, capiCluster) + assert.NoError(t, err) + defer deleteClusterDiscoveryAndInventory(t, k8sClient, capiCluster) + + 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-capi", + "clusters.weave.works/origin-namespace": "default", + "clusters.weave.works/origin-type": "capi", + } + + gitopsCluster := &gitopsv1alpha1.GitopsCluster{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: "cluster-1", Namespace: capiCluster.Namespace}, gitopsCluster) + assert.NoError(t, err) + assert.Equal(t, gitopsv1alpha1.GitopsClusterSpec{ + CAPIClusterRef: &meta.LocalObjectReference{Name: "cluster-1"}, + }, gitopsCluster.Spec) + assertHasLabels(t, gitopsCluster, wantLabels) + + secret := &corev1.Secret{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: "cluster-1-kubeconfig", Namespace: capiCluster.Namespace}, secret) + assert.NoError(t, err) + assertHasLabels(t, secret, wantLabels) + + value, err := clientcmd.Write(*testProvider.response[0].KubeConfig) + assert.NoError(t, err) + assert.Equal(t, value, secret.Data["value"]) + + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(capiCluster), capiCluster) + assert.NoError(t, err) + + assertInventoryHasItems(t, capiCluster, + newSecret(client.ObjectKeyFromObject(secret)), + newGitopsCluster(client.ObjectKeyFromObject(gitopsCluster))) + assertAutomatedClusterDiscoveryCondition(t, capiCluster, meta.ReadyCondition, "1 clusters discovered") + + discoveryRef := metav1.OwnerReference{ + Kind: "AutomatedClusterDiscovery", + APIVersion: "clusters.weave.works/v1alpha1", + Name: capiCluster.Name, + UID: capiCluster.UID, + } + assertHasOwnerReference(t, gitopsCluster, discoveryRef) + + clusterRef := metav1.OwnerReference{ + Kind: "GitopsCluster", + APIVersion: "gitops.weave.works/v1alpha1", + Name: gitopsCluster.Name, + UID: gitopsCluster.UID, + } + assertHasOwnerReference(t, secret, clusterRef) }) } diff --git a/internal/controller/testdata/crds/clusterapi.yaml b/internal/controller/testdata/crds/clusterapi.yaml new file mode 100644 index 0000000..1ee6407 --- /dev/null +++ b/internal/controller/testdata/crds/clusterapi.yaml @@ -0,0 +1,1538 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: clusters.cluster.x-k8s.io +spec: + group: cluster.x-k8s.io + names: + categories: + - cluster-api + kind: Cluster + listKind: ClusterList + plural: clusters + shortNames: + - cl + singular: cluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Time duration since creation of Cluster + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Cluster status such as Pending/Provisioning/Provisioned/Deleting/Failed + jsonPath: .status.phase + name: Phase + type: string + deprecated: true + name: v1alpha4 + schema: + openAPIV3Schema: + description: + "Cluster is the Schema for the clusters API. \n Deprecated: This + type will be removed in one of the next releases." + properties: + apiVersion: + description: + "APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources" + type: string + kind: + description: + "Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + metadata: + type: object + spec: + description: ClusterSpec defines the desired state of Cluster. + properties: + clusterNetwork: + description: Cluster network configuration. + properties: + apiServerPort: + description: + APIServerPort specifies the port the API Server should + bind to. Defaults to 6443. + format: int32 + type: integer + pods: + description: The network ranges from which Pod networks are allocated. + properties: + cidrBlocks: + items: + type: string + type: array + required: + - cidrBlocks + type: object + serviceDomain: + description: Domain name for services. + type: string + services: + description: The network ranges from which service VIPs are allocated. + properties: + cidrBlocks: + items: + type: string + type: array + required: + - cidrBlocks + type: object + type: object + controlPlaneEndpoint: + description: + ControlPlaneEndpoint represents the endpoint used to + communicate with the control plane. + properties: + host: + description: The hostname on which the API server is serving. + type: string + port: + description: The port on which the API server is serving. + format: int32 + type: integer + required: + - host + - port + type: object + controlPlaneRef: + description: + ControlPlaneRef is an optional reference to a provider-specific + resource that holds the details for provisioning the Control Plane + for a Cluster. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: + 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + name: + description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + type: string + namespace: + description: "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + type: string + resourceVersion: + description: + "Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency" + type: string + uid: + description: "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids" + type: string + type: object + x-kubernetes-map-type: atomic + infrastructureRef: + description: + InfrastructureRef is a reference to a provider-specific + resource that holds the details for provisioning infrastructure + for a cluster in said provider. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: + 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + name: + description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + type: string + namespace: + description: "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + type: string + resourceVersion: + description: + "Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency" + type: string + uid: + description: "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids" + type: string + type: object + x-kubernetes-map-type: atomic + paused: + description: + Paused can be used to prevent controllers from processing + the Cluster and all its associated objects. + type: boolean + topology: + description: + "This encapsulates the topology for the cluster. NOTE: + It is required to enable the ClusterTopology feature gate flag to + activate managed topologies support; this feature is highly experimental, + and parts of it might still be not implemented." + properties: + class: + description: + The name of the ClusterClass object to create the + topology. + type: string + controlPlane: + description: ControlPlane describes the cluster control plane. + properties: + metadata: + description: + "Metadata is the metadata applied to the machines + of the ControlPlane. At runtime this metadata is merged + with the corresponding metadata from the ClusterClass. \n + This field is supported if and only if the control plane + provider template referenced in the ClusterClass is Machine + based." + properties: + annotations: + additionalProperties: + type: string + description: + "Annotations is an unstructured key value + map stored with a resource that may be set by external + tools to store and retrieve arbitrary metadata. They + are not queryable and should be preserved when modifying + objects. More info: http://kubernetes.io/docs/user-guide/annotations" + type: object + labels: + additionalProperties: + type: string + description: + "Map of string keys and values that can be + used to organize and categorize (scope and select) objects. + May match selectors of replication controllers and services. + More info: http://kubernetes.io/docs/user-guide/labels" + type: object + type: object + replicas: + description: + Replicas is the number of control plane nodes. + If the value is nil, the ControlPlane object is created + without the number of Replicas and it's assumed that the + control plane controller does not implement support for + this field. When specified against a control plane provider + that lacks support for this field, this value will be ignored. + format: int32 + type: integer + type: object + rolloutAfter: + description: + RolloutAfter performs a rollout of the entire cluster + one component at a time, control plane first and then machine + deployments. + format: date-time + type: string + version: + description: The Kubernetes version of the cluster. + type: string + workers: + description: + Workers encapsulates the different constructs that + form the worker nodes for the cluster. + properties: + machineDeployments: + description: + MachineDeployments is a list of machine deployments + in the cluster. + items: + description: + MachineDeploymentTopology specifies the different + parameters for a set of worker nodes in the topology. + This set of nodes is managed by a MachineDeployment object + whose lifecycle is managed by the Cluster controller. + properties: + class: + description: + Class is the name of the MachineDeploymentClass + used to create the set of worker nodes. This should + match one of the deployment classes defined in the + ClusterClass object mentioned in the `Cluster.Spec.Class` + field. + type: string + metadata: + description: + Metadata is the metadata applied to the + machines of the MachineDeployment. At runtime this + metadata is merged with the corresponding metadata + from the ClusterClass. + properties: + annotations: + additionalProperties: + type: string + description: + "Annotations is an unstructured key + value map stored with a resource that may be set + by external tools to store and retrieve arbitrary + metadata. They are not queryable and should be + preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations" + type: object + labels: + additionalProperties: + type: string + description: + "Map of string keys and values that + can be used to organize and categorize (scope + and select) objects. May match selectors of replication + controllers and services. More info: http://kubernetes.io/docs/user-guide/labels" + type: object + type: object + name: + description: + Name is the unique identifier for this + MachineDeploymentTopology. The value is used with + other unique identifiers to create a MachineDeployment's + Name (e.g. cluster's name, etc). In case the name + is greater than the allowed maximum length, the values + are hashed together. + type: string + replicas: + description: + Replicas is the number of worker nodes + belonging to this set. If the value is nil, the MachineDeployment + is created without the number of Replicas (defaulting + to zero) and it's assumed that an external entity + (like cluster autoscaler) is responsible for the management + of this value. + format: int32 + type: integer + required: + - class + - name + type: object + type: array + type: object + required: + - class + - version + type: object + type: object + status: + description: ClusterStatus defines the observed state of Cluster. + properties: + conditions: + description: Conditions defines current service state of the cluster. + items: + description: + Condition defines an observation of a Cluster API resource + operational state. + properties: + lastTransitionTime: + description: + Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: + A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: + The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: + Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - status + - type + type: object + type: array + controlPlaneReady: + description: ControlPlaneReady defines if the control plane is ready. + type: boolean + failureDomains: + additionalProperties: + description: + FailureDomainSpec is the Schema for Cluster API failure + domains. It allows controllers to understand how many failure + domains a cluster can optionally span across. + properties: + attributes: + additionalProperties: + type: string + description: + Attributes is a free form map of attributes an + infrastructure provider might use or require. + type: object + controlPlane: + description: + ControlPlane determines if this failure domain + is suitable for use by control plane machines. + type: boolean + type: object + description: + FailureDomains is a slice of failure domain objects synced + from the infrastructure provider. + type: object + failureMessage: + description: + FailureMessage indicates that there is a fatal problem + reconciling the state, and will be set to a descriptive error message. + type: string + failureReason: + description: + FailureReason indicates that there is a fatal problem + reconciling the state, and will be set to a token value suitable + for programmatic interpretation. + type: string + infrastructureReady: + description: + InfrastructureReady is the state of the infrastructure + provider. + type: boolean + observedGeneration: + description: + ObservedGeneration is the latest generation observed + by the controller. + format: int64 + type: integer + phase: + description: + Phase represents the current phase of cluster actuation. + E.g. Pending, Running, Terminating, Failed etc. + type: string + type: object + type: object + served: false + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: + ClusterClass of this Cluster, empty if the Cluster is not using + a ClusterClass + jsonPath: .spec.topology.class + name: ClusterClass + type: string + - description: Cluster status such as Pending/Provisioning/Provisioned/Deleting/Failed + jsonPath: .status.phase + name: Phase + type: string + - description: Time duration since creation of Cluster + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Kubernetes version associated with this Cluster + jsonPath: .spec.topology.version + name: Version + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: Cluster is the Schema for the clusters API. + properties: + apiVersion: + description: + "APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources" + type: string + kind: + description: + "Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + metadata: + type: object + spec: + description: ClusterSpec defines the desired state of Cluster. + properties: + clusterNetwork: + description: Cluster network configuration. + properties: + apiServerPort: + description: + APIServerPort specifies the port the API Server should + bind to. Defaults to 6443. + format: int32 + type: integer + pods: + description: The network ranges from which Pod networks are allocated. + properties: + cidrBlocks: + items: + type: string + type: array + required: + - cidrBlocks + type: object + serviceDomain: + description: Domain name for services. + type: string + services: + description: The network ranges from which service VIPs are allocated. + properties: + cidrBlocks: + items: + type: string + type: array + required: + - cidrBlocks + type: object + type: object + controlPlaneEndpoint: + description: + ControlPlaneEndpoint represents the endpoint used to + communicate with the control plane. + properties: + host: + description: The hostname on which the API server is serving. + type: string + port: + description: The port on which the API server is serving. + format: int32 + type: integer + required: + - host + - port + type: object + controlPlaneRef: + description: + ControlPlaneRef is an optional reference to a provider-specific + resource that holds the details for provisioning the Control Plane + for a Cluster. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: + 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + name: + description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + type: string + namespace: + description: "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + type: string + resourceVersion: + description: + "Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency" + type: string + uid: + description: "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids" + type: string + type: object + x-kubernetes-map-type: atomic + infrastructureRef: + description: + InfrastructureRef is a reference to a provider-specific + resource that holds the details for provisioning infrastructure + for a cluster in said provider. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: + 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + name: + description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + type: string + namespace: + description: "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + type: string + resourceVersion: + description: + "Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency" + type: string + uid: + description: "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids" + type: string + type: object + x-kubernetes-map-type: atomic + paused: + description: + Paused can be used to prevent controllers from processing + the Cluster and all its associated objects. + type: boolean + topology: + description: + "This encapsulates the topology for the cluster. NOTE: + It is required to enable the ClusterTopology feature gate flag to + activate managed topologies support; this feature is highly experimental, + and parts of it might still be not implemented." + properties: + class: + description: + The name of the ClusterClass object to create the + topology. + type: string + controlPlane: + description: ControlPlane describes the cluster control plane. + properties: + machineHealthCheck: + description: + MachineHealthCheck allows to enable, disable + and override the MachineHealthCheck configuration in the + ClusterClass for this control plane. + properties: + enable: + description: + "Enable controls if a MachineHealthCheck + should be created for the target machines. \n If false: + No MachineHealthCheck will be created. \n If not set(default): + A MachineHealthCheck will be created if it is defined + here or in the associated ClusterClass. If no MachineHealthCheck + is defined then none will be created. \n If true: A + MachineHealthCheck is guaranteed to be created. Cluster + validation will block if `enable` is true and no MachineHealthCheck + definition is available." + type: boolean + maxUnhealthy: + anyOf: + - type: integer + - type: string + description: + Any further remediation is only allowed if + at most "MaxUnhealthy" machines selected by "selector" + are not healthy. + x-kubernetes-int-or-string: true + nodeStartupTimeout: + description: + Machines older than this duration without + a node will be considered to have failed and will be + remediated. If you wish to disable this feature, set + the value explicitly to 0. + type: string + remediationTemplate: + description: + "RemediationTemplate is a reference to a + remediation template provided by an infrastructure provider. + \n This field is completely optional, when filled, the + MachineHealthCheck controller creates a new object from + the template referenced and hands off remediation of + the machine to a controller that lives outside of Cluster + API." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: + 'If referring to a piece of an object + instead of an entire object, this string should + contain a valid JSON/Go field access statement, + such as desiredState.manifest.containers[2]. For + example, if the object reference is to a container + within a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container + that triggered the event) or if no container name + is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part + of an object. TODO: this design is not final and + this field is subject to change in the future.' + type: string + kind: + description: "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + name: + description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + type: string + namespace: + description: + "Namespace of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + type: string + resourceVersion: + description: + "Specific resourceVersion to which this + reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency" + type: string + uid: + description: "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids" + type: string + type: object + x-kubernetes-map-type: atomic + unhealthyConditions: + description: + UnhealthyConditions contains a list of the + conditions that determine whether a node is considered + unhealthy. The conditions are combined in a logical + OR, i.e. if any of the conditions is met, the node is + unhealthy. + items: + description: + UnhealthyCondition represents a Node condition + type and value with a timeout specified as a duration. When + the named condition has been in the given status for + at least the timeout value, a node is considered unhealthy. + properties: + status: + minLength: 1 + type: string + timeout: + type: string + type: + minLength: 1 + type: string + required: + - status + - timeout + - type + type: object + type: array + unhealthyRange: + description: + 'Any further remediation is only allowed + if the number of machines selected by "selector" as + not healthy is within the range of "UnhealthyRange". + Takes precedence over MaxUnhealthy. Eg. "[3-5]" - This + means that remediation will be allowed only when: (a) + there are at least 3 unhealthy machines (and) (b) there + are at most 5 unhealthy machines' + pattern: ^\[[0-9]+-[0-9]+\]$ + type: string + type: object + metadata: + description: + Metadata is the metadata applied to the ControlPlane + and the Machines of the ControlPlane if the ControlPlaneTemplate + referenced by the ClusterClass is machine based. If not, + it is applied only to the ControlPlane. At runtime this + metadata is merged with the corresponding metadata from + the ClusterClass. + properties: + annotations: + additionalProperties: + type: string + description: + "Annotations is an unstructured key value + map stored with a resource that may be set by external + tools to store and retrieve arbitrary metadata. They + are not queryable and should be preserved when modifying + objects. More info: http://kubernetes.io/docs/user-guide/annotations" + type: object + labels: + additionalProperties: + type: string + description: + "Map of string keys and values that can be + used to organize and categorize (scope and select) objects. + May match selectors of replication controllers and services. + More info: http://kubernetes.io/docs/user-guide/labels" + type: object + type: object + nodeDeletionTimeout: + description: + NodeDeletionTimeout defines how long the controller + will attempt to delete the Node that the Machine hosts after + the Machine is marked for deletion. A duration of 0 will + retry deletion indefinitely. Defaults to 10 seconds. + type: string + nodeDrainTimeout: + description: + "NodeDrainTimeout is the total amount of time + that the controller will spend on draining a node. The default + value is 0, meaning that the node can be drained without + any time limitations. NOTE: NodeDrainTimeout is different + from `kubectl drain --timeout`" + type: string + nodeVolumeDetachTimeout: + description: + NodeVolumeDetachTimeout is the total amount of + time that the controller will spend on waiting for all volumes + to be detached. The default value is 0, meaning that the + volumes can be detached without any time limitations. + type: string + replicas: + description: + Replicas is the number of control plane nodes. + If the value is nil, the ControlPlane object is created + without the number of Replicas and it's assumed that the + control plane controller does not implement support for + this field. When specified against a control plane provider + that lacks support for this field, this value will be ignored. + format: int32 + type: integer + type: object + rolloutAfter: + description: + "RolloutAfter performs a rollout of the entire cluster + one component at a time, control plane first and then machine + deployments. \n Deprecated: This field has no function and is + going to be removed in the next apiVersion." + format: date-time + type: string + variables: + description: + Variables can be used to customize the Cluster through + patches. They must comply to the corresponding VariableClasses + defined in the ClusterClass. + items: + description: + ClusterVariable can be used to customize the Cluster + through patches. Each ClusterVariable is associated with a + Variable definition in the ClusterClass `status` variables. + properties: + definitionFrom: + description: + "DefinitionFrom specifies where the definition + of this Variable is from. DefinitionFrom is `inline` when + the definition is from the ClusterClass `.spec.variables` + or the name of a patch defined in the ClusterClass `.spec.patches` + where the patch is external and provides external variables. + This field is mandatory if the variable has `DefinitionsConflict: + true` in ClusterClass `status.variables[]`" + type: string + name: + description: Name of the variable. + type: string + value: + description: + "Value of the variable. Note: the value will + be validated against the schema of the corresponding ClusterClassVariable + from the ClusterClass. Note: We have to use apiextensionsv1.JSON + instead of a custom JSON type, because controller-tools + has a hard-coded schema for apiextensionsv1.JSON which + cannot be produced by another type via controller-tools, + i.e. it is not possible to have no type field. Ref: https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111" + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array + version: + description: The Kubernetes version of the cluster. + type: string + workers: + description: + Workers encapsulates the different constructs that + form the worker nodes for the cluster. + properties: + machineDeployments: + description: + MachineDeployments is a list of machine deployments + in the cluster. + items: + description: + MachineDeploymentTopology specifies the different + parameters for a set of worker nodes in the topology. + This set of nodes is managed by a MachineDeployment object + whose lifecycle is managed by the Cluster controller. + properties: + class: + description: + Class is the name of the MachineDeploymentClass + used to create the set of worker nodes. This should + match one of the deployment classes defined in the + ClusterClass object mentioned in the `Cluster.Spec.Class` + field. + type: string + failureDomain: + description: + FailureDomain is the failure domain the + machines will be created in. Must match a key in the + FailureDomains map stored on the cluster object. + type: string + machineHealthCheck: + description: + MachineHealthCheck allows to enable, disable + and override the MachineHealthCheck configuration + in the ClusterClass for this MachineDeployment. + properties: + enable: + description: + "Enable controls if a MachineHealthCheck + should be created for the target machines. \n + If false: No MachineHealthCheck will be created. + \n If not set(default): A MachineHealthCheck will + be created if it is defined here or in the associated + ClusterClass. If no MachineHealthCheck is defined + then none will be created. \n If true: A MachineHealthCheck + is guaranteed to be created. Cluster validation + will block if `enable` is true and no MachineHealthCheck + definition is available." + type: boolean + maxUnhealthy: + anyOf: + - type: integer + - type: string + description: + Any further remediation is only allowed + if at most "MaxUnhealthy" machines selected by + "selector" are not healthy. + x-kubernetes-int-or-string: true + nodeStartupTimeout: + description: + Machines older than this duration without + a node will be considered to have failed and will + be remediated. If you wish to disable this feature, + set the value explicitly to 0. + type: string + remediationTemplate: + description: + "RemediationTemplate is a reference + to a remediation template provided by an infrastructure + provider. \n This field is completely optional, + when filled, the MachineHealthCheck controller + creates a new object from the template referenced + and hands off remediation of the machine to a + controller that lives outside of Cluster API." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: + 'If referring to a piece of an + object instead of an entire object, this string + should contain a valid JSON/Go field access + statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to + a container within a pod, this would take + on a value like: "spec.containers{name}" (where + "name" refers to the name of the container + that triggered the event) or if no container + name is specified "spec.containers[2]" (container + with index 2 in this pod). This syntax is + chosen only to have some well-defined way + of referencing a part of an object. TODO: + this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: + "Kind of the referent. More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds" + type: string + name: + description: + "Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names" + type: string + namespace: + description: + "Namespace of the referent. More + info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" + type: string + resourceVersion: + description: + "Specific resourceVersion to which + this reference is made, if any. More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency" + type: string + uid: + description: + "UID of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids" + type: string + type: object + x-kubernetes-map-type: atomic + unhealthyConditions: + description: + UnhealthyConditions contains a list + of the conditions that determine whether a node + is considered unhealthy. The conditions are combined + in a logical OR, i.e. if any of the conditions + is met, the node is unhealthy. + items: + description: + UnhealthyCondition represents a Node + condition type and value with a timeout specified + as a duration. When the named condition has + been in the given status for at least the timeout + value, a node is considered unhealthy. + properties: + status: + minLength: 1 + type: string + timeout: + type: string + type: + minLength: 1 + type: string + required: + - status + - timeout + - type + type: object + type: array + unhealthyRange: + description: + 'Any further remediation is only allowed + if the number of machines selected by "selector" + as not healthy is within the range of "UnhealthyRange". + Takes precedence over MaxUnhealthy. Eg. "[3-5]" + - This means that remediation will be allowed + only when: (a) there are at least 3 unhealthy + machines (and) (b) there are at most 5 unhealthy + machines' + pattern: ^\[[0-9]+-[0-9]+\]$ + type: string + type: object + metadata: + description: + Metadata is the metadata applied to the + MachineDeployment and the machines of the MachineDeployment. + At runtime this metadata is merged with the corresponding + metadata from the ClusterClass. + properties: + annotations: + additionalProperties: + type: string + description: + "Annotations is an unstructured key + value map stored with a resource that may be set + by external tools to store and retrieve arbitrary + metadata. They are not queryable and should be + preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations" + type: object + labels: + additionalProperties: + type: string + description: + "Map of string keys and values that + can be used to organize and categorize (scope + and select) objects. May match selectors of replication + controllers and services. More info: http://kubernetes.io/docs/user-guide/labels" + type: object + type: object + minReadySeconds: + description: + Minimum number of seconds for which a newly + created machine should be ready. Defaults to 0 (machine + will be considered available as soon as it is ready) + format: int32 + type: integer + name: + description: + Name is the unique identifier for this + MachineDeploymentTopology. The value is used with + other unique identifiers to create a MachineDeployment's + Name (e.g. cluster's name, etc). In case the name + is greater than the allowed maximum length, the values + are hashed together. + type: string + nodeDeletionTimeout: + description: + NodeDeletionTimeout defines how long the + controller will attempt to delete the Node that the + Machine hosts after the Machine is marked for deletion. + A duration of 0 will retry deletion indefinitely. + Defaults to 10 seconds. + type: string + nodeDrainTimeout: + description: + "NodeDrainTimeout is the total amount of + time that the controller will spend on draining a + node. The default value is 0, meaning that the node + can be drained without any time limitations. NOTE: + NodeDrainTimeout is different from `kubectl drain + --timeout`" + type: string + nodeVolumeDetachTimeout: + description: + NodeVolumeDetachTimeout is the total amount + of time that the controller will spend on waiting + for all volumes to be detached. The default value + is 0, meaning that the volumes can be detached without + any time limitations. + type: string + replicas: + description: + Replicas is the number of worker nodes + belonging to this set. If the value is nil, the MachineDeployment + is created without the number of Replicas (defaulting + to 1) and it's assumed that an external entity (like + cluster autoscaler) is responsible for the management + of this value. + format: int32 + type: integer + strategy: + description: + The deployment strategy to use to replace + existing machines with new ones. + properties: + rollingUpdate: + description: + Rolling update config params. Present + only if MachineDeploymentStrategyType = RollingUpdate. + properties: + deletePolicy: + description: + DeletePolicy defines the policy + used by the MachineDeployment to identify + nodes to delete when downscaling. Valid values + are "Random, "Newest", "Oldest" When no value + is supplied, the default DeletePolicy of MachineSet + is used + enum: + - Random + - Newest + - Oldest + type: string + maxSurge: + anyOf: + - type: integer + - type: string + description: + "The maximum number of machines + that can be scheduled above the desired number + of machines. Value can be an absolute number + (ex: 5) or a percentage of desired machines + (ex: 10%). This can not be 0 if MaxUnavailable + is 0. Absolute number is calculated from percentage + by rounding up. Defaults to 1. Example: when + this is set to 30%, the new MachineSet can + be scaled up immediately when the rolling + update starts, such that the total number + of old and new machines do not exceed 130% + of desired machines. Once old machines have + been killed, new MachineSet can be scaled + up further, ensuring that total number of + machines running at any time during the update + is at most 130% of desired machines." + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: + "The maximum number of machines + that can be unavailable during the update. + Value can be an absolute number (ex: 5) or + a percentage of desired machines (ex: 10%). + Absolute number is calculated from percentage + by rounding down. This can not be 0 if MaxSurge + is 0. Defaults to 0. Example: when this is + set to 30%, the old MachineSet can be scaled + down to 70% of desired machines immediately + when the rolling update starts. Once new machines + are ready, old MachineSet can be scaled down + further, followed by scaling up the new MachineSet, + ensuring that the total number of machines + available at all times during the update is + at least 70% of desired machines." + x-kubernetes-int-or-string: true + type: object + type: + description: + Type of deployment. Allowed values + are RollingUpdate and OnDelete. The default is + RollingUpdate. + enum: + - RollingUpdate + - OnDelete + type: string + type: object + variables: + description: + Variables can be used to customize the + MachineDeployment through patches. + properties: + overrides: + description: + Overrides can be used to override Cluster + level variables. + items: + description: + ClusterVariable can be used to customize + the Cluster through patches. Each ClusterVariable + is associated with a Variable definition in + the ClusterClass `status` variables. + properties: + definitionFrom: + description: + "DefinitionFrom specifies where + the definition of this Variable is from. + DefinitionFrom is `inline` when the definition + is from the ClusterClass `.spec.variables` + or the name of a patch defined in the ClusterClass + `.spec.patches` where the patch is external + and provides external variables. This field + is mandatory if the variable has `DefinitionsConflict: + true` in ClusterClass `status.variables[]`" + type: string + name: + description: Name of the variable. + type: string + value: + description: + "Value of the variable. Note: + the value will be validated against the + schema of the corresponding ClusterClassVariable + from the ClusterClass. Note: We have to + use apiextensionsv1.JSON instead of a custom + JSON type, because controller-tools has + a hard-coded schema for apiextensionsv1.JSON + which cannot be produced by another type + via controller-tools, i.e. it is not possible + to have no type field. Ref: https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111" + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array + type: object + required: + - class + - name + type: object + type: array + machinePools: + description: + MachinePools is a list of machine pools in the + cluster. + items: + description: + MachinePoolTopology specifies the different + parameters for a pool of worker nodes in the topology. + This pool of nodes is managed by a MachinePool object + whose lifecycle is managed by the Cluster controller. + properties: + class: + description: + Class is the name of the MachinePoolClass + used to create the pool of worker nodes. This should + match one of the deployment classes defined in the + ClusterClass object mentioned in the `Cluster.Spec.Class` + field. + type: string + failureDomains: + description: + FailureDomains is the list of failure domains + the machine pool will be created in. Must match a + key in the FailureDomains map stored on the cluster + object. + items: + type: string + type: array + metadata: + description: + Metadata is the metadata applied to the + MachinePool. At runtime this metadata is merged with + the corresponding metadata from the ClusterClass. + properties: + annotations: + additionalProperties: + type: string + description: + "Annotations is an unstructured key + value map stored with a resource that may be set + by external tools to store and retrieve arbitrary + metadata. They are not queryable and should be + preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations" + type: object + labels: + additionalProperties: + type: string + description: + "Map of string keys and values that + can be used to organize and categorize (scope + and select) objects. May match selectors of replication + controllers and services. More info: http://kubernetes.io/docs/user-guide/labels" + type: object + type: object + minReadySeconds: + description: + Minimum number of seconds for which a newly + created machine pool should be ready. Defaults to + 0 (machine will be considered available as soon as + it is ready) + format: int32 + type: integer + name: + description: + Name is the unique identifier for this + MachinePoolTopology. The value is used with other + unique identifiers to create a MachinePool's Name + (e.g. cluster's name, etc). In case the name is greater + than the allowed maximum length, the values are hashed + together. + type: string + nodeDeletionTimeout: + description: + NodeDeletionTimeout defines how long the + controller will attempt to delete the Node that the + MachinePool hosts after the MachinePool is marked + for deletion. A duration of 0 will retry deletion + indefinitely. Defaults to 10 seconds. + type: string + nodeDrainTimeout: + description: + "NodeDrainTimeout is the total amount of + time that the controller will spend on draining a + node. The default value is 0, meaning that the node + can be drained without any time limitations. NOTE: + NodeDrainTimeout is different from `kubectl drain + --timeout`" + type: string + nodeVolumeDetachTimeout: + description: + NodeVolumeDetachTimeout is the total amount + of time that the controller will spend on waiting + for all volumes to be detached. The default value + is 0, meaning that the volumes can be detached without + any time limitations. + type: string + replicas: + description: + Replicas is the number of nodes belonging + to this pool. If the value is nil, the MachinePool + is created without the number of Replicas (defaulting + to 1) and it's assumed that an external entity (like + cluster autoscaler) is responsible for the management + of this value. + format: int32 + type: integer + variables: + description: + Variables can be used to customize the + MachinePool through patches. + properties: + overrides: + description: + Overrides can be used to override Cluster + level variables. + items: + description: + ClusterVariable can be used to customize + the Cluster through patches. Each ClusterVariable + is associated with a Variable definition in + the ClusterClass `status` variables. + properties: + definitionFrom: + description: + "DefinitionFrom specifies where + the definition of this Variable is from. + DefinitionFrom is `inline` when the definition + is from the ClusterClass `.spec.variables` + or the name of a patch defined in the ClusterClass + `.spec.patches` where the patch is external + and provides external variables. This field + is mandatory if the variable has `DefinitionsConflict: + true` in ClusterClass `status.variables[]`" + type: string + name: + description: Name of the variable. + type: string + value: + description: + "Value of the variable. Note: + the value will be validated against the + schema of the corresponding ClusterClassVariable + from the ClusterClass. Note: We have to + use apiextensionsv1.JSON instead of a custom + JSON type, because controller-tools has + a hard-coded schema for apiextensionsv1.JSON + which cannot be produced by another type + via controller-tools, i.e. it is not possible + to have no type field. Ref: https://github.com/kubernetes-sigs/controller-tools/blob/d0e03a142d0ecdd5491593e941ee1d6b5d91dba6/pkg/crd/known_types.go#L106-L111" + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array + type: object + required: + - class + - name + type: object + type: array + type: object + required: + - class + - version + type: object + type: object + status: + description: ClusterStatus defines the observed state of Cluster. + properties: + conditions: + description: Conditions defines current service state of the cluster. + items: + description: + Condition defines an observation of a Cluster API resource + operational state. + properties: + lastTransitionTime: + description: + Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: + A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: + The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: + Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: + Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + controlPlaneReady: + description: ControlPlaneReady defines if the control plane is ready. + type: boolean + failureDomains: + additionalProperties: + description: + FailureDomainSpec is the Schema for Cluster API failure + domains. It allows controllers to understand how many failure + domains a cluster can optionally span across. + properties: + attributes: + additionalProperties: + type: string + description: + Attributes is a free form map of attributes an + infrastructure provider might use or require. + type: object + controlPlane: + description: + ControlPlane determines if this failure domain + is suitable for use by control plane machines. + type: boolean + type: object + description: + FailureDomains is a slice of failure domain objects synced + from the infrastructure provider. + type: object + failureMessage: + description: + FailureMessage indicates that there is a fatal problem + reconciling the state, and will be set to a descriptive error message. + type: string + failureReason: + description: + FailureReason indicates that there is a fatal problem + reconciling the state, and will be set to a token value suitable + for programmatic interpretation. + type: string + infrastructureReady: + description: + InfrastructureReady is the state of the infrastructure + provider. + type: boolean + observedGeneration: + description: + ObservedGeneration is the latest generation observed + by the controller. + format: int64 + type: integer + phase: + description: + Phase represents the current phase of cluster actuation. + E.g. Pending, Running, Terminating, Failed etc. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/main.go b/main.go index 941db72..3c16703 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -37,6 +38,8 @@ import ( "github.com/weaveworks/cluster-reflector-controller/internal/controller" "github.com/weaveworks/cluster-reflector-controller/pkg/providers" "github.com/weaveworks/cluster-reflector-controller/pkg/providers/azure" + "github.com/weaveworks/cluster-reflector-controller/pkg/providers/capi" + capiclusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" //+kubebuilder:scaffold:imports ) @@ -51,6 +54,7 @@ func init() { utilruntime.Must(gitopsv1alpha1.AddToScheme(scheme)) utilruntime.Must(clustersv1alpha1.AddToScheme(scheme)) + utilruntime.Must(capiclusterv1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -110,6 +114,9 @@ func main() { AKSProvider: func(subscriptionID string) providers.Provider { return azure.NewAzureProvider(subscriptionID) }, + CAPIProvider: func(kubeclient client.Client, namespace string) providers.Provider { + return capi.NewCAPIProvider(kubeclient, namespace) + }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AutomatedClusterDiscovery") os.Exit(1) diff --git a/pkg/providers/capi/capi.go b/pkg/providers/capi/capi.go new file mode 100644 index 0000000..a25bf2e --- /dev/null +++ b/pkg/providers/capi/capi.go @@ -0,0 +1,53 @@ +package capi + +import ( + "context" + + "github.com/weaveworks/cluster-reflector-controller/pkg/providers" + capiclusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type CAPIProvider struct { + Kubeclient client.Client + Namespace string +} + +var _ providers.Provider = (*CAPIProvider)(nil) + +// NewCAPIProvider creates and returns a CAPIProvider ready for use +func NewCAPIProvider(client client.Client, namespace string) *CAPIProvider { + provider := &CAPIProvider{ + Kubeclient: client, + Namespace: namespace, + } + return provider +} + +func (p *CAPIProvider) ListClusters(ctx context.Context) ([]*providers.ProviderCluster, error) { + kubeClient := p.Kubeclient + capiClusters := &capiclusterv1.ClusterList{} + err := kubeClient.List(ctx, capiClusters, &client.ListOptions{Namespace: p.Namespace}) + if err != nil { + return nil, err + } + + clusters := []*providers.ProviderCluster{} + + for _, capiCluster := range capiClusters.Items { + clusters = append(clusters, &providers.ProviderCluster{ + Name: capiCluster.Name, + ID: string(capiCluster.GetObjectMeta().GetUID()), + KubeConfig: nil, + Labels: capiCluster.Labels, + }) + } + + return clusters, nil +} + +// ProviderCluster has an ID to identify the cluster, but capi cluster doesn't have a Cluster ID +// therefore wont't match in the case of CAPI +func (p *CAPIProvider) ClusterID(ctx context.Context, kubeClient client.Reader) (string, error) { + return "", nil +} diff --git a/pkg/providers/capi/capi_test.go b/pkg/providers/capi/capi_test.go new file mode 100644 index 0000000..77e5374 --- /dev/null +++ b/pkg/providers/capi/capi_test.go @@ -0,0 +1,48 @@ +package capi + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + capiclusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/weaveworks/cluster-reflector-controller/pkg/providers" +) + +func TestClusterProvider_ListClusters(t *testing.T) { + scheme := runtime.NewScheme() + assert.NoError(t, capiclusterv1.AddToScheme(scheme)) + + // create clusters to list (capi) + clusters := []client.Object{ + &capiclusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-1", + Namespace: "default", + }, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(clusters...).Build() + provider := NewCAPIProvider(client, "default") + + provided, err := provider.ListClusters(context.TODO()) + if err != nil { + t.Fatal(err) + } + + expected := []*providers.ProviderCluster{ + { + Name: "cluster-1", + KubeConfig: nil, + }, + } + + assert.Equal(t, expected, provided, "expected clusters to be equal") + +}