diff --git a/workspaces/konflux/.changeset/better-melons-shave.md b/workspaces/konflux/.changeset/better-melons-shave.md new file mode 100644 index 0000000000..ba53f707d7 --- /dev/null +++ b/workspaces/konflux/.changeset/better-melons-shave.md @@ -0,0 +1,7 @@ +--- +'@red-hat-developer-hub/backstage-plugin-konflux-backend': patch +--- + +- Fix: replace unfiltered catalog scan in getRelatedEntities with targeted lookup using hasPart relations and getEntitiesByRefs. +- Perf: cache K8s API clients per cluster and catalog lookups (30s TTL) to avoid redundant work across parallel requests. +- Perf: strip metadata.managedFields from K8s and Kubearchive responses to reduce payload size. diff --git a/workspaces/konflux/.changeset/fifty-moose-appear.md b/workspaces/konflux/.changeset/fifty-moose-appear.md new file mode 100644 index 0000000000..4fc9d7f661 --- /dev/null +++ b/workspaces/konflux/.changeset/fifty-moose-appear.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-konflux': patch +--- + +- Increase react-query staleTime from 30s to 5min and disable refetchOnWindowFocus to reduce unnecessary re-fetches. diff --git a/workspaces/konflux/plugins/konflux-backend/src/helpers/__tests__/config.test.ts b/workspaces/konflux/plugins/konflux-backend/src/helpers/__tests__/config.test.ts index bc90a46d00..5fd9a041f2 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/helpers/__tests__/config.test.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/helpers/__tests__/config.test.ts @@ -113,6 +113,7 @@ describe('config', () => { mockCatalog = { getEntities: jest.fn(), getEntityByRef: jest.fn(), + getEntitiesByRefs: jest.fn().mockResolvedValue({ items: [] }), } as unknown as CatalogService; mockCredentials = {} as BackstageCredentials; @@ -269,8 +270,13 @@ describe('config', () => { mockParseEntityKonfluxConfig.mockReturnValue(clusterConfigs); - const entity = createMockEntity('test-entity'); - (mockCatalog.getEntities as jest.Mock).mockResolvedValue({ + const entity = createMockEntity('test-entity', undefined, [ + { + type: 'hasPart', + targetRef: 'component:default/subcomponent1', + }, + ]); + (mockCatalog.getEntitiesByRefs as jest.Mock).mockResolvedValue({ items: [subcomponent1], }); @@ -502,7 +508,6 @@ describe('config', () => { authProvider: 'serviceAccount', }; - const entity = createMockEntity('test-entity'); const subcomponent1 = createMockEntity('sub1', {}, [ { type: 'partOf', targetRef: 'component:default/test-entity' }, ]); @@ -510,6 +515,11 @@ describe('config', () => { { type: 'partOf', targetRef: 'component:default/test-entity' }, ]); + const entity = createMockEntity('test-entity', undefined, [ + { type: 'hasPart', targetRef: 'component:default/sub1' }, + { type: 'hasPart', targetRef: 'component:default/sub2' }, + ]); + mockParseSubcomponentClusterConfigurations.mockReturnValue([ createMockSubcomponentClusterConfig('sub1', 'cluster1', 'ns1', [ 'app1', @@ -519,7 +529,7 @@ describe('config', () => { ]), ]); - (mockCatalog.getEntities as jest.Mock).mockResolvedValue({ + (mockCatalog.getEntitiesByRefs as jest.Mock).mockResolvedValue({ items: [subcomponent1, subcomponent2], }); diff --git a/workspaces/konflux/plugins/konflux-backend/src/helpers/client-factory.ts b/workspaces/konflux/plugins/konflux-backend/src/helpers/client-factory.ts index 95bc972a46..ff03fa25a2 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/helpers/client-factory.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/helpers/client-factory.ts @@ -14,10 +14,21 @@ * limitations under the License. */ import { KonfluxConfig } from '@red-hat-developer-hub/backstage-plugin-konflux-common'; -import type { KubeConfig } from '@kubernetes/client-node'; +import type { KubeConfig, CustomObjectsApi } from '@kubernetes/client-node'; import { KonfluxLogger } from './logger'; import { getKubeClient } from './kube-client'; +/** + * Cache for CustomObjectsApi clients keyed by "cluster:apiUrl". + * + * Per-request auth headers are injected via middleware in the callers, + * so cached clients are safe to share across users. + * + * Caching avoids creating a new KubeConfig, CustomObjectsApi, and TLS + * connection on every API call. + */ +const clientCache = new Map(); + /** * Creates a KubeConfig for connecting to a Kubernetes cluster. * @@ -94,3 +105,54 @@ export const createKubeConfig = async ( return null; } }; + +/** + * Returns a cached CustomObjectsApi client for the given cluster, creating + * one on first access. The client is safe to share across requests because + * auth headers are injected per-request via middleware. + * + * @param konfluxConfig - Konflux configuration + * @param cluster - Cluster name + * @param konfluxLogger - Logger instance + * @param useKubearchiveUrl - If true, targets the kubearchive API URL + * @returns CustomObjectsApi instance or null if client creation fails + */ +export const getOrCreateClient = async ( + konfluxConfig: KonfluxConfig | undefined, + cluster: string, + konfluxLogger: KonfluxLogger, + useKubearchiveUrl = false, +): Promise => { + if (!konfluxConfig) { + return null; + } + + const clusterConfig = konfluxConfig.clusters?.[cluster]; + const apiUrl = useKubearchiveUrl + ? clusterConfig?.kubearchiveApiUrl + : clusterConfig?.apiUrl; + + const cacheKey = `${cluster}:${apiUrl}`; + + const cached = clientCache.get(cacheKey); + if (cached) { + return cached; + } + + const kc = await createKubeConfig( + konfluxConfig, + cluster, + konfluxLogger, + clusterConfig?.serviceAccountToken, + useKubearchiveUrl, + ); + + if (!kc) { + return null; + } + + const { CustomObjectsApi } = await getKubeClient(); + const client = kc.makeApiClient(CustomObjectsApi); + clientCache.set(cacheKey, client); + return client; +}; diff --git a/workspaces/konflux/plugins/konflux-backend/src/helpers/config.ts b/workspaces/konflux/plugins/konflux-backend/src/helpers/config.ts index 671b8a042d..5d76ffe0d0 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/helpers/config.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/helpers/config.ts @@ -28,7 +28,7 @@ import { import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { CatalogService } from '@backstage/plugin-catalog-node'; import { KonfluxLogger } from './logger'; @@ -37,8 +37,9 @@ import { KonfluxLogger } from './logger'; * * In Konflux plugin, a "subcomponent" refers to a Backstage Component that has a * `subcomponentOf` relationship to the current Component being viewed. - * Backstage automatically creates a `partOf` relation from the subcomponent - * entity to the parent entity, which is what we query for. + * Backstage automatically creates a `hasPart` relation on the parent entity + * for each subcomponent, so we read those refs and batch-fetch the entities + * instead of scanning the entire catalog. * * @param entity - The main/parent entity to find related entities for * @param credentials - Backstage credentials for authentication @@ -53,20 +54,22 @@ const getRelatedEntities = async ( ): Promise => { try { if (catalog) { - const entityRef = stringifyEntityRef(entity); + const hasPartRefs = (entity.relations || []) + .filter(rel => rel.type === 'hasPart') + .map(rel => rel.targetRef); - const allEntitiesForFiltering = await catalog.getEntities( - {}, + if (hasPartRefs.length === 0) { + return []; + } + + const response = await catalog.getEntitiesByRefs( + { entityRefs: hasPartRefs }, { credentials }, ); - const filteredRelatedEntities = allEntitiesForFiltering.items.filter( - item => - item.relations?.some( - rel => rel.type === 'partOf' && rel.targetRef === entityRef, - ), - ); - return filteredRelatedEntities; + return response.items.filter( + (item): item is Entity => item !== undefined, + ); } return null; } catch (error) { diff --git a/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/kubearchive-service.test.ts b/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/kubearchive-service.test.ts index 4d98df318b..f8ac4fcfdf 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/kubearchive-service.test.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/kubearchive-service.test.ts @@ -16,13 +16,13 @@ import { KubearchiveService } from '../kubearchive-service'; import { LoggerService } from '@backstage/backend-plugin-api'; -import type { KubeConfig, CustomObjectsApi } from '@kubernetes/client-node'; +import type { CustomObjectsApi } from '@kubernetes/client-node'; import { KonfluxConfig, K8sResourceCommonWithClusterInfo, } from '@red-hat-developer-hub/backstage-plugin-konflux-common'; import { KonfluxLogger } from '../../helpers/logger'; -import { createKubeConfig } from '../../helpers/client-factory'; +import { getOrCreateClient } from '../../helpers/client-factory'; import { getKubeClient } from '../../helpers/kube-client'; jest.mock('../../helpers/logger'); @@ -32,7 +32,6 @@ jest.mock('../../helpers/kube-client'); describe('KubearchiveService', () => { let mockLogger: jest.Mocked; let mockKonfluxLogger: jest.Mocked; - let mockKubeConfig: jest.Mocked; let mockCustomObjectsApi: jest.Mocked; let service: KubearchiveService; @@ -104,18 +103,13 @@ describe('KubearchiveService', () => { listNamespacedCustomObjectWithHttpInfo: jest.fn(), } as unknown as jest.Mocked; - mockKubeConfig = { - makeApiClient: jest.fn().mockReturnValue(mockCustomObjectsApi), - loadFromOptions: jest.fn(), - } as unknown as jest.Mocked; - ( KonfluxLogger as jest.MockedClass ).mockImplementation(() => mockKonfluxLogger); ( - createKubeConfig as jest.MockedFunction - ).mockResolvedValue(mockKubeConfig); + getOrCreateClient as jest.MockedFunction + ).mockResolvedValue(mockCustomObjectsApi); class MockObservable { constructor(public value: T) {} @@ -545,10 +539,10 @@ describe('KubearchiveService', () => { }); ( - createKubeConfig as jest.MockedFunction + getOrCreateClient as jest.MockedFunction ).mockResolvedValue(null); - // provide OIDC token so it passes token check and reaches createKubeConfig + // provide OIDC token so it passes token check and reaches getOrCreateClient await expect( service.fetchResources({ konfluxConfig, @@ -564,7 +558,7 @@ describe('KubearchiveService', () => { ).rejects.toThrow(`Cluster '${cluster}' not found`); expect(mockKonfluxLogger.error).toHaveBeenCalledWith( - 'Failed to create KubeConfig - cluster not found', + 'Failed to create API client - cluster not found', undefined, expect.objectContaining({ cluster, @@ -586,7 +580,7 @@ describe('KubearchiveService', () => { }); ( - createKubeConfig as jest.MockedFunction + getOrCreateClient as jest.MockedFunction ).mockResolvedValue(null); await expect( @@ -602,7 +596,7 @@ describe('KubearchiveService', () => { ).rejects.toThrow(`Cluster '${cluster}' not found`); expect(mockKonfluxLogger.error).toHaveBeenCalledWith( - 'Failed to create KubeConfig - cluster not found', + 'Failed to create API client - cluster not found', undefined, expect.objectContaining({ cluster, diff --git a/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/resource-fetcher.test.ts b/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/resource-fetcher.test.ts index 92aceb9fdb..74c3d3660d 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/resource-fetcher.test.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/services/__tests__/resource-fetcher.test.ts @@ -20,10 +20,10 @@ import { FetchOptions, } from '../resource-fetcher'; import { LoggerService } from '@backstage/backend-plugin-api'; -import type { KubeConfig, CustomObjectsApi } from '@kubernetes/client-node'; +import type { CustomObjectsApi } from '@kubernetes/client-node'; import { K8sResourceCommonWithClusterInfo } from '@red-hat-developer-hub/backstage-plugin-konflux-common'; import { KubearchiveService } from '../kubearchive-service'; -import { createKubeConfig } from '../../helpers/client-factory'; +import { getOrCreateClient } from '../../helpers/client-factory'; import { KonfluxLogger } from '../../helpers/logger'; import { getKubeClient } from '../../helpers/kube-client'; @@ -36,7 +36,6 @@ describe('ResourceFetcherService', () => { let mockLogger: jest.Mocked; let mockKonfluxLogger: jest.Mocked; let mockKubearchiveService: jest.Mocked; - let mockKubeConfig: jest.Mocked; let mockCustomObjectsApi: jest.Mocked; let service: ResourceFetcherService; @@ -122,10 +121,6 @@ describe('ResourceFetcherService', () => { listNamespacedCustomObjectWithHttpInfo: jest.fn(), } as unknown as jest.Mocked; - mockKubeConfig = { - makeApiClient: jest.fn().mockReturnValue(mockCustomObjectsApi), - } as unknown as jest.Mocked; - ( KonfluxLogger as jest.MockedClass ).mockImplementation(() => mockKonfluxLogger); @@ -135,8 +130,8 @@ describe('ResourceFetcherService', () => { ).mockImplementation(() => mockKubearchiveService); ( - createKubeConfig as jest.MockedFunction - ).mockResolvedValue(mockKubeConfig); + getOrCreateClient as jest.MockedFunction + ).mockResolvedValue(mockCustomObjectsApi); class MockObservable { constructor(public value: T) {} @@ -174,11 +169,10 @@ describe('ResourceFetcherService', () => { expect(result.items).toEqual(mockItems); expect(result.continueToken).toBeUndefined(); - expect(createKubeConfig).toHaveBeenCalledWith( + expect(getOrCreateClient).toHaveBeenCalledWith( context.konfluxConfig, 'cluster1', mockKonfluxLogger, - 'service-token-123', ); expect( mockCustomObjectsApi.listNamespacedCustomObjectWithHttpInfo, @@ -301,11 +295,10 @@ describe('ResourceFetcherService', () => { await service.fetchFromKubernetes(context); - expect(createKubeConfig).toHaveBeenCalledWith( - expect.anything(), + expect(getOrCreateClient).toHaveBeenCalledWith( expect.anything(), + 'cluster1', expect.anything(), - 'oidc-token-456', ); expect(mockKonfluxLogger.debug).toHaveBeenCalledWith( 'Using OIDC token for authentication', @@ -423,7 +416,7 @@ describe('ResourceFetcherService', () => { it('should throw error when cluster not found', async () => { const context = createMockFetchContext(); ( - createKubeConfig as jest.MockedFunction + getOrCreateClient as jest.MockedFunction ).mockResolvedValue(null); await expect(service.fetchFromKubernetes(context)).rejects.toThrow( diff --git a/workspaces/konflux/plugins/konflux-backend/src/services/konflux-service.ts b/workspaces/konflux/plugins/konflux-backend/src/services/konflux-service.ts index 559b034c5a..2cd7b25a4c 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/services/konflux-service.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/services/konflux-service.ts @@ -25,11 +25,14 @@ import { SubcomponentClusterConfig, Filters, K8sResourceCommonWithClusterInfo, + KonfluxConfig, PAGINATION_CONFIG, ClusterError, GroupVersionKind, } from '@red-hat-developer-hub/backstage-plugin-konflux-common'; +import { Entity } from '@backstage/catalog-model'; + import { createResourceWithClusterInfo, filterResourcesByApplication, @@ -50,6 +53,36 @@ import { KonfluxLogger } from '../helpers/logger'; import { validateUserEmailForImpersonation } from '../helpers/validation'; import { extractKubernetesErrorDetails } from '../helpers/error-extraction'; +const CATALOG_CACHE_TTL_MS = 30_000; // 30 seconds + +interface CacheEntry { + data: T; + expiry: number; +} + +/** + * Simple TTL cache for catalog lookups. Prevents duplicate catalog calls + * when multiple resource requests arrive in parallel for the same entity. + */ +class CatalogCache { + private readonly cache = new Map>(); + + get(key: string): T | undefined { + const entry = this.cache.get(key); + if (entry && Date.now() < entry.expiry) { + return entry.data as T; + } + if (entry) { + this.cache.delete(key); + } + return undefined; + } + + set(key: string, data: T): void { + this.cache.set(key, { data, expiry: Date.now() + CATALOG_CACHE_TTL_MS }); + } +} + export interface AggregatedResourcesResponse { data: K8sResourceCommonWithClusterInfo[]; metadata?: { @@ -85,6 +118,7 @@ export class KonfluxService { private readonly catalog?: CatalogService; private readonly config: Config; private readonly resourceFetcher: ResourceFetcherService; + private readonly catalogCache = new CatalogCache(); constructor(config: Config, logger: LoggerService, catalog: CatalogService) { this.konfluxLogger = new KonfluxLogger(logger); @@ -141,6 +175,80 @@ export class KonfluxService { return filtered; } + /** + * Fetch and cache the entity from the catalog + */ + private async getCachedEntity( + entityRef: string, + credentials: BackstageCredentials, + userId: string, + ): Promise { + const cacheKey = `entity:${entityRef}:${userId}`; + let entity = this.catalogCache.get(cacheKey); + if (!entity) { + entity = + (await this.catalog!.getEntityByRef(entityRef, { + credentials, + })) ?? undefined; + if (entity) { + this.catalogCache.set(cacheKey, entity); + } + } + return entity; + } + + /** + * Fetch and cache the Konflux configuration for an entity + */ + private async getCachedKonfluxConfig( + entityRef: string, + entity: Entity, + credentials: BackstageCredentials, + userId: string, + ): Promise { + const cacheKey = `config:${entityRef}:${userId}`; + let konfluxConfig = this.catalogCache.get(cacheKey); + if (!konfluxConfig) { + konfluxConfig = await getKonfluxConfig( + this.config, + entity, + credentials, + this.catalog!, + this.konfluxLogger, + ); + if (konfluxConfig) { + this.catalogCache.set(cacheKey, konfluxConfig); + } + } + return konfluxConfig; + } + + /** + * Fetch and cache cluster-namespace combinations for an entity + */ + private async getCachedCombinations( + entityRef: string, + entity: Entity, + credentials: BackstageCredentials, + konfluxConfig: KonfluxConfig, + userId: string, + ): Promise { + const cacheKey = `combinations:${entityRef}:${userId}`; + let combinations = + this.catalogCache.get(cacheKey); + if (!combinations) { + combinations = await determineClusterNamespaceCombinations( + entity, + credentials, + konfluxConfig, + this.konfluxLogger, + this.catalog!, + ); + this.catalogCache.set(cacheKey, combinations); + } + return combinations; + } + /** * Aggregate resources from multiple clusters based on entity configuration */ @@ -167,10 +275,9 @@ export class KonfluxService { limitPerCluster: PAGINATION_CONFIG.DEFAULT_PAGE_SIZE, }; - // fetch the main entity - const entity = await this.catalog.getEntityByRef(entityRef, { - credentials, - }); + const userId = userEntityRef || userEmail || 'unknown'; + + const entity = await this.getCachedEntity(entityRef, credentials, userId); if (!entity) { this.konfluxLogger.error('Entity not found', undefined, { entityRef, @@ -179,14 +286,12 @@ export class KonfluxService { throw new Error(`Entity not found: ${entityRef}`); } - const konfluxConfig = await getKonfluxConfig( - this.config, + const konfluxConfig = await this.getCachedKonfluxConfig( + entityRef, entity, credentials, - this.catalog, - this.konfluxLogger, + userId, ); - if (!konfluxConfig) { this.konfluxLogger.warn('No Konflux configuration found', { entityRef, @@ -195,15 +300,13 @@ export class KonfluxService { return { data: [] }; } - // determine cluster-namespace combinations - let combinations = await determineClusterNamespaceCombinations( + let combinations = await this.getCachedCombinations( + entityRef, entity, credentials, konfluxConfig, - this.konfluxLogger, - this.catalog, + userId, ); - if (combinations.length === 0) { this.konfluxLogger.warn('No cluster-namespace combinations found', { entityRef, @@ -224,8 +327,6 @@ export class KonfluxService { let paginationState: PaginationState = {}; const isLoadMoreRequest = !!validatedFilters?.continuationToken; - const userId = userEntityRef || userEmail || 'unknown'; - if (validatedFilters?.continuationToken) { try { paginationState = decodeContinuationToken( @@ -292,7 +393,8 @@ export class KonfluxService { const aggregatedData = results .filter((r): r is NonNullable => !!r && !!r.items) .flatMap(result => { - const clusterInfo = konfluxConfig.clusters[result.combination.cluster]; + const clusterInfo = + konfluxConfig?.clusters?.[result.combination.cluster]; return result.items.map((item: K8sResourceCommonWithClusterInfo) => createResourceWithClusterInfo( item, diff --git a/workspaces/konflux/plugins/konflux-backend/src/services/kubearchive-service.ts b/workspaces/konflux/plugins/konflux-backend/src/services/kubearchive-service.ts index 2655fb67eb..b8a09fe86b 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/services/kubearchive-service.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/services/kubearchive-service.ts @@ -22,7 +22,7 @@ import { import type { ObservableMiddleware } from '@kubernetes/client-node'; import { KonfluxLogger } from '../helpers/logger'; import { getAuthToken } from '../helpers/auth'; -import { createKubeConfig } from '../helpers/client-factory'; +import { getOrCreateClient } from '../helpers/client-factory'; import { getKubeClient } from '../helpers/kube-client'; /** @@ -81,16 +81,15 @@ export class KubearchiveService { { cluster, namespace, resource }, ); - const kc = await createKubeConfig( + const customApi = await getOrCreateClient( konfluxConfig, cluster, this.logger, - token, true, // useKubearchiveUrl = true ); - if (!kc) { + if (!customApi) { this.logger.error( - 'Failed to create KubeConfig - cluster not found', + 'Failed to create API client - cluster not found', undefined, { cluster, @@ -101,7 +100,7 @@ export class KubearchiveService { throw new Error(`Cluster '${cluster}' not found`); } - const { CustomObjectsApi, Observable } = await getKubeClient(); + const { Observable } = await getKubeClient(); const requestHeaders = { ...(requiresImpersonation && { @@ -130,8 +129,6 @@ export class KubearchiveService { middleware: [headerMiddleware], } : undefined; - - const customApi = kc.makeApiClient(CustomObjectsApi); const response = await customApi.listNamespacedCustomObjectWithHttpInfo( { group: apiGroup, @@ -169,7 +166,13 @@ export class KubearchiveService { } | undefined; - const items = responseBody?.items ?? []; + const items = (responseBody?.items ?? []).map(item => { + if (item.metadata?.managedFields) { + const { managedFields, ...rest } = item.metadata; + return { ...item, metadata: rest }; + } + return item; + }); const nextPageToken = responseBody?.metadata?.continue; this.logger.debug('Fetched items from Kubearchive', { diff --git a/workspaces/konflux/plugins/konflux-backend/src/services/resource-fetcher.ts b/workspaces/konflux/plugins/konflux-backend/src/services/resource-fetcher.ts index d86244ab72..e3f8b68b78 100644 --- a/workspaces/konflux/plugins/konflux-backend/src/services/resource-fetcher.ts +++ b/workspaces/konflux/plugins/konflux-backend/src/services/resource-fetcher.ts @@ -26,7 +26,7 @@ import { } from '@red-hat-developer-hub/backstage-plugin-konflux-common'; import { KubearchiveService } from './kubearchive-service'; import uniqBy from 'lodash/uniqBy'; -import { createKubeConfig } from '../helpers/client-factory'; +import { getOrCreateClient } from '../helpers/client-factory'; import { KonfluxLogger } from '../helpers/logger'; import { getAuthToken } from '../helpers/auth'; import { getKubeClient } from '../helpers/kube-client'; @@ -125,15 +125,14 @@ export class ResourceFetcherService { }, ); - const kc = await createKubeConfig( + const customApi = await getOrCreateClient( konfluxConfig, cluster, this.konfluxLogger, - token, ); - if (!kc) { + if (!customApi) { this.konfluxLogger.error( - 'Failed to create KubeConfig - cluster not found', + 'Failed to create API client - cluster not found', undefined, { cluster, @@ -147,7 +146,7 @@ export class ResourceFetcherService { try { const { apiGroup, apiVersion, plural } = resourceModel; - const { CustomObjectsApi, Observable } = await getKubeClient(); + const { Observable } = await getKubeClient(); const requestHeaders = { ...(requiresImpersonation && { @@ -178,8 +177,6 @@ export class ResourceFetcherService { } : undefined; - const customApi = kc.makeApiClient(CustomObjectsApi); - const response = await customApi.listNamespacedCustomObjectWithHttpInfo( { group: apiGroup, @@ -220,7 +217,13 @@ export class ResourceFetcherService { } | undefined; - const items = responseBody?.items || []; + const items = (responseBody?.items || []).map(item => { + if (item.metadata?.managedFields) { + const { managedFields, ...rest } = item.metadata; + return { ...item, metadata: rest }; + } + return item; + }); const continueToken = responseBody?.metadata?.continue; this.konfluxLogger.info('Fetched resources from Kubernetes', { diff --git a/workspaces/konflux/plugins/konflux/src/api/KonfluxQueryProvider.tsx b/workspaces/konflux/plugins/konflux/src/api/KonfluxQueryProvider.tsx index 5b8b669011..e2b431017f 100644 --- a/workspaces/konflux/plugins/konflux/src/api/KonfluxQueryProvider.tsx +++ b/workspaces/konflux/plugins/konflux/src/api/KonfluxQueryProvider.tsx @@ -24,12 +24,10 @@ function getQueryClient() { queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 30 * 1000, // 30 seconds + staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 5 * 60 * 1000, // 5 minutes retry: 1, - // TODO: check if the bellow options are really needed - refetchOnWindowFocus: true, // refetch when window regains focus - refetchOnReconnect: true, // refetch when network reconnects + refetchOnWindowFocus: false, // do not refetch when window regains focus }, }, }); diff --git a/workspaces/konflux/plugins/konflux/src/hooks/useKonfluxResource.ts b/workspaces/konflux/plugins/konflux/src/hooks/useKonfluxResource.ts index 0cc54ae281..a9cfc969ed 100644 --- a/workspaces/konflux/plugins/konflux/src/hooks/useKonfluxResource.ts +++ b/workspaces/konflux/plugins/konflux/src/hooks/useKonfluxResource.ts @@ -186,10 +186,9 @@ export const useKonfluxResource = < getNextPageParam: lastPage => lastPage.continuationToken, initialPageParam: undefined as string | undefined, enabled: !!entity && !!resource && options?.enabled !== false, - staleTime: 30 * 1000, - gcTime: 5 * 60 * 1000, - refetchOnWindowFocus: true, - refetchOnReconnect: true, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 5 * 60 * 1000, // 5 minutes + refetchOnWindowFocus: false, }); const allData = query.data?.pages.flatMap(page => page.data) ?? [];