Skip to content

Commit

Permalink
Apps lists page optimized to query per cluster (#7211)
Browse files Browse the repository at this point in the history
* Apps list optimization

* Apps list optimized to query per cluster

* fixed lint

fixed tests

fixed check of apps
  • Loading branch information
hhovsepy committed Mar 25, 2024
1 parent 076f52e commit 76c4ec1
Show file tree
Hide file tree
Showing 22 changed files with 308 additions and 123 deletions.
123 changes: 122 additions & 1 deletion business/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package business

import (
"context"
"fmt"
"sort"
"strings"
"sync"
Expand Down Expand Up @@ -58,6 +59,126 @@ func buildFinalLabels(m map[string][]string) map[string]string {
return consolidated
}

// GetClusterAppList is the API handler to fetch the list of applications in a given namespace and cluster
func (in *AppService) GetClusterAppList(ctx context.Context, criteria AppCriteria) (models.ClusterApps, error) {
var end observability.EndFunc
ctx, end = observability.StartSpan(ctx, "GetClusterAppList",
observability.Attribute("package", "business"),
observability.Attribute("namespace", criteria.Namespace),
observability.Attribute("cluster", criteria.Cluster),
observability.Attribute("includeHealth", criteria.IncludeHealth),
observability.Attribute("includeIstioResources", criteria.IncludeIstioResources),
observability.Attribute("rateInterval", criteria.RateInterval),
observability.Attribute("queryTime", criteria.QueryTime),
)
defer end()

appList := &models.ClusterApps{
Apps: []models.AppListItem{},
}

namespace := criteria.Namespace
cluster := criteria.Cluster

if _, ok := in.userClients[cluster]; !ok {
return *appList, fmt.Errorf("Cluster [%s] is not found or is not accessible for Kiali", cluster)
}

if _, err := in.businessLayer.Namespace.GetClusterNamespace(ctx, namespace, cluster); err != nil {
return *appList, err
}

allApps, err := in.businessLayer.App.fetchNamespaceApps(ctx, namespace, cluster, "")
if err != nil {
log.Errorf("Error fetching Applications for cluster %s per namespace %s: %s", cluster, namespace, err)
return *appList, err
}

icCriteria := IstioConfigCriteria{
IncludeAuthorizationPolicies: true,
IncludeDestinationRules: true,
IncludeEnvoyFilters: true,
IncludeGateways: true,
IncludePeerAuthentications: true,
IncludeRequestAuthentications: true,
IncludeSidecars: true,
IncludeVirtualServices: true,
}
istioConfigList := &models.IstioConfigList{}

if criteria.IncludeIstioResources {
istioConfigList, err = in.businessLayer.IstioConfig.GetIstioConfigListForNamespace(ctx, cluster, namespace, icCriteria)
if err != nil {
log.Errorf("Error fetching Istio Config for Cluster %s per namespace %s: %s", cluster, namespace, err)
return *appList, err
}
}

for keyApp, valueApp := range allApps {
appItem := &models.AppListItem{
Name: keyApp,
IstioSidecar: true,
Health: models.EmptyAppHealth(),
}
applabels := make(map[string][]string)
svcReferences := make([]*models.IstioValidationKey, 0)
for _, srv := range valueApp.Services {
joinMap(applabels, srv.Labels)
if criteria.IncludeIstioResources {
vsFiltered := kubernetes.FilterVirtualServicesByService(istioConfigList.VirtualServices, srv.Namespace, srv.Name)
for _, v := range vsFiltered {
ref := models.BuildKey(v.Kind, v.Name, v.Namespace)
svcReferences = append(svcReferences, &ref)
}
drFiltered := kubernetes.FilterDestinationRulesByService(istioConfigList.DestinationRules, srv.Namespace, srv.Name)
for _, d := range drFiltered {
ref := models.BuildKey(d.Kind, d.Name, d.Namespace)
svcReferences = append(svcReferences, &ref)
}
gwFiltered := kubernetes.FilterGatewaysByVirtualServices(istioConfigList.Gateways, istioConfigList.VirtualServices)
for _, g := range gwFiltered {
ref := models.BuildKey(g.Kind, g.Name, g.Namespace)
svcReferences = append(svcReferences, &ref)
}

}

}

wkdReferences := make([]*models.IstioValidationKey, 0)
for _, wrk := range valueApp.Workloads {
joinMap(applabels, wrk.Labels)
if criteria.IncludeIstioResources {
wSelector := labels.Set(wrk.Labels).AsSelector().String()
wkdReferences = append(wkdReferences, FilterWorkloadReferences(wSelector, *istioConfigList)...)
}
}
appItem.Labels = buildFinalLabels(applabels)
appItem.IstioReferences = FilterUniqueIstioReferences(append(svcReferences, wkdReferences...))

for _, w := range valueApp.Workloads {
if appItem.IstioSidecar = w.IstioSidecar; !appItem.IstioSidecar {
break
}
}
for _, w := range valueApp.Workloads {
if appItem.IstioAmbient = w.HasIstioAmbient(); !appItem.IstioAmbient {
break
}
}
if criteria.IncludeHealth {
appItem.Health, err = in.businessLayer.Health.GetAppHealth(ctx, criteria.Namespace, valueApp.cluster, appItem.Name, criteria.RateInterval, criteria.QueryTime, valueApp)
if err != nil {
log.Errorf("Error fetching Health in namespace %s for app %s: %s", criteria.Namespace, appItem.Name, err)
}
}
appItem.Namespace = models.Namespace{Cluster: cluster, Name: namespace}
appList.Apps = append(appList.Apps, *appItem)
}

return *appList, nil
}

// GetAppList is the API handler to fetch the list of applications in a given namespace
func (in *AppService) GetAppList(ctx context.Context, criteria AppCriteria) (models.AppList, error) {
var end observability.EndFunc
Expand Down Expand Up @@ -206,7 +327,7 @@ func (in *AppService) GetAppList(ctx context.Context, criteria AppCriteria) (mod
log.Errorf("Error fetching Health in namespace %s for app %s: %s", criteria.Namespace, appItem.Name, err)
}
}
appItem.Cluster = valueApp.cluster
appItem.Namespace = models.Namespace{Cluster: valueApp.cluster, Name: criteria.Namespace}
appList.Apps = append(appList.Apps, *appItem)
}
}
Expand Down
14 changes: 6 additions & 8 deletions business/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,13 @@ func TestGetAppListFromDeployments(t *testing.T) {

svc := setupAppService(mockClientFactory.Clients)

criteria := AppCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
appList, err := svc.GetAppList(context.TODO(), criteria)
criteria := AppCriteria{Cluster: conf.KubernetesConfig.ClusterName, Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
appList, err := svc.GetClusterAppList(context.TODO(), criteria)
require.NoError(err)

assert.Equal("Namespace", appList.Namespace.Name)

assert.Equal(1, len(appList.Apps))
assert.Equal("httpbin", appList.Apps[0].Name)
assert.Equal("Namespace", appList.Apps[0].Namespace.Name)
}

func TestGetAppFromDeployments(t *testing.T) {
Expand Down Expand Up @@ -127,13 +126,12 @@ func TestGetAppListFromReplicaSets(t *testing.T) {

svc := setupAppService(mockClientFactory.Clients)

criteria := AppCriteria{Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
appList, _ := svc.GetAppList(context.TODO(), criteria)

assert.Equal("Namespace", appList.Namespace.Name)
criteria := AppCriteria{Cluster: conf.KubernetesConfig.ClusterName, Namespace: "Namespace", IncludeIstioResources: false, IncludeHealth: false}
appList, _ := svc.GetClusterAppList(context.TODO(), criteria)

assert.Equal(1, len(appList.Apps))
assert.Equal("httpbin", appList.Apps[0].Name)
assert.Equal("Namespace", appList.Apps[0].Namespace.Name)
}

func TestGetAppFromReplicaSets(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/cypress/integration/common/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ Then('user sees Details information for Apps', () => {
});
});

Then('user only sees the apps with the {string} name in the {string} namespace', (name: string, ns: string) => {
Then('user only sees the apps with the {string} name', (name: string) => {
let count: number;

cy.request('GET', `/api/namespaces/${ns}/apps`).should(response => {
cy.request('GET', `/api/clusters/apps`).should(response => {
count = response.body.applications.filter(item => item.name.includes(name)).length;
});

Expand Down
6 changes: 3 additions & 3 deletions frontend/cypress/integration/featureFiles/apps.feature
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Feature: Kiali Apps List page
Scenario: Filter Applications table by Label
When the user filters by "Label" for "app=reviews"
Then user sees "reviews"
And user only sees the apps with the "reviews" name in the "bookinfo" namespace
And user only sees the apps with the "reviews" name

@bookinfo-app
Scenario: The healthy status of a logical mesh application is reported in the list of applications
Expand Down Expand Up @@ -103,7 +103,7 @@ Feature: Kiali Apps List page

@multi-cluster
Scenario: The column related to cluster name should be visible
Then the "Cluster" column "appears"
Then the "Cluster" column "appears"
And an entry for "east" cluster should be in the table
And an entry for "west" cluster should be in the table

Expand All @@ -118,4 +118,4 @@ Feature: Kiali Apps List page
Then the list is sorted by column "Cluster" in "ascending" order
When user sorts the list by column "Cluster" in "descending" order
Then the list is sorted by column "Cluster" in "descending" order

7 changes: 5 additions & 2 deletions frontend/src/components/VirtualList/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as React from 'react';
import { StatefulFiltersComponent } from '../Filters/StatefulFilters';
import { PFBadges, PFBadgeType } from '../../components/Pf/PfBadges';
import { isGateway, isWaypoint } from '../../helpers/LabelFilterHelper';
import { getNamespace } from '../../utils/Common';

export type SortResource = AppListItem | WorkloadListItem | ServiceListItem;
export type TResource = SortResource | IstioConfigItem;
Expand All @@ -29,11 +30,13 @@ export const hasHealth = (r: RenderResource): r is SortResource => {
};

export const hasMissingSidecar = (r: SortResource): boolean => {
return !isIstioNamespace(r.namespace) && !r.istioSidecar && !isGateway(r.labels) && !isWaypoint(r.labels);
return (
!isIstioNamespace(getNamespace(r.namespace)) && !r.istioSidecar && !isGateway(r.labels) && !isWaypoint(r.labels)
);
};

export const noAmbientLabels = (r: SortResource): boolean => {
return !isIstioNamespace(r.namespace) && !r.istioAmbient;
return !isIstioNamespace(getNamespace(r.namespace)) && !r.istioAmbient;
};

export type ResourceType<R extends RenderResource> = {
Expand Down
34 changes: 19 additions & 15 deletions frontend/src/components/VirtualList/Renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ import { NamespaceStatuses } from 'pages/Overview/NamespaceStatuses';
import { isGateway, isWaypoint } from '../../helpers/LabelFilterHelper';
import { KialiIcon } from '../../config/KialiIcon';
import { Td } from '@patternfly/react-table';
import { getNamespace } from '../../utils/Common';

// Links

const getLink = (item: TResource, config: Resource, query?: string): string => {
let url = config.name === 'istio' ? getIstioLink(item) : `/namespaces/${item.namespace}/${config.name}/${item.name}`;
let url =
config.name === 'istio'
? getIstioLink(item)
: `/namespaces/${getNamespace(item.namespace)}/${config.name}/${item.name}`;

if (item.cluster && isMultiCluster && !url.includes('cluster')) {
if (url.includes('?')) {
Expand All @@ -65,7 +69,7 @@ const getLink = (item: TResource, config: Resource, query?: string): string => {
const getIstioLink = (item: TResource): string => {
const type = item['type'];

return GetIstioObjectUrl(item.name, item.namespace, type, item.cluster);
return GetIstioObjectUrl(item.name, getNamespace(item.namespace), type, item.cluster);
};

// Cells
Expand Down Expand Up @@ -94,20 +98,20 @@ export const details: Renderer<AppListItem | WorkloadListItem | ServiceListItem>
<Td
role="gridcell"
dataLabel="Details"
key={`VirtuaItem_Details_${item.namespace}_${item.name}`}
key={`VirtuaItem_Details_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle', whiteSpace: 'nowrap' }}
>
<ul>
{hasMissingAP && (
<li>
<MissingAuthPolicy namespace={item.namespace} />
<MissingAuthPolicy namespace={getNamespace(item.namespace)} />
</li>
)}

{((hasMissingSC && hasMissingA && serverConfig.ambientEnabled) ||
(!serverConfig.ambientEnabled && hasMissingSC)) && (
<li>
<MissingSidecar namespace={item.namespace} isGateway={isGateway(item.labels)} />
<MissingSidecar namespace={getNamespace(item.namespace)} isGateway={isGateway(item.labels)} />
</li>
)}

Expand Down Expand Up @@ -257,7 +261,7 @@ export const nsItem: Renderer<NamespaceInfo> = (ns: NamespaceInfo, _config: Reso
};

export const item: Renderer<TResource> = (item: TResource, config: Resource, badge: PFBadgeType) => {
const key = `link_definition_${config.name}_${item.namespace}_${item.name}`;
const key = `link_definition_${config.name}_${getNamespace(item.namespace)}_${item.name}`;
let serviceBadge = badge;

if (item['serviceRegistry']) {
Expand All @@ -275,7 +279,7 @@ export const item: Renderer<TResource> = (item: TResource, config: Resource, bad
<Td
role="gridcell"
dataLabel="Name"
key={`VirtuaItem_Item_${item.namespace}_${item.name}`}
key={`VirtuaItem_Item_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle' }}
>
<PFBadge badge={serviceBadge} position={TooltipPosition.top} />
Expand Down Expand Up @@ -306,11 +310,11 @@ export const namespace: Renderer<TResource> = (item: TResource) => {
<Td
role="gridcell"
dataLabel="Namespace"
key={`VirtuaItem_Namespace_${item.namespace}_${item.name}`}
key={`VirtuaItem_Namespace_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle' }}
>
<PFBadge badge={PFBadges.Namespace} position={TooltipPosition.top} />
{item.namespace}
{getNamespace(item.namespace)}
</Td>
);
};
Expand Down Expand Up @@ -355,7 +359,7 @@ export const labels: Renderer<SortResource | NamespaceInfo> = (
<Td
role="gridcell"
dataLabel="Labels"
key={`VirtuaItem_Labels_${'namespace' in item && `${item.namespace}_`}${item.name}`}
key={`VirtuaItem_Labels_${'namespace' in item && `${getNamespace(item.namespace)}_`}${item.name}`}
style={{ verticalAlign: 'middle', paddingBottom: '0.25rem' }}
>
{item.labels &&
Expand Down Expand Up @@ -415,7 +419,7 @@ export const health: Renderer<TResource> = (item: TResource, __: Resource, _: PF
<Td
role="gridcell"
dataLabel="Health"
key={`VirtuaItem_Health_${item.namespace}_${item.name}`}
key={`VirtuaItem_Health_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle' }}
>
{health && <HealthIndicator id={item.name} health={health} />}
Expand All @@ -428,7 +432,7 @@ export const workloadType: Renderer<WorkloadListItem> = (item: WorkloadListItem)
<Td
role="gridcell"
dataLabel="Type"
key={`VirtuaItem_WorkloadType_${item.namespace}_${item.name}`}
key={`VirtuaItem_WorkloadType_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle' }}
>
{item.type}
Expand All @@ -444,7 +448,7 @@ export const istioType: Renderer<IstioConfigItem> = (item: IstioConfigItem) => {
<Td
role="gridcell"
dataLabel="Type"
key={`VirtuaItem_IstioType_${item.namespace}_${item.name}`}
key={`VirtuaItem_IstioType_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle' }}
>
{object.name}
Expand All @@ -461,7 +465,7 @@ export const istioConfiguration: Renderer<IstioConfigItem> = (item: IstioConfigI
<Td
role="gridcell"
dataLabel="Configuration"
key={`VirtuaItem_Conf_${item.namespace}_${item.name}`}
key={`VirtuaItem_Conf_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle' }}
>
{validation ? (
Expand All @@ -487,7 +491,7 @@ export const serviceConfiguration: Renderer<ServiceListItem> = (item: ServiceLis
<Td
role="gridcell"
dataLabel="Configuration"
key={`VirtuaItem_Conf_${item.namespace}_${item.name}`}
key={`VirtuaItem_Conf_${getNamespace(item.namespace)}_${item.name}`}
style={{ verticalAlign: 'middle' }}
>
{validation ? (
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/VirtualList/VirtualItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Health } from '../../types/Health';
import { StatefulFiltersComponent } from '../Filters/StatefulFilters';
import { actionRenderer } from './Renderers';
import { CSSProperties } from 'react';
import { getNamespace } from '../../utils/Common';

type VirtualItemProps = {
action?: JSX.Element;
Expand Down Expand Up @@ -59,7 +60,7 @@ export class VirtualItem extends React.Component<VirtualItemProps, VirtualItemSt
render(): React.ReactNode {
const { style, className, item } = this.props;
const cluster = item.cluster ? `_Cluster${item.cluster}` : '';
const namespace = 'namespace' in item ? `_Ns${item.namespace}` : '';
const namespace = 'namespace' in item ? `_Ns${getNamespace(item.namespace)}` : '';
const type = 'type' in item ? `_${item.type}` : '';
// End result looks like: VirtualItem_Clusterwest_Nsbookinfo_gateway_bookinfo-gateway

Expand Down
Loading

0 comments on commit 76c4ec1

Please sign in to comment.