mirror of https://github.com/portainer/portainer
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
259 lines
10 KiB
259 lines
10 KiB
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
|
"github.com/rs/zerolog/log"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
storagev1 "k8s.io/api/storage/v1"
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
// GetVolumes gets the volumes in the current k8s environment(endpoint).
|
|
// If the user is an admin, it fetches all the volumes in the cluster.
|
|
// If the user is not an admin, it fetches the volumes in the namespaces the user has access to.
|
|
// It returns a list of K8sVolumeInfo.
|
|
func (kcl *KubeClient) GetVolumes(namespace string) ([]models.K8sVolumeInfo, error) {
|
|
if kcl.IsKubeAdmin {
|
|
return kcl.fetchVolumes(namespace)
|
|
}
|
|
return kcl.fetchVolumesForNonAdmin(namespace)
|
|
}
|
|
|
|
// GetVolume gets the volume with the given name and namespace.
|
|
func (kcl *KubeClient) GetVolume(namespace, volumeName string) (*models.K8sVolumeInfo, error) {
|
|
persistentVolumeClaim, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).Get(context.TODO(), volumeName, metav1.GetOptions{})
|
|
if err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
persistentVolumesMap, storageClassesMap, err := kcl.fetchPersistentVolumesAndStorageClassesMap()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
volume := parseVolume(persistentVolumeClaim, persistentVolumesMap, storageClassesMap)
|
|
return &volume, nil
|
|
}
|
|
|
|
// fetchVolumesForNonAdmin fetches the volumes in the namespaces the user has access to.
|
|
// This function is called when the user is not an admin.
|
|
// It fetches all the persistent volume claims, persistent volumes and storage classes in the namespaces the user has access to.
|
|
func (kcl *KubeClient) fetchVolumesForNonAdmin(namespace string) ([]models.K8sVolumeInfo, error) {
|
|
log.Debug().Msgf("Fetching volumes for non-admin user: %v", kcl.NonAdminNamespaces)
|
|
|
|
if len(kcl.NonAdminNamespaces) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
volumes, err := kcl.fetchVolumes(namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap()
|
|
results := make([]models.K8sVolumeInfo, 0)
|
|
for _, volume := range volumes {
|
|
if _, ok := nonAdminNamespaceSet[volume.PersistentVolumeClaim.Namespace]; ok {
|
|
results = append(results, volume)
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// fetchVolumes fetches all the persistent volume claims, persistent volumes and storage classes in the given namespace.
|
|
// It returns a list of K8sVolumeInfo.
|
|
// This function is called by fetchVolumesForAdmin and fetchVolumesForNonAdmin.
|
|
func (kcl *KubeClient) fetchVolumes(namespace string) ([]models.K8sVolumeInfo, error) {
|
|
volumes := make([]models.K8sVolumeInfo, 0)
|
|
persistentVolumeClaims, err := kcl.cli.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(persistentVolumeClaims.Items) > 0 {
|
|
persistentVolumesMap, storageClassesMap, err := kcl.fetchPersistentVolumesAndStorageClassesMap()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, persistentVolumeClaim := range persistentVolumeClaims.Items {
|
|
volumes = append(volumes, parseVolume(&persistentVolumeClaim, persistentVolumesMap, storageClassesMap))
|
|
}
|
|
}
|
|
|
|
return volumes, nil
|
|
}
|
|
|
|
// parseVolume parses the given persistent volume claim and returns a K8sVolumeInfo.
|
|
// This function is called by fetchVolumes.
|
|
// It returns a K8sVolumeInfo.
|
|
func parseVolume(persistentVolumeClaim *corev1.PersistentVolumeClaim, persistentVolumesMap map[string]models.K8sPersistentVolume, storageClassesMap map[string]models.K8sStorageClass) models.K8sVolumeInfo {
|
|
volume := models.K8sVolumeInfo{}
|
|
volumeClaim := parsePersistentVolumeClaim(persistentVolumeClaim)
|
|
|
|
if volumeClaim.VolumeName != "" {
|
|
persistentVolume, ok := persistentVolumesMap[volumeClaim.VolumeName]
|
|
if ok {
|
|
volume.PersistentVolume = persistentVolume
|
|
}
|
|
}
|
|
|
|
if volumeClaim.StorageClass != nil {
|
|
storageClass, ok := storageClassesMap[*volumeClaim.StorageClass]
|
|
if ok {
|
|
volume.StorageClass = storageClass
|
|
}
|
|
}
|
|
|
|
volume.PersistentVolumeClaim = volumeClaim
|
|
return volume
|
|
}
|
|
|
|
// parsePersistentVolumeClaim parses the given persistent volume claim and returns a K8sPersistentVolumeClaim.
|
|
func parsePersistentVolumeClaim(volume *corev1.PersistentVolumeClaim) models.K8sPersistentVolumeClaim {
|
|
storage := volume.Spec.Resources.Requests[corev1.ResourceStorage]
|
|
return models.K8sPersistentVolumeClaim{
|
|
ID: string(volume.UID),
|
|
Name: volume.Name,
|
|
Namespace: volume.Namespace,
|
|
CreationDate: volume.CreationTimestamp.Time,
|
|
Storage: storage.Value(),
|
|
AccessModes: volume.Spec.AccessModes,
|
|
VolumeName: volume.Spec.VolumeName,
|
|
ResourcesRequests: &volume.Spec.Resources.Requests,
|
|
StorageClass: volume.Spec.StorageClassName,
|
|
VolumeMode: volume.Spec.VolumeMode,
|
|
OwningApplications: nil,
|
|
Phase: volume.Status.Phase,
|
|
}
|
|
}
|
|
|
|
// parsePersistentVolume parses the given persistent volume and returns a K8sPersistentVolume.
|
|
func parsePersistentVolume(volume *corev1.PersistentVolume) models.K8sPersistentVolume {
|
|
return models.K8sPersistentVolume{
|
|
Name: volume.Name,
|
|
Annotations: volume.Annotations,
|
|
AccessModes: volume.Spec.AccessModes,
|
|
Capacity: volume.Spec.Capacity,
|
|
ClaimRef: volume.Spec.ClaimRef,
|
|
StorageClassName: volume.Spec.StorageClassName,
|
|
PersistentVolumeReclaimPolicy: volume.Spec.PersistentVolumeReclaimPolicy,
|
|
VolumeMode: volume.Spec.VolumeMode,
|
|
CSI: volume.Spec.CSI,
|
|
}
|
|
}
|
|
|
|
// buildPersistentVolumesMap builds a map of persistent volumes.
|
|
func (kcl *KubeClient) buildPersistentVolumesMap(persistentVolumes *corev1.PersistentVolumeList) map[string]models.K8sPersistentVolume {
|
|
persistentVolumesMap := make(map[string]models.K8sPersistentVolume)
|
|
for _, persistentVolume := range persistentVolumes.Items {
|
|
persistentVolumesMap[persistentVolume.Name] = parsePersistentVolume(&persistentVolume)
|
|
}
|
|
|
|
return persistentVolumesMap
|
|
}
|
|
|
|
// parseStorageClass parses the given storage class and returns a K8sStorageClass.
|
|
func parseStorageClass(storageClass *storagev1.StorageClass) models.K8sStorageClass {
|
|
return models.K8sStorageClass{
|
|
Name: storageClass.Name,
|
|
Provisioner: storageClass.Provisioner,
|
|
ReclaimPolicy: storageClass.ReclaimPolicy,
|
|
AllowVolumeExpansion: storageClass.AllowVolumeExpansion,
|
|
}
|
|
}
|
|
|
|
// buildStorageClassesMap builds a map of storage classes.
|
|
func (kcl *KubeClient) buildStorageClassesMap(storageClasses *storagev1.StorageClassList) map[string]models.K8sStorageClass {
|
|
storageClassesMap := make(map[string]models.K8sStorageClass)
|
|
for _, storageClass := range storageClasses.Items {
|
|
storageClassesMap[storageClass.Name] = parseStorageClass(&storageClass)
|
|
}
|
|
|
|
return storageClassesMap
|
|
}
|
|
|
|
// fetchPersistentVolumesAndStorageClassesMap fetches all the persistent volumes and storage classes in the cluster.
|
|
// It returns a map of persistent volumes and a map of storage classes.
|
|
func (kcl *KubeClient) fetchPersistentVolumesAndStorageClassesMap() (map[string]models.K8sPersistentVolume, map[string]models.K8sStorageClass, error) {
|
|
persistentVolumes, err := kcl.cli.CoreV1().PersistentVolumes().List(context.TODO(), metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
persistentVolumesMap := kcl.buildPersistentVolumesMap(persistentVolumes)
|
|
|
|
storageClasses, err := kcl.cli.StorageV1().StorageClasses().List(context.TODO(), metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
storageClassesMap := kcl.buildStorageClassesMap(storageClasses)
|
|
|
|
return persistentVolumesMap, storageClassesMap, nil
|
|
}
|
|
|
|
// CombineVolumesWithApplications combines the volumes with the applications that use them.
|
|
func (kcl *KubeClient) CombineVolumesWithApplications(volumes *[]models.K8sVolumeInfo) (*[]models.K8sVolumeInfo, error) {
|
|
pods, err := kcl.cli.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{})
|
|
if err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
return volumes, nil
|
|
}
|
|
log.Error().Err(err).Msg("Failed to list pods across the cluster")
|
|
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to list pods across the cluster. Error: %w", err)
|
|
}
|
|
|
|
hasReplicaSetOwnerReference := containsReplicaSetOwnerReference(pods)
|
|
replicaSetItems := make([]appsv1.ReplicaSet, 0)
|
|
if hasReplicaSetOwnerReference {
|
|
replicaSets, err := kcl.cli.AppsV1().ReplicaSets("").List(context.Background(), metav1.ListOptions{})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to list replica sets across the cluster")
|
|
return nil, fmt.Errorf("an error occurred during the CombineVolumesWithApplications operation, unable to list replica sets across the cluster. Error: %w", err)
|
|
}
|
|
replicaSetItems = replicaSets.Items
|
|
}
|
|
|
|
return kcl.updateVolumesWithOwningApplications(volumes, pods, replicaSetItems)
|
|
}
|
|
|
|
// updateVolumesWithOwningApplications updates the volumes with the applications that use them.
|
|
func (kcl *KubeClient) updateVolumesWithOwningApplications(volumes *[]models.K8sVolumeInfo, pods *corev1.PodList, replicaSetItems []appsv1.ReplicaSet) (*[]models.K8sVolumeInfo, error) {
|
|
for i, volume := range *volumes {
|
|
for _, pod := range pods.Items {
|
|
if pod.Spec.Volumes != nil {
|
|
for _, podVolume := range pod.Spec.Volumes {
|
|
if podVolume.PersistentVolumeClaim != nil && podVolume.PersistentVolumeClaim.ClaimName == volume.PersistentVolumeClaim.Name && pod.Namespace == volume.PersistentVolumeClaim.Namespace {
|
|
application, err := kcl.ConvertPodToApplication(pod, replicaSetItems, []appsv1.Deployment{}, []appsv1.StatefulSet{}, []appsv1.DaemonSet{}, []corev1.Service{}, false)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to convert pod to application")
|
|
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to convert pod to application. Error: %w", err)
|
|
}
|
|
// Check if the application already exists in the OwningApplications slice
|
|
exists := false
|
|
for _, existingApp := range (*volumes)[i].PersistentVolumeClaim.OwningApplications {
|
|
if existingApp.Name == application.Name && existingApp.Namespace == application.Namespace {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
if !exists && application != nil {
|
|
(*volumes)[i].PersistentVolumeClaim.OwningApplications = append((*volumes)[i].PersistentVolumeClaim.OwningApplications, *application)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return volumes, nil
|
|
}
|