fix(apps): update associated resources on deletion [r8s-124] (#75)

pull/12292/merge
Ali 3 weeks ago committed by GitHub
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…
Cancel
Save