Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): dashboard api [EE-7111] #11844

Merged
merged 4 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions api/http/handler/kubernetes/dashboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kubernetes

import (
"net/http"

httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
)

// @id GetKubernetesDashboard
// @summary Get the dashboard summary data
// @description Get the dashboard summary data which is simply a count of a range of different commonly used kubernetes resources
// @description **Access policy**: authenticated
// @tags kubernetes
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment (Endpoint) identifier"
// @success 200 {array} kubernetes.K8sDashboard "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /kubernetes/{id}/dashboard [get]
func (handler *Handler) getKubernetesDashboard(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {

cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
return httpErr
}

dashboard, err := cli.GetDashboard()
if err != nil {
return httperror.InternalServerError("Unable to retrieve dashboard data", err)
}

return response.JSON(w, dashboard)
}
1 change: 1 addition & 0 deletions api/http/handler/kubernetes/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Use(kubeOnlyMiddleware)
endpointRouter.Use(h.kubeClientMiddleware)

endpointRouter.Handle("/dashboard", httperror.LoggerHandler(h.getKubernetesDashboard)).Methods(http.MethodGet)
endpointRouter.Handle("/nodes_limits", httperror.LoggerHandler(h.getKubernetesNodesLimits)).Methods(http.MethodGet)
endpointRouter.Handle("/max_resource_limits", httperror.LoggerHandler(h.getKubernetesMaxResourceLimits)).Methods(http.MethodGet)
endpointRouter.Handle("/metrics/nodes", httperror.LoggerHandler(h.getKubernetesMetricsForAllNodes)).Methods(http.MethodGet)
Expand Down
13 changes: 13 additions & 0 deletions api/http/models/kubernetes/dashboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package kubernetes

type (
K8sDashboard struct {
NamespacesCount int64 `json:"namespacesCount"`
ApplicationsCount int64 `json:"applicationsCount"`
ServicesCount int64 `json:"servicesCount"`
IngressesCount int64 `json:"ingressesCount"`
ConfigMapsCount int64 `json:"configMapsCount"`
SecretsCount int64 `json:"secretsCount"`
VolumesCount int64 `json:"volumesCount"`
}
)
144 changes: 144 additions & 0 deletions api/internal/concurrent/concurrent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Package concurrent provides utilities for running multiple functions concurrently in Go.
// For example, many kubernetes calls can take a while to fulfill. Oftentimes in Portainer
// we need to get a list of objects from multiple kubernetes REST APIs. We can often call these
// apis concurrently to speed up the response time.
// This package provides a clean way to do just that.
//
// Examples:
// The ConfigMaps and Secrets function converted using concurrent.Run.
/*

// GetConfigMapsAndSecrets gets all the ConfigMaps AND all the Secrets for a
// given namespace in a k8s endpoint. The result is a list of both config maps
// and secrets. The IsSecret boolean property indicates if a given struct is a
// secret or configmap.
func (kcl *KubeClient) GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) {
prabhat83 marked this conversation as resolved.
Show resolved Hide resolved

// use closures to capture the current kube client and namespace by declaring wrapper functions
// that match the interface signature for concurrent.Func

listConfigMaps := func(ctx context.Context) (interface{}, error) {
return kcl.cli.CoreV1().ConfigMaps(namespace).List(context.Background(), meta.ListOptions{})
}

listSecrets := func(ctx context.Context) (interface{}, error) {
return kcl.cli.CoreV1().Secrets(namespace).List(context.Background(), meta.ListOptions{})
}

// run the functions concurrently and wait for results. We can also pass in a context to cancel.
// e.g. Deadline timer.
results, err := concurrent.Run(context.TODO(), listConfigMaps, listSecrets)
if err != nil {
return nil, err
}

var configMapList *core.ConfigMapList
var secretList *core.SecretList
for _, r := range results {
switch v := r.Result.(type) {
case *core.ConfigMapList:
configMapList = v
case *core.SecretList:
secretList = v
}
}

// TODO: Applications
var combined []models.K8sConfigMapOrSecret
for _, m := range configMapList.Items {
var cm models.K8sConfigMapOrSecret
cm.UID = string(m.UID)
cm.Name = m.Name
cm.Namespace = m.Namespace
cm.Annotations = m.Annotations
cm.Data = m.Data
cm.CreationDate = m.CreationTimestamp.Time.UTC().Format(time.RFC3339)
combined = append(combined, cm)
}

for _, s := range secretList.Items {
var secret models.K8sConfigMapOrSecret
secret.UID = string(s.UID)
secret.Name = s.Name
secret.Namespace = s.Namespace
secret.Annotations = s.Annotations
secret.Data = msbToMss(s.Data)
secret.CreationDate = s.CreationTimestamp.Time.UTC().Format(time.RFC3339)
secret.IsSecret = true
secret.SecretType = string(s.Type)
combined = append(combined, secret)
}

return combined, nil
}

*/

package concurrent

import (
"context"
"sync"
)

// Result contains the result and any error returned from running a client task function
type Result struct {
Result any // the result of running the task function
Err error // any error that occurred while running the task function
}

// Func is a function returns a result or error
type Func func(ctx context.Context) (any, error)

// Run runs a list of functions returns the results
func Run(ctx context.Context, maxConcurrency int, tasks ...Func) ([]Result, error) {
var wg sync.WaitGroup
resultsChan := make(chan Result, len(tasks))
taskChan := make(chan Func, len(tasks))

localCtx, cancelCtx := context.WithCancel(ctx)
defer cancelCtx()

runTask := func() {
defer wg.Done()
for fn := range taskChan {
result, err := fn(localCtx)
resultsChan <- Result{Result: result, Err: err}
}
}

// Set maxConcurrency to the number of tasks if zero or negative
if maxConcurrency <= 0 {
maxConcurrency = len(tasks)
}

// Start worker goroutines
for i := 0; i < maxConcurrency; i++ {
wg.Add(1)
go runTask()
}

// Add tasks to the task channel
for _, fn := range tasks {
taskChan <- fn
}

// Close the task channel to signal workers to stop when all tasks are done
close(taskChan)

// Wait for all workers to complete
wg.Wait()
close(resultsChan)

// Collect the results and cancel on error
results := make([]Result, 0, len(tasks))
for r := range resultsChan {
if r.Err != nil {
cancelCtx()
return nil, r.Err
}
results = append(results, r)
}

return results, nil
}
154 changes: 154 additions & 0 deletions api/kubernetes/cli/applications.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions api/kubernetes/cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const (
DefaultKubeClientBurst = 100
)

const maxConcurrency = 30

type (
// ClientFactory is used to create Kubernetes clients
ClientFactory struct {
Expand Down
Loading
Loading