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 (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type K8sApplication struct {
|
type K8sApplication struct {
|
||||||
ID string `json:"Id"`
|
ID string `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Image string `json:"Image"`
|
Image string `json:"Image"`
|
||||||
Containers []interface{} `json:"Containers,omitempty"`
|
Containers []interface{} `json:"Containers,omitempty"`
|
||||||
Services []corev1.Service `json:"Services"`
|
Services []corev1.Service `json:"Services"`
|
||||||
CreationDate time.Time `json:"CreationDate"`
|
CreationDate time.Time `json:"CreationDate"`
|
||||||
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
|
ApplicationOwner string `json:"ApplicationOwner,omitempty"`
|
||||||
StackName string `json:"StackName,omitempty"`
|
StackName string `json:"StackName,omitempty"`
|
||||||
ResourcePool string `json:"ResourcePool"`
|
ResourcePool string `json:"ResourcePool"`
|
||||||
ApplicationType string `json:"ApplicationType"`
|
ApplicationType string `json:"ApplicationType"`
|
||||||
Metadata *Metadata `json:"Metadata,omitempty"`
|
Metadata *Metadata `json:"Metadata,omitempty"`
|
||||||
Status string `json:"Status"`
|
Status string `json:"Status"`
|
||||||
TotalPodsCount int `json:"TotalPodsCount"`
|
TotalPodsCount int `json:"TotalPodsCount"`
|
||||||
RunningPodsCount int `json:"RunningPodsCount"`
|
RunningPodsCount int `json:"RunningPodsCount"`
|
||||||
DeploymentType string `json:"DeploymentType"`
|
DeploymentType string `json:"DeploymentType"`
|
||||||
Pods []Pod `json:"Pods,omitempty"`
|
Pods []Pod `json:"Pods,omitempty"`
|
||||||
Configurations []Configuration `json:"Configurations,omitempty"`
|
Configurations []Configuration `json:"Configurations,omitempty"`
|
||||||
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
|
LoadBalancerIPAddress string `json:"LoadBalancerIPAddress,omitempty"`
|
||||||
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
|
PublishedPorts []PublishedPort `json:"PublishedPorts,omitempty"`
|
||||||
Namespace string `json:"Namespace,omitempty"`
|
Namespace string `json:"Namespace,omitempty"`
|
||||||
UID string `json:"Uid,omitempty"`
|
UID string `json:"Uid,omitempty"`
|
||||||
StackID string `json:"StackId,omitempty"`
|
StackID string `json:"StackId,omitempty"`
|
||||||
ServiceID string `json:"ServiceId,omitempty"`
|
ServiceID string `json:"ServiceId,omitempty"`
|
||||||
ServiceName string `json:"ServiceName,omitempty"`
|
ServiceName string `json:"ServiceName,omitempty"`
|
||||||
ServiceType string `json:"ServiceType,omitempty"`
|
ServiceType string `json:"ServiceType,omitempty"`
|
||||||
Kind string `json:"Kind,omitempty"`
|
Kind string `json:"Kind,omitempty"`
|
||||||
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
|
MatchLabels map[string]string `json:"MatchLabels,omitempty"`
|
||||||
Labels map[string]string `json:"Labels,omitempty"`
|
Labels map[string]string `json:"Labels,omitempty"`
|
||||||
Resource K8sApplicationResource `json:"Resource,omitempty"`
|
Resource K8sApplicationResource `json:"Resource,omitempty"`
|
||||||
|
HorizontalPodAutoscaler *autoscalingv2.HorizontalPodAutoscaler `json:"HorizontalPodAutoscaler,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
labels "k8s.io/apimachinery/pkg/labels"
|
labels "k8s.io/apimachinery/pkg/labels"
|
||||||
|
@ -31,20 +32,20 @@ func (kcl *KubeClient) fetchApplications(namespace, nodeName string, withDepende
|
||||||
}
|
}
|
||||||
if !withDependencies {
|
if !withDependencies {
|
||||||
// TODO: make sure not to fetch services in fetchAllApplicationsListResources from this call
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// 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 {
|
if !withDependencies {
|
||||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
|
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets(namespace, podListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// 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{}
|
applications := []models.K8sApplication{}
|
||||||
processedOwners := make(map[string]struct{})
|
processedOwners := make(map[string]struct{})
|
||||||
|
|
||||||
|
@ -105,7 +106,7 @@ func (kcl *KubeClient) convertPodsToApplications(pods []corev1.Pod, replicaSets
|
||||||
processedOwners[ownerUID] = struct{}{}
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -150,7 +151,7 @@ func (kcl *KubeClient) GetApplicationNamesFromConfigMap(configMap models.K8sConf
|
||||||
for _, pod := range pods {
|
for _, pod := range pods {
|
||||||
if pod.Namespace == configMap.Namespace {
|
if pod.Namespace == configMap.Namespace {
|
||||||
if isPodUsingConfigMap(&pod, configMap.Name) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -167,7 +168,7 @@ func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, po
|
||||||
for _, pod := range pods {
|
for _, pod := range pods {
|
||||||
if pod.Namespace == secret.Namespace {
|
if pod.Namespace == secret.Namespace {
|
||||||
if isPodUsingSecret(&pod, secret.Name) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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) {
|
if isReplicaSetOwner(pod) {
|
||||||
updateOwnerReferenceToDeployment(&pod, replicaSets)
|
updateOwnerReferenceToDeployment(&pod, replicaSets)
|
||||||
}
|
}
|
||||||
|
|
||||||
application := createApplication(&pod, deployments, statefulSets, daemonSets, services)
|
application := createApplication(&pod, deployments, statefulSets, daemonSets, services, hpas)
|
||||||
if application.ID == "" && application.Name == "" {
|
if application.ID == "" && application.Name == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -204,7 +205,7 @@ func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, replicaSets []app
|
||||||
|
|
||||||
// createApplication creates a K8sApplication object from a pod
|
// createApplication creates a K8sApplication object from a pod
|
||||||
// it sets the application name, namespace, kind, image, stack id, stack name, and labels
|
// 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"
|
kind := "Pod"
|
||||||
name := pod.Name
|
name := pod.Name
|
||||||
|
|
||||||
|
@ -324,7 +325,11 @@ func createApplication(pod *corev1.Pod, deployments []appsv1.Deployment, statefu
|
||||||
}
|
}
|
||||||
|
|
||||||
if application.ID != "" && application.Name != "" && len(services) > 0 {
|
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
|
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
|
// updateApplicationWithService updates the application with the services that match the application's selector match labels
|
||||||
// and are in the same namespace as the application
|
// 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 {
|
for _, service := range services {
|
||||||
serviceSelector := labels.SelectorFromSet(service.Spec.Selector)
|
serviceSelector := labels.SelectorFromSet(service.Spec.Selector)
|
||||||
|
|
||||||
|
@ -341,8 +346,23 @@ func updateApplicationWithService(application models.K8sApplication, services []
|
||||||
application.Services = append(application.Services, service)
|
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
|
// 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 {
|
for _, pod := range pods {
|
||||||
if pod.Namespace == configMap.Namespace {
|
if pod.Namespace == configMap.Namespace {
|
||||||
if isPodUsingConfigMap(&pod, configMap.Name) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -416,7 +436,7 @@ func (kcl *KubeClient) GetApplicationConfigurationOwnersFromSecret(secret models
|
||||||
for _, pod := range pods {
|
for _, pod := range pods {
|
||||||
if pod.Namespace == secret.Namespace {
|
if pod.Namespace == secret.Namespace {
|
||||||
if isPodUsingSecret(&pod, secret.Name) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (kcl *KubeClient) CombineConfigMapsWithApplications(configMaps []models.K8sConfigMap) ([]models.K8sConfigMap, error) {
|
||||||
updatedConfigMaps := make([]models.K8sConfigMap, len(configMaps))
|
updatedConfigMaps := make([]models.K8sConfigMap, len(configMaps))
|
||||||
|
|
||||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("an error occurred during the CombineConfigMapsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
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/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
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
|
// 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)
|
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
|
// 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
|
// 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)
|
return kcl.fetchResourcesWithOwnerReferences(namespace, podListOptions, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchResourcesWithOwnerReferences fetches pods and other resources based on owner references
|
// 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)
|
pods, err := kcl.cli.CoreV1().Pods(namespace).List(context.Background(), podListOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if k8serrors.IsNotFound(err) {
|
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
|
// if replicaSet owner reference exists, fetch the replica sets
|
||||||
|
@ -199,12 +200,12 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
|
||||||
if containsReplicaSetOwnerReference(pods) {
|
if containsReplicaSetOwnerReference(pods) {
|
||||||
replicaSets, err = kcl.cli.AppsV1().ReplicaSets(namespace).List(context.Background(), metav1.ListOptions{})
|
replicaSets, err = kcl.cli.AppsV1().ReplicaSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
if err != nil && !k8serrors.IsNotFound(err) {
|
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{})
|
deployments, err = kcl.cli.AppsV1().Deployments(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
if err != nil && !k8serrors.IsNotFound(err) {
|
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) {
|
if includeStatefulSets && containsStatefulSetOwnerReference(pods) {
|
||||||
statefulSets, err = kcl.cli.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{})
|
statefulSets, err = kcl.cli.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
if err != nil && !k8serrors.IsNotFound(err) {
|
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) {
|
if includeDaemonSets && containsDaemonSetOwnerReference(pods) {
|
||||||
daemonSets, err = kcl.cli.AppsV1().DaemonSets(namespace).List(context.Background(), metav1.ListOptions{})
|
daemonSets, err = kcl.cli.AppsV1().DaemonSets(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
if err != nil && !k8serrors.IsNotFound(err) {
|
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{})
|
services, err := kcl.cli.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{})
|
||||||
if err != nil && !k8serrors.IsNotFound(err) {
|
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
|
// 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) {
|
func (kcl *KubeClient) CombineSecretsWithApplications(secrets []models.K8sSecret) ([]models.K8sSecret, error) {
|
||||||
updatedSecrets := make([]models.K8sSecret, len(secrets))
|
updatedSecrets := make([]models.K8sSecret, len(secrets))
|
||||||
|
|
||||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("an error occurred during the CombineSecretsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
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) {
|
func (kcl *KubeClient) CombineServicesWithApplications(services []models.K8sServiceInfo) ([]models.K8sServiceInfo, error) {
|
||||||
if containsServiceWithSelector(services) {
|
if containsServiceWithSelector(services) {
|
||||||
updatedServices := make([]models.K8sServiceInfo, len(services))
|
updatedServices := make([]models.K8sServiceInfo, len(services))
|
||||||
pods, replicaSets, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
pods, replicaSets, _, _, _, _, _, err := kcl.fetchAllPodsAndReplicaSets("", metav1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("an error occurred during the CombineServicesWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
|
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"
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
storagev1 "k8s.io/api/storage/v1"
|
storagev1 "k8s.io/api/storage/v1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
@ -264,7 +265,7 @@ func (kcl *KubeClient) updateVolumesWithOwningApplications(volumes *[]models.K8s
|
||||||
if pod.Spec.Volumes != nil {
|
if pod.Spec.Volumes != nil {
|
||||||
for _, podVolume := range pod.Spec.Volumes {
|
for _, podVolume := range pod.Spec.Volumes {
|
||||||
if podVolume.VolumeSource.PersistentVolumeClaim != nil && podVolume.VolumeSource.PersistentVolumeClaim.ClaimName == volume.PersistentVolumeClaim.Name && pod.Namespace == volume.PersistentVolumeClaim.Namespace {
|
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 {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to convert pod to application")
|
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)
|
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 clsx from 'clsx';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { Link } from '@@/Link';
|
||||||
|
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
name: ReactNode;
|
name: ReactNode;
|
||||||
icon: ReactNode;
|
icon?: ReactNode;
|
||||||
widget: ReactNode;
|
widget: ReactNode;
|
||||||
selectedTabParam: string;
|
selectedTabParam: string;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ export function WidgetTabs({ currentTabIndex, tabs }: Props) {
|
||||||
)}
|
)}
|
||||||
data-cy={`tab-${index}`}
|
data-cy={`tab-${index}`}
|
||||||
>
|
>
|
||||||
<Icon icon={icon} />
|
{icon && <Icon icon={icon} />}
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
@ -67,5 +67,15 @@ export function findSelectedTabIndex(
|
||||||
const currentTabIndex = tabs.findIndex(
|
const currentTabIndex = tabs.findIndex(
|
||||||
(tab) => tab.selectedTabParam === selectedTabParam
|
(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 { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
|
||||||
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models/appConstants';
|
import { KubernetesApplicationTypes } from '@/kubernetes/models/application/models/appConstants';
|
||||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||||
|
import { useIngresses } from '@/react/kubernetes/ingresses/queries';
|
||||||
|
|
||||||
import { TableSettingsMenu } from '@@/datatables';
|
import { TableSettingsMenu } from '@@/datatables';
|
||||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||||
|
@ -58,6 +59,8 @@ export function ApplicationsDatatable({
|
||||||
namespace: tableState.namespace,
|
namespace: tableState.namespace,
|
||||||
withDependencies: true,
|
withDependencies: true,
|
||||||
});
|
});
|
||||||
|
const ingressesQuery = useIngresses(environmentId);
|
||||||
|
const ingresses = ingressesQuery.data ?? [];
|
||||||
const applications = useApplicationsRowData(applicationsQuery.data);
|
const applications = useApplicationsRowData(applicationsQuery.data);
|
||||||
const filteredApplications = tableState.showSystemResources
|
const filteredApplications = tableState.showSystemResources
|
||||||
? applications
|
? applications
|
||||||
|
@ -69,6 +72,7 @@ export function ApplicationsDatatable({
|
||||||
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
||||||
environmentId,
|
environmentId,
|
||||||
stacks,
|
stacks,
|
||||||
|
ingresses,
|
||||||
reportStacks: false,
|
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';
|
import { AppType, DeploymentType } from '../../types';
|
||||||
|
|
||||||
export interface ApplicationRowData extends Application {
|
export interface ApplicationRowData extends Application {
|
||||||
|
@ -9,7 +12,7 @@ export interface Application {
|
||||||
Name: string;
|
Name: string;
|
||||||
Image: string;
|
Image: string;
|
||||||
Containers?: Array<unknown>;
|
Containers?: Array<unknown>;
|
||||||
Services?: Array<unknown>;
|
Services?: Array<Service>;
|
||||||
CreationDate: string;
|
CreationDate: string;
|
||||||
ApplicationOwner?: string;
|
ApplicationOwner?: string;
|
||||||
StackName?: string;
|
StackName?: string;
|
||||||
|
@ -48,6 +51,7 @@ export interface Application {
|
||||||
MemoryLimit?: number;
|
MemoryLimit?: number;
|
||||||
MemoryRequest?: number;
|
MemoryRequest?: number;
|
||||||
};
|
};
|
||||||
|
HorizontalPodAutoscaler?: HorizontalPodAutoscaler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConfigKind {
|
export enum ConfigKind {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
|
import { isSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace';
|
||||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||||
|
import { useIngresses } from '@/react/kubernetes/ingresses/queries';
|
||||||
|
|
||||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||||
import { TableSettingsMenu } from '@@/datatables';
|
import { TableSettingsMenu } from '@@/datatables';
|
||||||
|
@ -39,6 +40,8 @@ export function ApplicationsStacksDatatable({
|
||||||
namespace: tableState.namespace,
|
namespace: tableState.namespace,
|
||||||
withDependencies: true,
|
withDependencies: true,
|
||||||
});
|
});
|
||||||
|
const ingressesQuery = useIngresses(environmentId);
|
||||||
|
const ingresses = ingressesQuery.data ?? [];
|
||||||
const applications = applicationsQuery.data ?? [];
|
const applications = applicationsQuery.data ?? [];
|
||||||
const filteredApplications = tableState.showSystemResources
|
const filteredApplications = tableState.showSystemResources
|
||||||
? applications
|
? applications
|
||||||
|
@ -50,6 +53,7 @@ export function ApplicationsStacksDatatable({
|
||||||
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
const removeApplicationsMutation = useDeleteApplicationsMutation({
|
||||||
environmentId,
|
environmentId,
|
||||||
stacks,
|
stacks,
|
||||||
|
ingresses,
|
||||||
reportStacks: true,
|
reportStacks: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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 { withGlobalError } from '@/react-tools/react-query';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
@ -27,23 +30,15 @@ export function useApplicationHorizontalPodAutoscaler(
|
||||||
if (!app) {
|
if (!app) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const horizontalPodAutoscalers =
|
const horizontalPodAutoscalers =
|
||||||
await getNamespaceHorizontalPodAutoscalers(environmentId, namespace);
|
await getNamespaceHorizontalPodAutoscalers(environmentId, namespace);
|
||||||
const matchingHorizontalPodAutoscaler =
|
const matchingHorizontalPodAutoscaler =
|
||||||
horizontalPodAutoscalers.find((horizontalPodAutoscaler) => {
|
getMatchingHorizontalPodAutoscaler(
|
||||||
const scaleTargetRef = horizontalPodAutoscaler.spec?.scaleTargetRef;
|
horizontalPodAutoscalers,
|
||||||
if (scaleTargetRef) {
|
namespace,
|
||||||
const scaleTargetRefName = scaleTargetRef.name;
|
appName,
|
||||||
const scaleTargetRefKind = scaleTargetRef.kind;
|
app.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;
|
|
||||||
return matchingHorizontalPodAutoscaler;
|
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(
|
async function getNamespaceHorizontalPodAutoscalers(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
namespace: string
|
namespace: string
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v2';
|
||||||
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
@ -10,23 +11,34 @@ import { pluralize } from '@/portainer/helpers/strings';
|
||||||
import { parseKubernetesAxiosError } from '../../axiosError';
|
import { parseKubernetesAxiosError } from '../../axiosError';
|
||||||
import { ApplicationRowData } from '../ListView/ApplicationsDatatable/types';
|
import { ApplicationRowData } from '../ListView/ApplicationsDatatable/types';
|
||||||
import { Stack } from '../ListView/ApplicationsStacksDatatable/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';
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
export function useDeleteApplicationsMutation({
|
export function useDeleteApplicationsMutation({
|
||||||
environmentId,
|
environmentId,
|
||||||
stacks,
|
stacks,
|
||||||
|
ingresses,
|
||||||
reportStacks,
|
reportStacks,
|
||||||
}: {
|
}: {
|
||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
stacks: Stack[];
|
stacks: Stack[];
|
||||||
|
ingresses: Ingress[];
|
||||||
reportStacks?: boolean;
|
reportStacks?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (applications: ApplicationRowData[]) =>
|
mutationFn: (applications: ApplicationRowData[]) =>
|
||||||
deleteApplications(applications, stacks, environmentId),
|
deleteApplications(applications, stacks, ingresses, environmentId),
|
||||||
onSuccess: ({ settledAppDeletions, settledStackDeletions }) => {
|
// apps and stacks deletions results (success and error) are handled here
|
||||||
|
onSuccess: ({
|
||||||
|
settledAppDeletions,
|
||||||
|
settledStackDeletions,
|
||||||
|
settledIngressUpdates,
|
||||||
|
settledHpaDeletions,
|
||||||
|
}) => {
|
||||||
// one error notification per rejected item
|
// one error notification per rejected item
|
||||||
settledAppDeletions.rejectedItems.forEach(({ item, reason }) => {
|
settledAppDeletions.rejectedItems.forEach(({ item, reason }) => {
|
||||||
notifyError(
|
notifyError(
|
||||||
|
@ -37,6 +49,18 @@ export function useDeleteApplicationsMutation({
|
||||||
settledStackDeletions.rejectedItems.forEach(({ item, reason }) => {
|
settledStackDeletions.rejectedItems.forEach(({ item, reason }) => {
|
||||||
notifyError(`Failed to remove stack '${item.Name}'`, new Error(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
|
// one success notification for all fulfilled items
|
||||||
if (settledAppDeletions.fulfilledItems.length && !reportStacks) {
|
if (settledAppDeletions.fulfilledItems.length && !reportStacks) {
|
||||||
|
@ -59,8 +83,13 @@ export function useDeleteApplicationsMutation({
|
||||||
.join(', ')
|
.join(', ')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// dont notify successful ingress updates to avoid notification spam
|
||||||
queryClient.invalidateQueries(queryKeys.applications(environmentId));
|
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'),
|
...withGlobalError('Unable to remove applications'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -68,6 +97,7 @@ export function useDeleteApplicationsMutation({
|
||||||
async function deleteApplications(
|
async function deleteApplications(
|
||||||
applications: ApplicationRowData[],
|
applications: ApplicationRowData[],
|
||||||
stacks: Stack[],
|
stacks: Stack[],
|
||||||
|
ingresses: Ingress[],
|
||||||
environmentId: EnvironmentId
|
environmentId: EnvironmentId
|
||||||
) {
|
) {
|
||||||
const settledAppDeletions = await getAllSettledItems(
|
const settledAppDeletions = await getAllSettledItems(
|
||||||
|
@ -84,7 +114,83 @@ async function deleteApplications(
|
||||||
(stack) => deleteStack(stack, environmentId)
|
(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) {
|
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
|
// mutate the stacks array to remove the application
|
||||||
function removeApplicationFromStack(
|
function removeApplicationFromStack(
|
||||||
application: ApplicationRowData,
|
application: ApplicationRowData,
|
||||||
|
|
|
@ -68,7 +68,7 @@ export async function updateIngress(
|
||||||
ingress: Ingress
|
ingress: Ingress
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return await axios.put(buildUrl(environmentId, ingress.Namespace), ingress);
|
await axios.put(buildUrl(environmentId, ingress.Namespace), ingress);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw parseAxiosError(e as Error, 'Unable to update an ingress');
|
throw parseAxiosError(e as Error, 'Unable to update an ingress');
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@ import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
export function useDeleteServiceAccountsMutation(environmentId: EnvironmentId) {
|
export function useDeleteServiceAccountsMutation(environmentId: EnvironmentId) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation(deleteServices, {
|
return useMutation(deleteServiceAccounts, {
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
queryClient.invalidateQueries(queryKeys.list(environmentId)),
|
queryClient.invalidateQueries(queryKeys.list(environmentId)),
|
||||||
...withGlobalError('Unable to delete service accounts'),
|
...withGlobalError('Unable to delete service accounts'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteServices({
|
export async function deleteServiceAccounts({
|
||||||
environmentId,
|
environmentId,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
|
|
Loading…
Reference in New Issue