mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
feat(dashboard): dashboard api [EE-7111] (#11843)
This commit is contained in:
parent
659abe553d
commit
9cef912c44
11 changed files with 695 additions and 69 deletions
154
api/kubernetes/cli/applications.go
Normal file
154
api/kubernetes/cli/applications.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GetApplications gets a list of kubernetes workloads (or applications) by kind. If Kind is not specified, gets the all
|
||||
func (kcl *KubeClient) GetApplications(namespace, kind string) ([]models.K8sApplication, error) {
|
||||
applicationList := []models.K8sApplication{}
|
||||
listOpts := metav1.ListOptions{}
|
||||
|
||||
if kind == "" || strings.EqualFold(kind, "deployment") {
|
||||
deployments, err := kcl.cli.AppsV1().Deployments(namespace).List(context.TODO(), listOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range deployments.Items {
|
||||
applicationList = append(applicationList, models.K8sApplication{
|
||||
UID: string(d.UID),
|
||||
Name: d.Name,
|
||||
Namespace: d.Namespace,
|
||||
Kind: "Deployment",
|
||||
Labels: d.Labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "" || strings.EqualFold(kind, "statefulset") {
|
||||
statefulSets, err := kcl.cli.AppsV1().StatefulSets(namespace).List(context.TODO(), listOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, s := range statefulSets.Items {
|
||||
applicationList = append(applicationList, models.K8sApplication{
|
||||
UID: string(s.UID),
|
||||
Name: s.Name,
|
||||
Namespace: s.Namespace,
|
||||
Kind: "StatefulSet",
|
||||
Labels: s.Labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "" || strings.EqualFold(kind, "daemonset") {
|
||||
daemonSets, err := kcl.cli.AppsV1().DaemonSets(namespace).List(context.TODO(), listOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, d := range daemonSets.Items {
|
||||
applicationList = append(applicationList, models.K8sApplication{
|
||||
UID: string(d.UID),
|
||||
Name: d.Name,
|
||||
Namespace: d.Namespace,
|
||||
Kind: "DaemonSet",
|
||||
Labels: d.Labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "" || strings.EqualFold(kind, "nakedpods") {
|
||||
pods, _ := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
for _, pod := range pods.Items {
|
||||
naked := false
|
||||
if len(pod.OwnerReferences) == 0 {
|
||||
naked = true
|
||||
} else {
|
||||
managed := false
|
||||
loop:
|
||||
for _, ownerRef := range pod.OwnerReferences {
|
||||
switch ownerRef.Kind {
|
||||
case "Deployment", "DaemonSet", "ReplicaSet":
|
||||
managed = true
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if !managed {
|
||||
naked = true
|
||||
}
|
||||
}
|
||||
|
||||
if naked {
|
||||
applicationList = append(applicationList, models.K8sApplication{
|
||||
UID: string(pod.UID),
|
||||
Name: pod.Name,
|
||||
Namespace: pod.Namespace,
|
||||
Kind: "Pod",
|
||||
Labels: pod.Labels,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return applicationList, nil
|
||||
}
|
||||
|
||||
// GetApplication gets a kubernetes workload (application) by kind and name. If Kind is not specified, gets the all
|
||||
func (kcl *KubeClient) GetApplication(namespace, kind, name string) (models.K8sApplication, error) {
|
||||
|
||||
opts := metav1.GetOptions{}
|
||||
|
||||
switch strings.ToLower(kind) {
|
||||
case "deployment":
|
||||
d, err := kcl.cli.AppsV1().Deployments(namespace).Get(context.TODO(), name, opts)
|
||||
if err != nil {
|
||||
return models.K8sApplication{}, err
|
||||
}
|
||||
|
||||
return models.K8sApplication{
|
||||
UID: string(d.UID),
|
||||
Name: d.Name,
|
||||
Namespace: d.Namespace,
|
||||
Kind: "Deployment",
|
||||
Labels: d.Labels,
|
||||
}, nil
|
||||
|
||||
case "statefulset":
|
||||
s, err := kcl.cli.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, opts)
|
||||
if err != nil {
|
||||
return models.K8sApplication{}, err
|
||||
}
|
||||
|
||||
return models.K8sApplication{
|
||||
UID: string(s.UID),
|
||||
Name: s.Name,
|
||||
Namespace: s.Namespace,
|
||||
Kind: "StatefulSet",
|
||||
Labels: s.Labels,
|
||||
}, nil
|
||||
|
||||
case "daemonset":
|
||||
d, err := kcl.cli.AppsV1().DaemonSets(namespace).Get(context.TODO(), name, opts)
|
||||
if err != nil {
|
||||
return models.K8sApplication{}, err
|
||||
}
|
||||
|
||||
return models.K8sApplication{
|
||||
UID: string(d.UID),
|
||||
Name: d.Name,
|
||||
Namespace: d.Namespace,
|
||||
Kind: "DaemonSet",
|
||||
Labels: d.Labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return models.K8sApplication{}, nil
|
||||
}
|
|
@ -25,6 +25,8 @@ const (
|
|||
DefaultKubeClientBurst = 100
|
||||
)
|
||||
|
||||
const maxConcurrency = 30
|
||||
|
||||
type (
|
||||
// ClientFactory is used to create Kubernetes clients
|
||||
ClientFactory struct {
|
||||
|
@ -46,6 +48,13 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func NewKubeClientFromClientset(cli *kubernetes.Clientset) *KubeClient {
|
||||
return &KubeClient{
|
||||
cli: cli,
|
||||
instanceID: "",
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientFactory returns a new instance of a ClientFactory
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*ClientFactory, error) {
|
||||
if userSessionTimeout == "" {
|
||||
|
|
258
api/kubernetes/cli/dashboard.go
Normal file
258
api/kubernetes/cli/dashboard.go
Normal file
|
@ -0,0 +1,258 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/concurrent"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func (kcl *KubeClient) GetDashboard() (models.K8sDashboard, error) {
|
||||
dashboardData := models.K8sDashboard{}
|
||||
|
||||
// Get a list of all the namespaces first
|
||||
namespaces, err := kcl.cli.CoreV1().Namespaces().List(context.TODO(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
return dashboardData, err
|
||||
}
|
||||
|
||||
getNamespaceCounts := func(namespace string) concurrent.Func {
|
||||
return func(ctx context.Context) (any, error) {
|
||||
data := models.K8sDashboard{}
|
||||
|
||||
// apps (deployments, statefulsets, daemonsets)
|
||||
applicationCount, err := getApplicationsCount(ctx, kcl, namespace)
|
||||
if err != nil {
|
||||
// skip namespaces we're not allowed access to. But don't return an error so that we
|
||||
// can still count the other namespaces. Returning an error here will stop concurrent.Run
|
||||
if errors.IsForbidden(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
data.ApplicationsCount = applicationCount
|
||||
|
||||
// services
|
||||
serviceCount, err := getServicesCount(ctx, kcl, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.ServicesCount = serviceCount
|
||||
|
||||
// ingresses
|
||||
ingressesCount, err := getIngressesCount(ctx, kcl, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.IngressesCount = ingressesCount
|
||||
|
||||
// configmaps
|
||||
configMapCount, err := getConfigMapsCount(ctx, kcl, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.ConfigMapsCount = configMapCount
|
||||
|
||||
// secrets
|
||||
secretsCount, err := getSecretsCount(ctx, kcl, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.SecretsCount = secretsCount
|
||||
|
||||
// volumes
|
||||
volumesCount, err := getVolumesCount(ctx, kcl, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data.VolumesCount = volumesCount
|
||||
|
||||
// count this namespace for the user
|
||||
data.NamespacesCount = 1
|
||||
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
dashboardTasks := make([]concurrent.Func, 0)
|
||||
for _, ns := range namespaces.Items {
|
||||
dashboardTasks = append(dashboardTasks, getNamespaceCounts(ns.Name))
|
||||
}
|
||||
|
||||
// Fetch all the data for each namespace concurrently
|
||||
results, err := concurrent.Run(context.TODO(), maxConcurrency, dashboardTasks...)
|
||||
if err != nil {
|
||||
return dashboardData, err
|
||||
}
|
||||
|
||||
// Sum up the results
|
||||
for i := range results {
|
||||
data, _ := results[i].Result.(models.K8sDashboard)
|
||||
dashboardData.NamespacesCount += data.NamespacesCount
|
||||
dashboardData.ApplicationsCount += data.ApplicationsCount
|
||||
dashboardData.ServicesCount += data.ServicesCount
|
||||
dashboardData.IngressesCount += data.IngressesCount
|
||||
dashboardData.ConfigMapsCount += data.ConfigMapsCount
|
||||
dashboardData.SecretsCount += data.SecretsCount
|
||||
dashboardData.VolumesCount += data.VolumesCount
|
||||
}
|
||||
|
||||
return dashboardData, nil
|
||||
}
|
||||
|
||||
// Get applications excluding nakedpods
|
||||
func getApplicationsCount(ctx context.Context, kcl *KubeClient, namespace string) (int64, error) {
|
||||
options := v1.ListOptions{Limit: 1}
|
||||
count := int64(0)
|
||||
|
||||
// deployments
|
||||
deployments, err := kcl.cli.AppsV1().Deployments(namespace).List(ctx, options)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(deployments.Items) > 0 {
|
||||
count = 1 // first deployment
|
||||
remainingItemsCount := deployments.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining deployments if any
|
||||
}
|
||||
}
|
||||
|
||||
// StatefulSets
|
||||
statefulSets, err := kcl.cli.AppsV1().StatefulSets(namespace).List(ctx, options)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(statefulSets.Items) > 0 {
|
||||
count += 1 // + first statefulset
|
||||
remainingItemsCount := statefulSets.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining statefulsets if any
|
||||
}
|
||||
}
|
||||
|
||||
// Daemonsets
|
||||
daemonsets, err := kcl.cli.AppsV1().DaemonSets(namespace).List(ctx, options)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(daemonsets.Items) > 0 {
|
||||
count += 1 // + first daemonset
|
||||
remainingItemsCount := daemonsets.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining daemonsets if any
|
||||
}
|
||||
}
|
||||
|
||||
// + (naked pods)
|
||||
nakedPods, err := kcl.GetApplications(namespace, "nakedpods")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count + int64(len(nakedPods)), nil
|
||||
}
|
||||
|
||||
// Get the total count of services for the given namespace
|
||||
func getServicesCount(ctx context.Context, kcl *KubeClient, namespace string) (int64, error) {
|
||||
options := v1.ListOptions{
|
||||
Limit: 1,
|
||||
}
|
||||
var count int64 = 0
|
||||
services, err := kcl.cli.CoreV1().Services(namespace).List(ctx, options)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(services.Items) > 0 {
|
||||
count = 1 // first service
|
||||
remainingItemsCount := services.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining services if any
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Get the total count of ingresses for the given namespace
|
||||
func getIngressesCount(ctx context.Context, kcl *KubeClient, namespace string) (int64, error) {
|
||||
ingresses, err := kcl.cli.NetworkingV1().Ingresses(namespace).List(ctx, v1.ListOptions{Limit: 1})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := int64(0)
|
||||
if len(ingresses.Items) > 0 {
|
||||
count = 1 // first ingress
|
||||
remainingItemsCount := ingresses.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining ingresses if any
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Get the total count of configMaps for the given namespace
|
||||
func getConfigMapsCount(ctx context.Context, kcl *KubeClient, namespace string) (int64, error) {
|
||||
configMaps, err := kcl.cli.CoreV1().ConfigMaps(namespace).List(ctx, v1.ListOptions{Limit: 1})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := int64(0)
|
||||
if len(configMaps.Items) > 0 {
|
||||
count = 1 // first configmap
|
||||
remainingItemsCount := configMaps.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining configmaps if any
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Get the total count of secrets for the given namespace
|
||||
func getSecretsCount(ctx context.Context, kcl *KubeClient, namespace string) (int64, error) {
|
||||
secrets, err := kcl.cli.CoreV1().Secrets(namespace).List(ctx, v1.ListOptions{Limit: 1})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := int64(0)
|
||||
if len(secrets.Items) > 0 {
|
||||
count = 1 // first secret
|
||||
remainingItemsCount := secrets.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining secrets if any
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Get the total count of volumes for the given namespace
|
||||
func getVolumesCount(ctx context.Context, kcl *KubeClient, namespace string) (int64, error) {
|
||||
volumes, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).List(ctx, v1.ListOptions{Limit: 1})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := int64(0)
|
||||
if len(volumes.Items) > 0 {
|
||||
count = 1 // first volume
|
||||
remainingItemsCount := volumes.GetRemainingItemCount()
|
||||
if remainingItemsCount != nil {
|
||||
count += *remainingItemsCount // add the remaining volumes if any
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue