mirror of https://github.com/portainer/portainer
fix(apps): update associated resources on deletion [r8s-124] (#75)
parent
d418784346
commit
c1316532eb
|
@ -3,39 +3,41 @@ package kubernetes
|
|||
import (
|
||||
"time"
|
||||
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type K8sApplication struct {
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Image string `json:"Image"`
|
||||
Containers []interface{} `json:"Containers,omitempty"`
|
||||
Services []corev1.Service `json:"Services"`
|
||||
CreationDate time.Time `json:"CreationDate"`
|
||||
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
|
||||
StackName string `json:"StackName,omitempty"`
|
||||
ResourcePool string `json:"ResourcePool"`
|
||||
ApplicationType string `json:"ApplicationType"`
|
||||
Metadata *Metadata `json:"Metadata,omitempty"`
|
||||
Status string `json:"Status"`
|
||||
TotalPodsCount int `json:"TotalPodsCount"`
|
||||
RunningPodsCount int `json:"RunningPodsCount"`
|
||||
DeploymentType string `json:"DeploymentType"`
|
||||
Pods []Pod `json:"Pods,omitempty"`
|
||||
Configurations []Configuration `json:"Configurations,omitempty"`
|
||||
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
|
||||
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
|
||||
Namespace string `json:"Namespace,omitempty"`
|
||||
UID string `json:"Uid,omitempty"`
|
||||
StackID string `json:"StackId,omitempty"`
|
||||
ServiceID string `json:"ServiceId,omitempty"`
|
||||
ServiceName string `json:"ServiceName,omitempty"`
|
||||
ServiceType string `json:"ServiceType,omitempty"`
|
||||
Kind string `json:"Kind,omitempty"`
|
||||
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
|
||||
Labels map[string]string `json:"Labels,omitempty"`
|
||||
Resource K8sApplicationResource `json:"Resource,omitempty"`
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Image string `json:"Image"`
|
||||
Containers []interface{} `json:"Containers,omitempty"`
|
||||
Services []corev1.Service `json:"Services"`
|
||||
CreationDate time.Time `json:"CreationDate"`
|
||||
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
|
||||
StackName string `json:"StackName,omitempty"`
|
||||
ResourcePool string `json:"ResourcePool"`
|
||||
ApplicationType string `json:"ApplicationType"`
|
||||
Metadata *Metadata `json:"Metadata,omitempty"`
|
||||
Status string `json:"Status"`
|
||||
TotalPodsCount int `json:"TotalPodsCount"`
|
||||
RunningPodsCount int `json:"RunningPodsCount"`
|
||||
DeploymentType string `json:"DeploymentType"`
|
||||
Pods []Pod `json:"Pods,omitempty"`
|
||||
Configurations []Configuration `json:"Configurations,omitempty"`
|
||||
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
|
||||
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
|
||||
Namespace string `json:"Namespace,omitempty"`
|
||||
UID string `json:"Uid,omitempty"`
|
||||
StackID string `json:"StackId,omitempty"`
|
||||
ServiceID string `json:"ServiceId,omitempty"`
|
||||
ServiceName string `json:"ServiceName,omitempty"`
|
||||
ServiceType string `json:"ServiceType,omitempty"`
|
||||
Kind string `json:"Kind,omitempty"`
|
||||
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
|
||||
Labels map[string]string `json:"Labels,omitempty"`
|
||||
Resource K8sApplicationResource `json:"Resource,omitempty"`
|
||||
HorizontalPodAutoscaler *autoscalingv2.HorizontalPodAutoscaler `json:"HorizontalPodAutoscaler,omitempty"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
labels "k8s.io/apimachinery/pkg/labels"
|
||||
|
@ -31,20 +32,20 @@ func (kcl *KubeClient) fetchApplications(namespace, nodeName string, withDepende
|
|||
}
|
||||
if !withDependencies {
|
||||
// TODO: make sure not to fetch services in fetchAllApplicationsListResources from this call
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, _, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, _, _, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, nil)
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, nil, nil)
|
||||
}
|
||||
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services)
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas)
|
||||
}
|
||||
|
||||
// fetchApplicationsForNonAdmin fetches the applications in the namespaces the user has access to.
|
||||
|
@ -62,20 +63,20 @@ func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string,
|
|||
}
|
||||
|
||||
if !withDependencies {
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, nil, nil, nil, nil)
|
||||
return kcl.convertPodsToApplications(pods, replicaSets, nil, nil, nil, nil, nil)
|
||||
}
|
||||
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas, err := kcl.fetchAllApplicationsListResources(namespace, podListOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
applications, err := kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services)
|
||||
applications, err := kcl.convertPodsToApplications(pods, replicaSets, deployments, statefulSets, daemonSets, services, hpas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -92,7 +93,7 @@ func (kcl *KubeClient) fetchApplicationsForNonAdmin(namespace, nodeName string,
|
|||
}
|
||||
|
||||
// convertPodsToApplications processes pods and converts them to applications, ensuring uniqueness by owner reference.
|
||||
func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service) ([]models.K8sApplication, error) {
|
||||
func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, hpas []autoscalingv2.HorizontalPodAutoscaler) ([]models.K8sApplication, error) {
|
||||
applications := []models.K8sApplication{}
|
||||
processedOwners := make(map[string]struct{})
|
||||
|
||||
|
@ -105,7 +106,7 @@ func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets
|
|||
processedOwners[ownerUID] = struct{}{}
|
||||
}
|
||||
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, deployments, statefulSets, daemonSets, services, true)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, deployments, statefulSets, daemonSets, services, hpas, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -150,7 +151,7 @@ func (kcl *KubeClient) GetApplicationNamesFromConfigMap(configMap models.K8sConf
|
|||
for _, pod := range pods {
|
||||
if pod.Namespace == configMap.Namespace {
|
||||
if isPodUsingConfigMap(&pod, configMap.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -167,7 +168,7 @@ func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, po
|
|||
for _, pod := range pods {
|
||||
if pod.Namespace == secret.Namespace {
|
||||
if isPodUsingSecret(&pod, secret.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -180,12 +181,12 @@ func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, po
|
|||
}
|
||||
|
||||
// ConvertPodToApplication converts a pod to an application, updating owner references if necessary
|
||||
func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, withResource bool) (*models.K8sApplication, error) {
|
||||
func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []appsv1.ReplicaSet, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, hpas []autoscalingv2.HorizontalPodAutoscaler, withResource bool) (*models.K8sApplication, error) {
|
||||
if isReplicaSetOwner(pod) {
|
||||
updateOwnerReferenceToDeployment(&pod, replicaSets)
|
||||
}
|
||||
|
||||
application := createApplication(&pod, deployments, statefulSets, daemonSets, services)
|
||||
application := createApplication(&pod, deployments, statefulSets, daemonSets, services, hpas)
|
||||
if application.ID == "" && application.Name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -204,7 +205,7 @@ func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []app
|
|||
|
||||
// createApplication creates a K8sApplication object from a pod
|
||||
// it sets the application name, namespace, kind, image, stack id, stack name, and labels
|
||||
func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service) models.K8sApplication {
|
||||
func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefulSets []appsv1.StatefulSet, daemonSets []appsv1.DaemonSet, services []corev1.Service, hpas []autoscalingv2.HorizontalPodAutoscaler) models.K8sApplication {
|
||||
kind := "Pod"
|
||||
name := pod.Name
|
||||
|
||||
|
@ -324,7 +325,11 @@ func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefu
|
|||
}
|
||||
|
||||
if application.ID != "" && application.Name != "" && len(services) > 0 {
|
||||
return updateApplicationWithService(application, services)
|
||||
updateApplicationWithService(&application, services)
|
||||
}
|
||||
|
||||
if application.ID != "" && application.Name != "" && len(hpas) > 0 {
|
||||
updateApplicationWithHorizontalPodAutoscaler(&application, hpas)
|
||||
}
|
||||
|
||||
return application
|
||||
|
@ -332,7 +337,7 @@ func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefu
|
|||
|
||||
// updateApplicationWithService updates the application with the services that match the application's selector match labels
|
||||
// and are in the same namespace as the application
|
||||
func updateApplicationWithService(application models.K8sApplication, services []corev1.Service) models.K8sApplication {
|
||||
func updateApplicationWithService(application *models.K8sApplication, services []corev1.Service) {
|
||||
for _, service := range services {
|
||||
serviceSelector := labels.SelectorFromSet(service.Spec.Selector)
|
||||
|
||||
|
@ -341,8 +346,23 @@ func updateApplicationWithService(application models.K8sApplication, services []
|
|||
application.Services = append(application.Services, service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return application
|
||||
func updateApplicationWithHorizontalPodAutoscaler(application *models.K8sApplication, hpas []autoscalingv2.HorizontalPodAutoscaler) {
|
||||
for _, hpa := range hpas {
|
||||
// Check if HPA is in the same namespace as the application
|
||||
if hpa.Namespace != application.ResourcePool {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the scale target ref matches the application
|
||||
scaleTargetRef := hpa.Spec.ScaleTargetRef
|
||||
if scaleTargetRef.Name == application.Name && scaleTargetRef.Kind == application.Kind {
|
||||
hpaCopy := hpa // Create a local copy
|
||||
application.HorizontalPodAutoscaler = &hpaCopy
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculatePodResourceUsage calculates the resource usage for a pod in CPU cores and Bytes
|
||||
|
@ -390,7 +410,7 @@ func (kcl *KubeClient) GetApplicationConfigurationOwnersFromConfigMap(configMap
|
|||
for _, pod := range pods {
|
||||
if pod.Namespace == configMap.Namespace {
|
||||
if isPodUsingConfigMap(&pod, configMap.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -416,7 +436,7 @@ func (kcl *KubeClient) GetApplicationConfigurationOwnersFromSecret(secret models
|
|||
for _, pod := range pods {
|
||||
if pod.Namespace == secret.Namespace {
|
||||
if isPodUsingSecret(&pod, secret.Name) {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSets, nil, nil, nil, nil, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ func parseConfigMap(configMap *corev1.ConfigMap, withData bool) models.K8sConfig
|
|||
func (kcl *KubeClient) CombineConfigMapsWithApplications(configMaps []models.K8sConfigMap) ([]models.K8sConfigMap, error) {
|
||||
updatedConfigMaps := make([]models.K8sConfigMap, len(configMaps))
|
||||
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("an error occurred during the CombineConfigMapsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -172,24 +173,24 @@ func (kcl *KubeClient) waitForPodStatus(ctx context.Context, phase corev1.PodPha
|
|||
}
|
||||
|
||||
// fetchAllPodsAndReplicaSets fetches all pods and replica sets across the cluster, i.e. all namespaces
|
||||
func (kcl *KubeClient) fetchAllPodsAndReplicaSets(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||
func (kcl *KubeClient) fetchAllPodsAndReplicaSets(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, []autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
return kcl.fetchResourcesWithOwnerReferences(namespace, podListOptions, false, false)
|
||||
}
|
||||
|
||||
// fetchAllApplicationsListResources fetches all pods, replica sets, stateful sets, and daemon sets across the cluster, i.e. all namespaces
|
||||
// this is required for the applications list view
|
||||
func (kcl *KubeClient) fetchAllApplicationsListResources(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||
func (kcl *KubeClient) fetchAllApplicationsListResources(namespace string, podListOptions metav1.ListOptions) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, []autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
return kcl.fetchResourcesWithOwnerReferences(namespace, podListOptions, true, true)
|
||||
}
|
||||
|
||||
// fetchResourcesWithOwnerReferences fetches pods and other resources based on owner references
|
||||
func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podListOptions metav1.ListOptions, includeStatefulSets, includeDaemonSets bool) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, error) {
|
||||
func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podListOptions metav1.ListOptions, includeStatefulSets, includeDaemonSets bool) ([]corev1.Pod, []appsv1.ReplicaSet, []appsv1.Deployment, []appsv1.StatefulSet, []appsv1.DaemonSet, []corev1.Service, []autoscalingv2.HorizontalPodAutoscaler, error) {
|
||||
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), podListOptions)
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, nil
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil
|
||||
}
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list pods across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list pods across the cluster: %w", err)
|
||||
}
|
||||
|
||||
// if replicaSet owner reference exists, fetch the replica sets
|
||||
|
@ -199,12 +200,12 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
|
|||
if containsReplicaSetOwnerReference(pods) {
|
||||
replicaSets, err = kcl.cli.AppsV1().ReplicaSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list replica sets across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list replica sets across the cluster: %w", err)
|
||||
}
|
||||
|
||||
deployments, err = kcl.cli.AppsV1().Deployments(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list deployments across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list deployments across the cluster: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,7 +213,7 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
|
|||
if includeStatefulSets && containsStatefulSetOwnerReference(pods) {
|
||||
statefulSets, err = kcl.cli.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list stateful sets across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list stateful sets across the cluster: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,16 +221,21 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
|
|||
if includeDaemonSets && containsDaemonSetOwnerReference(pods) {
|
||||
daemonSets, err = kcl.cli.AppsV1().DaemonSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list daemon sets across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list daemon sets across the cluster: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
services, err := kcl.cli.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list services across the cluster: %w", err)
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list services across the cluster: %w", err)
|
||||
}
|
||||
|
||||
return pods.Items, replicaSets.Items, deployments.Items, statefulSets.Items, daemonSets.Items, services.Items, nil
|
||||
hpas, err := kcl.cli.AutoscalingV2().HorizontalPodAutoscalers(namespace).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil && !k8serrors.IsNotFound(err) {
|
||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to list horizontal pod autoscalers across the cluster: %w", err)
|
||||
}
|
||||
|
||||
return pods.Items, replicaSets.Items, deployments.Items, statefulSets.Items, daemonSets.Items, services.Items, hpas.Items, nil
|
||||
}
|
||||
|
||||
// isPodUsingConfigMap checks if a pod is using a specific ConfigMap
|
||||
|
|
|
@ -118,7 +118,7 @@ func parseSecret(secret *corev1.Secret, withData bool) models.K8sSecret {
|
|||
func (kcl *KubeClient) CombineSecretsWithApplications(secrets []models.K8sSecret) ([]models.K8sSecret, error) {
|
||||
updatedSecrets := make([]models.K8sSecret, len(secrets))
|
||||
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("an error occurred during the CombineSecretsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||
}
|
||||
|
|
|
@ -174,7 +174,7 @@ func (kcl *KubeClient) UpdateService(namespace string, info models.K8sServiceInf
|
|||
func (kcl *KubeClient) CombineServicesWithApplications(services []models.K8sServiceInfo) ([]models.K8sServiceInfo, error) {
|
||||
if containsServiceWithSelector(services) {
|
||||
updatedServices := make([]models.K8sServiceInfo, len(services))
|
||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
@ -264,7 +265,7 @@ func (kcl *KubeClient) updateVolumesWithOwningApplications(volumes *[]models.K8s
|
|||
if pod.Spec.Volumes != nil {
|
||||
for _, podVolume := range pod.Spec.Volumes {
|
||||
if podVolume.VolumeSource.PersistentVolumeClaim != nil && podVolume.VolumeSource.PersistentVolumeClaim.ClaimName == volume.PersistentVolumeClaim.Name && pod.Namespace == volume.PersistentVolumeClaim.Namespace {
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSetItems, deploymentItems, statefulSetItems, daemonSetItems, []corev1.Service{}, false)
|
||||
application, err := kcl.ConvertPodToApplication(pod, replicaSetItems, deploymentItems, statefulSetItems, daemonSetItems, []corev1.Service{}, []autoscalingv2.HorizontalPodAutoscaler{}, 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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RawParams } from '@uirouter/react';
|
||||
import { RawParams, useCurrentStateAndParams } from '@uirouter/react';
|
||||
import clsx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { Link } from '@@/Link';
|
|||
|
||||
export interface Tab {
|
||||
name: ReactNode;
|
||||
icon: ReactNode;
|
||||
icon?: ReactNode;
|
||||
widget: ReactNode;
|
||||
selectedTabParam: string;
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export function WidgetTabs({ currentTabIndex, tabs }: Props) {
|
|||
)}
|
||||
data-cy={`tab-${index}`}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
{icon && <Icon icon={icon} />}
|
||||
{name}
|
||||
</Link>
|
||||
))}
|
||||
|
@ -67,5 +67,15 @@ export function findSelectedTabIndex(
|
|||
const currentTabIndex = tabs.findIndex(
|
||||
(tab) => tab.selectedTabParam === selectedTabParam
|
||||
);
|
||||
return currentTabIndex || 0;
|
||||
if (currentTabIndex === -1) {
|
||||
return 0;
|
||||
}
|
||||
return currentTabIndex;
|
||||
}
|
||||
|
||||
export function useCurrentTabIndex(tabs: Tab[]) {
|
||||
const prarms = useCurrentStateAndParams();
|
||||
const currentTabIndex = findSelectedTabIndex(prarms, tabs);
|
||||
|
||||
return [currentTabIndex];
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useAuthorizations } from '@/react/hooks/useUser';
|
|||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
|
||||
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models/appConstants';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||
import { useIngresses } from '@/react/kubernetes/ingresses/queries';
|
||||
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
@ -58,6 +59,8 @@ export function ApplicationsDatatable({
|
|||
namespace: tableState.namespace,
|
||||
withDependencies: true,
|
||||
});
|
||||
const ingressesQuery = useIngresses(environmentId);
|
||||
const ingresses = ingressesQuery.data ?? [];
|
||||
const applications = useApplicationsRowData(applicationsQuery.data);
|
||||
const filteredApplications = tableState.showSystemResources
|
||||
? applications
|
||||
|
@ -69,6 +72,7 @@ export function ApplicationsDatatable({
|
|||
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
||||
environmentId,
|
||||
stacks,
|
||||
ingresses,
|
||||
reportStacks: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { Service } from 'kubernetes-types/core/v1';
|
||||
import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v2';
|
||||
|
||||
import { AppType, DeploymentType } from '../../types';
|
||||
|
||||
export interface ApplicationRowData extends Application {
|
||||
|
@ -9,7 +12,7 @@ export interface Application {
|
|||
Name: string;
|
||||
Image: string;
|
||||
Containers?: Array<unknown>;
|
||||
Services?: Array<unknown>;
|
||||
Services?: Array<Service>;
|
||||
CreationDate: string;
|
||||
ApplicationOwner?: string;
|
||||
StackName?: string;
|
||||
|
@ -48,6 +51,7 @@ export interface Application {
|
|||
MemoryLimit?: number;
|
||||
MemoryRequest?: number;
|
||||
};
|
||||
HorizontalPodAutoscaler?: HorizontalPodAutoscaler;
|
||||
}
|
||||
|
||||
export enum ConfigKind {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
|||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
|
||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||
import { useIngresses } from '@/react/kubernetes/ingresses/queries';
|
||||
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
|
@ -39,6 +40,8 @@ export function ApplicationsStacksDatatable({
|
|||
namespace: tableState.namespace,
|
||||
withDependencies: true,
|
||||
});
|
||||
const ingressesQuery = useIngresses(environmentId);
|
||||
const ingresses = ingressesQuery.data ?? [];
|
||||
const applications = applicationsQuery.data ?? [];
|
||||
const filteredApplications = tableState.showSystemResources
|
||||
? applications
|
||||
|
@ -50,6 +53,7 @@ export function ApplicationsStacksDatatable({
|
|||
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
||||
environmentId,
|
||||
stacks,
|
||||
ingresses,
|
||||
reportStacks: true,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { HorizontalPodAutoscalerList } from 'kubernetes-types/autoscaling/v1';
|
||||
import {
|
||||
HorizontalPodAutoscaler,
|
||||
HorizontalPodAutoscalerList,
|
||||
} from 'kubernetes-types/autoscaling/v1';
|
||||
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
@ -27,23 +30,15 @@ export function useApplicationHorizontalPodAutoscaler(
|
|||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const horizontalPodAutoscalers =
|
||||
await getNamespaceHorizontalPodAutoscalers(environmentId, namespace);
|
||||
const matchingHorizontalPodAutoscaler =
|
||||
horizontalPodAutoscalers.find((horizontalPodAutoscaler) => {
|
||||
const scaleTargetRef = horizontalPodAutoscaler.spec?.scaleTargetRef;
|
||||
if (scaleTargetRef) {
|
||||
const scaleTargetRefName = scaleTargetRef.name;
|
||||
const scaleTargetRefKind = scaleTargetRef.kind;
|
||||
// include the horizontal pod autoscaler if the scale target ref name and kind match the application name and kind
|
||||
return (
|
||||
scaleTargetRefName === app.metadata?.name &&
|
||||
scaleTargetRefKind === app.kind
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}) || null;
|
||||
getMatchingHorizontalPodAutoscaler(
|
||||
horizontalPodAutoscalers,
|
||||
namespace,
|
||||
appName,
|
||||
app.kind || ''
|
||||
);
|
||||
return matchingHorizontalPodAutoscaler;
|
||||
},
|
||||
{
|
||||
|
@ -57,6 +52,29 @@ export function useApplicationHorizontalPodAutoscaler(
|
|||
);
|
||||
}
|
||||
|
||||
export function getMatchingHorizontalPodAutoscaler(
|
||||
horizontalPodAutoscalers: HorizontalPodAutoscaler[],
|
||||
appNamespace: string,
|
||||
appName: string,
|
||||
appKind: string
|
||||
) {
|
||||
const matchingHorizontalPodAutoscaler =
|
||||
horizontalPodAutoscalers.find((horizontalPodAutoscaler) => {
|
||||
if (horizontalPodAutoscaler.metadata?.namespace !== appNamespace) {
|
||||
return false;
|
||||
}
|
||||
const scaleTargetRef = horizontalPodAutoscaler.spec?.scaleTargetRef;
|
||||
if (scaleTargetRef) {
|
||||
const scaleTargetRefName = scaleTargetRef.name;
|
||||
const scaleTargetRefKind = scaleTargetRef.kind;
|
||||
// include the horizontal pod autoscaler if the scale target ref name and kind match the application name and kind
|
||||
return scaleTargetRefName === appName && scaleTargetRefKind === appKind;
|
||||
}
|
||||
return false;
|
||||
}) || null;
|
||||
return matchingHorizontalPodAutoscaler;
|
||||
}
|
||||
|
||||
async function getNamespaceHorizontalPodAutoscalers(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v2';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
@ -10,23 +11,34 @@ import { pluralize } from '@/portainer/helpers/strings';
|
|||
import { parseKubernetesAxiosError } from '../../axiosError';
|
||||
import { ApplicationRowData } from '../ListView/ApplicationsDatatable/types';
|
||||
import { Stack } from '../ListView/ApplicationsStacksDatatable/types';
|
||||
import { deleteServices } from '../../services/service';
|
||||
import { updateIngress } from '../../ingresses/service';
|
||||
import { Ingress } from '../../ingresses/types';
|
||||
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useDeleteApplicationsMutation({
|
||||
environmentId,
|
||||
stacks,
|
||||
ingresses,
|
||||
reportStacks,
|
||||
}: {
|
||||
environmentId: EnvironmentId;
|
||||
stacks: Stack[];
|
||||
ingresses: Ingress[];
|
||||
reportStacks?: boolean;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (applications: ApplicationRowData[]) =>
|
||||
deleteApplications(applications, stacks, environmentId),
|
||||
onSuccess: ({ settledAppDeletions, settledStackDeletions }) => {
|
||||
deleteApplications(applications, stacks, ingresses, environmentId),
|
||||
// apps and stacks deletions results (success and error) are handled here
|
||||
onSuccess: ({
|
||||
settledAppDeletions,
|
||||
settledStackDeletions,
|
||||
settledIngressUpdates,
|
||||
settledHpaDeletions,
|
||||
}) => {
|
||||
// one error notification per rejected item
|
||||
settledAppDeletions.rejectedItems.forEach(({ item, reason }) => {
|
||||
notifyError(
|
||||
|
@ -37,6 +49,18 @@ export function useDeleteApplicationsMutation({
|
|||
settledStackDeletions.rejectedItems.forEach(({ item, reason }) => {
|
||||
notifyError(`Failed to remove stack '${item.Name}'`, new Error(reason));
|
||||
});
|
||||
settledIngressUpdates.rejectedItems.forEach(({ item, reason }) => {
|
||||
notifyError(
|
||||
`Failed to update ingress paths for '${item.Name}'`,
|
||||
new Error(reason)
|
||||
);
|
||||
});
|
||||
settledHpaDeletions.rejectedItems.forEach(({ item, reason }) => {
|
||||
notifyError(
|
||||
`Failed to remove horizontal pod autoscaler for '${item.metadata?.name}'`,
|
||||
new Error(reason)
|
||||
);
|
||||
});
|
||||
|
||||
// one success notification for all fulfilled items
|
||||
if (settledAppDeletions.fulfilledItems.length && !reportStacks) {
|
||||
|
@ -59,8 +83,13 @@ export function useDeleteApplicationsMutation({
|
|||
.join(', ')
|
||||
);
|
||||
}
|
||||
// dont notify successful ingress updates to avoid notification spam
|
||||
queryClient.invalidateQueries(queryKeys.applications(environmentId));
|
||||
},
|
||||
// failed service deletions are handled here
|
||||
onError: (error: unknown) => {
|
||||
notifyError('Unable to remove applications', error as Error);
|
||||
},
|
||||
...withGlobalError('Unable to remove applications'),
|
||||
});
|
||||
}
|
||||
|
@ -68,6 +97,7 @@ export function useDeleteApplicationsMutation({
|
|||
async function deleteApplications(
|
||||
applications: ApplicationRowData[],
|
||||
stacks: Stack[],
|
||||
ingresses: Ingress[],
|
||||
environmentId: EnvironmentId
|
||||
) {
|
||||
const settledAppDeletions = await getAllSettledItems(
|
||||
|
@ -84,7 +114,83 @@ async function deleteApplications(
|
|||
(stack) => deleteStack(stack, environmentId)
|
||||
);
|
||||
|
||||
return { settledAppDeletions, settledStackDeletions };
|
||||
// update associated k8s ressources
|
||||
const servicesToDelete = getServicesFromApplications(applications);
|
||||
// axios error handling is done inside deleteServices already
|
||||
await deleteServices({
|
||||
environmentId,
|
||||
data: servicesToDelete,
|
||||
});
|
||||
const hpasToDelete = applications
|
||||
.map((app) => app.HorizontalPodAutoscaler)
|
||||
.filter((hpa) => !!hpa);
|
||||
const settledHpaDeletions = await getAllSettledItems(hpasToDelete, (hpa) =>
|
||||
deleteHorizontalPodAutoscaler(hpa, environmentId)
|
||||
);
|
||||
const updatedIngresses = getUpdatedIngressesWithRemovedPaths(
|
||||
ingresses,
|
||||
servicesToDelete
|
||||
);
|
||||
const settledIngressUpdates = await getAllSettledItems(
|
||||
updatedIngresses,
|
||||
(ingress) => updateIngress(environmentId, ingress)
|
||||
);
|
||||
|
||||
return {
|
||||
settledAppDeletions,
|
||||
settledStackDeletions,
|
||||
settledIngressUpdates,
|
||||
settledHpaDeletions,
|
||||
};
|
||||
}
|
||||
|
||||
function getServicesFromApplications(
|
||||
applications: ApplicationRowData[]
|
||||
): Record<string, string[]> {
|
||||
return applications.reduce<Record<string, string[]>>(
|
||||
(namespaceServicesMap, application) => {
|
||||
const serviceNames =
|
||||
application.Services?.map((service) => service.metadata?.name).filter(
|
||||
(name): name is string => !!name
|
||||
) || [];
|
||||
if (namespaceServicesMap[application.ResourcePool]) {
|
||||
return {
|
||||
...namespaceServicesMap,
|
||||
[application.ResourcePool]: [
|
||||
...namespaceServicesMap[application.ResourcePool],
|
||||
...serviceNames,
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...namespaceServicesMap,
|
||||
[application.ResourcePool]: serviceNames,
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// return a list of ingresses, which need updated because their paths should be removed for deleted services.
|
||||
function getUpdatedIngressesWithRemovedPaths(
|
||||
ingresses: Ingress[],
|
||||
servicesToDelete: Record<string, string[]>
|
||||
) {
|
||||
return ingresses.reduce<Ingress[]>((updatedIngresses, ingress) => {
|
||||
if (!ingress.Paths) {
|
||||
return updatedIngresses;
|
||||
}
|
||||
const servicesInNamespace = servicesToDelete[ingress.Namespace] || [];
|
||||
// filter out the paths for services that are in the list of services to delete
|
||||
const updatedIngressPaths =
|
||||
ingress.Paths.filter(
|
||||
(path) => !servicesInNamespace.includes(path.ServiceName)
|
||||
) ?? [];
|
||||
if (updatedIngressPaths.length !== ingress.Paths?.length) {
|
||||
return [...updatedIngresses, { ...ingress, Paths: updatedIngressPaths }];
|
||||
}
|
||||
return updatedIngresses;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async function deleteStack(stack: Stack, environmentId: EnvironmentId) {
|
||||
|
@ -173,6 +279,22 @@ async function uninstallHelmApplication(
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteHorizontalPodAutoscaler(
|
||||
hpa: HorizontalPodAutoscaler,
|
||||
environmentId: EnvironmentId
|
||||
) {
|
||||
try {
|
||||
await axios.delete(
|
||||
`/endpoints/${environmentId}/kubernetes/apis/autoscaling/v2/namespaces/${hpa.metadata?.namespace}/horizontalpodautoscalers/${hpa.metadata?.name}`
|
||||
);
|
||||
} catch (error) {
|
||||
throw parseKubernetesAxiosError(
|
||||
error,
|
||||
'Unable to remove horizontal pod autoscaler'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// mutate the stacks array to remove the application
|
||||
function removeApplicationFromStack(
|
||||
application: ApplicationRowData,
|
||||
|
|
|
@ -68,7 +68,7 @@ export async function updateIngress(
|
|||
ingress: Ingress
|
||||
) {
|
||||
try {
|
||||
return await axios.put(buildUrl(environmentId, ingress.Namespace), ingress);
|
||||
await axios.put(buildUrl(environmentId, ingress.Namespace), ingress);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update an ingress');
|
||||
}
|
||||
|
|
|
@ -8,14 +8,14 @@ import { queryKeys } from './query-keys';
|
|||
|
||||
export function useDeleteServiceAccountsMutation(environmentId: EnvironmentId) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(deleteServices, {
|
||||
return useMutation(deleteServiceAccounts, {
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries(queryKeys.list(environmentId)),
|
||||
...withGlobalError('Unable to delete service accounts'),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteServices({
|
||||
export async function deleteServiceAccounts({
|
||||
environmentId,
|
||||
data,
|
||||
}: {
|
||||
|
|
Loading…
Reference in New Issue