diff --git a/go.mod b/go.mod index 7599a420..48ad92ec 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( k8s.io/api v0.30.5 k8s.io/apiextensions-apiserver v0.30.5 k8s.io/apimachinery v0.30.5 + k8s.io/cli-runtime v0.30.5 k8s.io/client-go v0.30.5 k8s.io/klog/v2 v2.120.1 sigs.k8s.io/controller-runtime v0.18.5 @@ -26,6 +27,7 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect @@ -69,6 +71,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 0c1294d1..67830686 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/cnoe-io/argocd-api v0.0.0-20241031202925-3091d64cb3c4/go.mod h1:qItVg github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -141,6 +143,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -284,6 +288,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -357,6 +362,8 @@ k8s.io/apiextensions-apiserver v0.30.5 h1:JfXTIyzXf5+ryncbp7T/uaVjLdvkwtqoNG2vo7 k8s.io/apiextensions-apiserver v0.30.5/go.mod h1:uVLEME2UPA6UN22i+jTu66B9/0CnsjlHkId+Awo0lvs= k8s.io/apimachinery v0.30.5 h1:CQZO19GFgw4zcOjY2H+mJ3k1u1o7zFACTNCB7nu4O18= k8s.io/apimachinery v0.30.5/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/cli-runtime v0.30.5 h1:MWY6efoBVH3h0O6p2DgaQszabV5ZntHZwTHBkiz+PSI= +k8s.io/cli-runtime v0.30.5/go.mod h1:AKMWLDIJQUA5a7yEh5gmzkhpZqYpuDEVovanugfSnQk= k8s.io/client-go v0.30.5 h1:vEDSzfTz0F8TXcWVdXl+aqV7NAV8M3UvC2qnGTTCoKw= k8s.io/client-go v0.30.5/go.mod h1:/q5fHHBmhAUesOOFJACpD7VJ4e57rVtTPDOsvXrPpMk= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= diff --git a/pkg/cmd/get/clusters.go b/pkg/cmd/get/clusters.go index ba261f9d..6c3bac36 100644 --- a/pkg/cmd/get/clusters.go +++ b/pkg/cmd/get/clusters.go @@ -1,13 +1,61 @@ package get import ( + "bytes" + "context" "fmt" - + "github.com/cnoe-io/idpbuilder/api/v1alpha1" "github.com/cnoe-io/idpbuilder/pkg/cmd/helpers" + "github.com/cnoe-io/idpbuilder/pkg/k8s" + "github.com/cnoe-io/idpbuilder/pkg/kind" + "github.com/cnoe-io/idpbuilder/pkg/util" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/kind/pkg/cluster" + "slices" + "strings" ) +// ClusterManager holds the clients for the different idpbuilder clusters +type ClusterManager struct { + clients map[string]client.Client // map of cluster name to client +} + +type Cluster struct { + Name string + URLKubeApi string + KubePort int32 + TlsCheck bool + ExternalPort int32 + Nodes []Node +} + +type Node struct { + Name string + InternalIP string + ExternalIP string + Capacity Capacity + Allocated Allocated +} + +type Capacity struct { + Memory float64 + Pods int64 + Cpu int64 +} + +type Allocated struct { + Cpu string + Memory string +} + var ClustersCmd = &cobra.Command{ Use: "clusters", Short: "Get idp clusters", @@ -22,14 +70,313 @@ func preClustersE(cmd *cobra.Command, args []string) error { } func list(cmd *cobra.Command, args []string) error { - provider := cluster.NewProvider(cluster.ProviderWithDocker()) + clusters, err := populateClusterList() + if err != nil { + return err + } else { + // Convert the list of the clusters to Table of clusters + printTable(printers.PrintOptions{}, generateClusterTable(clusters)) + return nil + } +} + +func populateClusterList() ([]Cluster, error) { + logger := helpers.CmdLogger + + detectOpt, err := util.DetectKindNodeProvider() + if err != nil { + return nil, err + } + + kubeConfig, err := helpers.GetKubeConfig() + if err != nil { + return nil, err + } + + // TODO: Check if we need it or not like also if the new code handle the kubeconfig path passed as parameter + _, err = helpers.GetKubeClient(kubeConfig) + if err != nil { + return nil, err + } + + config, err := helpers.LoadKubeConfig() + if err != nil { + //logger.Error(err, "failed to load the kube config.") + return nil, err + } + + // Create an empty array of clusters to collect the information + clusterList := []Cluster{} + + // List the idp builder clusters according to the provider: podman or docker + provider := cluster.NewProvider(cluster.ProviderWithLogger(kind.KindLoggerFromLogr(&logger)), detectOpt) clusters, err := provider.List() if err != nil { - return fmt.Errorf("failed to list clusters: %w", err) + return nil, err + } + + // Populate a list of Kube client for each cluster/context matching an idpbuilder cluster + manager, err := CreateKubeClientForEachIDPCluster(config, clusters) + if err != nil { + return nil, err + } + + for _, cluster := range clusters { + aCluster := Cluster{Name: cluster} + + // Search about the idp cluster within the kubeconfig file and show information + c, found := findClusterByName(config, "kind-"+cluster) + if !found { + logger.Info(fmt.Sprintf("Cluster not found: %s within kube config file\n", cluster)) + } else { + cli, err := GetClientForCluster(manager, cluster) + if err != nil { + return nil, err + } + logger.V(1).Info(fmt.Sprintf("Got the context for the cluster: %s.", cluster)) + + // Print the external port mounted on the container and available also as ingress host port + targetPort, err := findExternalHTTPSPort(cli, cluster) + if err != nil { + return nil, err + } else { + aCluster.ExternalPort = targetPort + } + + aCluster.URLKubeApi = c.Server + aCluster.TlsCheck = c.InsecureSkipTLSVerify + + // Print the internal port running the Kube API service + kubeApiPort, err := findInternalKubeApiPort(cli) + if err != nil { + return nil, err + } else { + aCluster.KubePort = kubeApiPort + } + + // Let's check what the current node reports + var nodeList corev1.NodeList + err = cli.List(context.TODO(), &nodeList) + if err != nil { + return nil, err + } + + for _, node := range nodeList.Items { + nodeName := node.Name + + aNode := Node{} + aNode.Name = nodeName + + for _, addr := range node.Status.Addresses { + switch addr.Type { + case corev1.NodeInternalIP: + aNode.InternalIP = addr.Address + case corev1.NodeExternalIP: + aNode.ExternalIP = addr.Address + } + } + + // Get Node capacity + resources := node.Status.Capacity + + memory := resources[corev1.ResourceMemory] + cpu := resources[corev1.ResourceCPU] + pods := resources[corev1.ResourcePods] + + aNode.Capacity = Capacity{ + Memory: float64(memory.Value()) / (1024 * 1024 * 1024), + Cpu: cpu.Value(), + Pods: pods.Value(), + } + + // Get Node Allocated resources + allocated, err := printAllocatedResources(context.Background(), cli, node.Name) + if err != nil { + return nil, err + } + aNode.Allocated = allocated + + aCluster.Nodes = append(aCluster.Nodes, aNode) + } + + } + clusterList = append(clusterList, aCluster) + } + + return clusterList, nil +} + +func generateClusterTable(clusterTable []Cluster) metav1.Table { + table := &metav1.Table{} + table.ColumnDefinitions = []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string"}, + {Name: "External-Port", Type: "string"}, + {Name: "Kube-Api", Type: "string"}, + {Name: "TLS", Type: "string"}, + {Name: "Kube-Port", Type: "string"}, + {Name: "Nodes", Type: "string"}, + } + for _, cluster := range clusterTable { + row := metav1.TableRow{ + Cells: []interface{}{ + cluster.Name, + cluster.ExternalPort, + cluster.URLKubeApi, + cluster.TlsCheck, + cluster.KubePort, + generateNodeData(cluster.Nodes), + }, + } + table.Rows = append(table.Rows, row) + } + return *table +} + +func printTable(opts printers.PrintOptions, table metav1.Table) { + logger := helpers.CmdLogger + out := bytes.NewBuffer([]byte{}) + printer := printers.NewTablePrinter(opts) + err := printer.PrintObj(&table, out) + if err != nil { + logger.Error(err, "failed to print the table.") + return + } + fmt.Println(out.String()) +} + +func generateNodeData(nodes []Node) string { + var result string + for i, aNode := range nodes { + result += aNode.Name + if i < len(nodes)-1 { + result += "," + } + } + return result +} + +func printAllocatedResources(ctx context.Context, k8sClient client.Client, nodeName string) (Allocated, error) { + // List all pods on the specified node + var podList corev1.PodList + if err := k8sClient.List(ctx, &podList, client.MatchingFields{"spec.nodeName": nodeName}); err != nil { + return Allocated{}, fmt.Errorf("failed to list pods on node %s.", nodeName) + } + + // Initialize counters for CPU and memory requests + totalCPU := resource.NewQuantity(0, resource.DecimalSI) + totalMemory := resource.NewQuantity(0, resource.BinarySI) + + // Sum up CPU and memory requests from each container in each pod + for _, pod := range podList.Items { + for _, container := range pod.Spec.Containers { + if reqCPU, found := container.Resources.Requests[corev1.ResourceCPU]; found { + totalCPU.Add(reqCPU) + } + if reqMemory, found := container.Resources.Requests[corev1.ResourceMemory]; found { + totalMemory.Add(reqMemory) + } + } + } + + allocated := Allocated{ + Memory: totalMemory.String(), + Cpu: totalCPU.String(), + } + + return allocated, nil +} + +func findExternalHTTPSPort(cli client.Client, clusterName string) (int32, error) { + service := corev1.Service{} + namespacedName := types.NamespacedName{ + Name: "ingress-nginx-controller", + Namespace: "ingress-nginx", + } + err := cli.Get(context.TODO(), namespacedName, &service) + if err != nil { + return 0, fmt.Errorf("failed to get the ingress service on the cluster. %w", err) } - for _, c := range clusters { - fmt.Println(c) + localBuild := v1alpha1.Localbuild{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + }, + } + err = cli.Get(context.TODO(), client.ObjectKeyFromObject(&localBuild), &localBuild) + if err != nil { + return 0, fmt.Errorf("failed to get the localbuild on the cluster. %w", err) + } + + var targetPort corev1.ServicePort + protocol := localBuild.Spec.BuildCustomization.Protocol + "-" + for _, port := range service.Spec.Ports { + if port.Name != "" && strings.HasPrefix(port.Name, protocol) { + targetPort = port + break + } + } + return targetPort.Port, nil +} + +func findInternalKubeApiPort(cli client.Client) (int32, error) { + service := corev1.Service{} + namespacedName := types.NamespacedName{ + Name: "kubernetes", + Namespace: "default", + } + err := cli.Get(context.TODO(), namespacedName, &service) + if err != nil { + return 0, fmt.Errorf("failed to get the kubernetes default service on the cluster. %w", err) + } + + var targetPort corev1.ServicePort + for _, port := range service.Spec.Ports { + if port.Name != "" && strings.HasPrefix(port.Name, "https") { + targetPort = port + break + } + } + return targetPort.TargetPort.IntVal, nil +} + +// findClusterByName searches for a cluster by name in the kubeconfig +func findClusterByName(config *api.Config, name string) (*api.Cluster, bool) { + cluster, exists := config.Clusters[name] + return cluster, exists +} + +// GetClientForCluster returns the client for the specified cluster/context name +func GetClientForCluster(m *ClusterManager, clusterName string) (client.Client, error) { + cl, exists := m.clients["kind-"+clusterName] + if !exists { + return nil, fmt.Errorf("no client found for cluster %q", clusterName) + } + return cl, nil +} + +func CreateKubeClientForEachIDPCluster(config *api.Config, clusterList []string) (*ClusterManager, error) { + // Initialize the ClusterManager with a map of kube Client + manager := &ClusterManager{ + clients: make(map[string]client.Client), + } + + for contextName := range config.Contexts { + // Check if the kubconfig contains the cluster name + // We remove the prefix "kind-" to find the cluster name from the slice + if slices.Contains(clusterList, contextName[5:]) { + cfg, err := clientcmd.NewNonInteractiveClientConfig(*config, contextName, &clientcmd.ConfigOverrides{}, nil).ClientConfig() + if err != nil { + return nil, fmt.Errorf("Failed to build client for context %s.", contextName) + } + + cl, err := client.New(cfg, client.Options{Scheme: k8s.GetScheme()}) + if err != nil { + return nil, fmt.Errorf("failed to create client for context %s", contextName) + } + + manager.clients[contextName] = cl + } + } - return nil + return manager, nil } diff --git a/pkg/cmd/get/root.go b/pkg/cmd/get/root.go index 1e329010..c767b8c4 100644 --- a/pkg/cmd/get/root.go +++ b/pkg/cmd/get/root.go @@ -2,7 +2,7 @@ package get import ( "fmt" - + "github.com/cnoe-io/idpbuilder/pkg/cmd/helpers" "github.com/spf13/cobra" ) @@ -23,6 +23,7 @@ func init() { GetCmd.AddCommand(SecretsCmd) GetCmd.PersistentFlags().StringSliceVarP(&packages, "packages", "p", []string{}, "names of packages.") GetCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "", "Output format. json or yaml.") + GetCmd.PersistentFlags().StringVarP(&helpers.KubeConfigPath, "kubeconfig", "", "", "kube config file Path.") } func exportE(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/helpers/k8s.go b/pkg/cmd/helpers/k8s.go new file mode 100644 index 00000000..f371b865 --- /dev/null +++ b/pkg/cmd/helpers/k8s.go @@ -0,0 +1,50 @@ +package helpers + +import ( + "fmt" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/util/homedir" + "path/filepath" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + KubeConfigPath string + scheme *runtime.Scheme +) + +func GetKubeConfigPath() string { + if KubeConfigPath == "" { + return filepath.Join(homedir.HomeDir(), ".kube", "config") + } else { + return KubeConfigPath + } +} + +func LoadKubeConfig() (*api.Config, error) { + config, err := clientcmd.LoadFromFile(GetKubeConfigPath()) + if err != nil { + return nil, fmt.Errorf("Failed to load kubeconfig file: %w", err) + } else { + return config, nil + } +} + +func GetKubeConfig() (*rest.Config, error) { + kubeConfig, err := clientcmd.BuildConfigFromFlags("", GetKubeConfigPath()) + if err != nil { + return nil, fmt.Errorf("Error building kubeconfig: %w", err) + } + return kubeConfig, nil +} + +func GetKubeClient(kubeConfig *rest.Config) (client.Client, error) { + kubeClient, err := client.New(kubeConfig, client.Options{Scheme: scheme}) + if err != nil { + return nil, fmt.Errorf("Error creating kubernetes client: %w", err) + } + return kubeClient, nil +}