Skip to content

Commit dff3b21

Browse files
authored
[Konflux] add wildcard and glob pattern support for applications filter (#2752)
* refactor(Konflux): add wildcard and glob pattern support for applications filter Allow the `applications` field in `konflux-ci.dev/clusters` to use wildcards (*) and glob patterns (e.g. my-app-*, *-backend). Omitting `applications` fetches all applications from the namespace. * chore(konflux): add changesets Add changesets with the changes made for both konflux and konflux-backend plugins. * fixup! refactor(Konflux): add wildcard and glob pattern support for applications filter
1 parent 26ce797 commit dff3b21

10 files changed

Lines changed: 223 additions & 20 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-konflux-backend': patch
3+
---
4+
5+
Feat: add wildcard (_) and glob pattern support (e.g. my-app-_, \*-backend) for the applications field in konflux-ci.dev/clusters annotation.
6+
Feat: allow omitting applications to fetch all applications from a namespace.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-konflux': patch
3+
---
4+
5+
Docs: document wildcard and glob pattern usage with examples and YAML quoting note.

workspaces/konflux/plugins/konflux-backend/src/helpers/__tests__/kubernetes.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,74 @@ describe('kubernetes', () => {
261261
expect(result).toHaveLength(1);
262262
expect(result[0].metadata?.name).toBe('app1');
263263
});
264+
265+
it('should filter applications using glob pattern with prefix', () => {
266+
const items = [
267+
createMockApplication('my-app-frontend'),
268+
createMockApplication('my-app-backend'),
269+
createMockApplication('other-app'),
270+
];
271+
const result = filterResourcesByApplication(items, 'applications', [
272+
'my-app-*',
273+
]);
274+
expect(result).toHaveLength(2);
275+
expect(result[0].metadata?.name).toBe('my-app-frontend');
276+
expect(result[1].metadata?.name).toBe('my-app-backend');
277+
});
278+
279+
it('should filter applications using glob pattern with suffix', () => {
280+
const items = [
281+
createMockApplication('my-app-backend'),
282+
createMockApplication('other-backend'),
283+
createMockApplication('my-app-frontend'),
284+
];
285+
const result = filterResourcesByApplication(items, 'applications', [
286+
'*-backend',
287+
]);
288+
expect(result).toHaveLength(2);
289+
expect(result[0].metadata?.name).toBe('my-app-backend');
290+
expect(result[1].metadata?.name).toBe('other-backend');
291+
});
292+
293+
it('should filter applications using glob pattern with contains', () => {
294+
const items = [
295+
createMockApplication('my-api-service'),
296+
createMockApplication('other-app'),
297+
createMockApplication('test-api-backend'),
298+
];
299+
const result = filterResourcesByApplication(items, 'applications', [
300+
'*-api-*',
301+
]);
302+
expect(result).toHaveLength(2);
303+
expect(result[0].metadata?.name).toBe('my-api-service');
304+
expect(result[1].metadata?.name).toBe('test-api-backend');
305+
});
306+
307+
it('should filter with mix of exact and glob patterns', () => {
308+
const items = [
309+
createMockApplication('my-app-frontend'),
310+
createMockApplication('my-app-backend'),
311+
createMockApplication('special-app'),
312+
createMockApplication('other-app'),
313+
];
314+
const result = filterResourcesByApplication(items, 'applications', [
315+
'my-app-*',
316+
'special-app',
317+
]);
318+
expect(result).toHaveLength(3);
319+
});
320+
321+
it('should filter components using glob pattern', () => {
322+
const items = [
323+
createMockComponent('comp1', 'my-app-frontend'),
324+
createMockComponent('comp2', 'my-app-backend'),
325+
createMockComponent('comp3', 'other-app'),
326+
];
327+
const result = filterResourcesByApplication(items, 'components', [
328+
'my-app-*',
329+
]);
330+
expect(result).toHaveLength(2);
331+
});
264332
});
265333

266334
describe('createResourceWithClusterInfo', () => {

workspaces/konflux/plugins/konflux-backend/src/helpers/__tests__/label-selector.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,45 @@ describe('label-selector', () => {
144144

145145
expect(result).toBe(`${PipelineRunLabel.APPLICATION}=app1`);
146146
});
147+
148+
it('should skip application label selector when wildcard is used', () => {
149+
const combination = createMockCombination({ applications: ['*'] });
150+
const filters = createMockFilters();
151+
152+
const result = buildLabelSelector('pipelineruns', combination, filters);
153+
154+
expect(result).toBeUndefined();
155+
});
156+
157+
it('should only include component filter when wildcard is used with component', () => {
158+
const combination = createMockCombination({ applications: ['*'] });
159+
const filters = createMockFilters({ component: 'comp1' });
160+
161+
const result = buildLabelSelector('pipelineruns', combination, filters);
162+
163+
expect(result).toBe(`${PipelineRunLabel.COMPONENT}=comp1`);
164+
});
165+
166+
it('should skip application label selector when glob pattern is used', () => {
167+
const combination = createMockCombination({
168+
applications: ['app-*'],
169+
});
170+
const filters = createMockFilters();
171+
172+
const result = buildLabelSelector('pipelineruns', combination, filters);
173+
174+
expect(result).toBeUndefined();
175+
});
176+
177+
it('should skip application label selector when mix of exact and glob patterns', () => {
178+
const combination = createMockCombination({
179+
applications: ['app1', '*-backend'],
180+
});
181+
const filters = createMockFilters();
182+
183+
const result = buildLabelSelector('pipelineruns', combination, filters);
184+
185+
expect(result).toBeUndefined();
186+
});
147187
});
148188
});

workspaces/konflux/plugins/konflux-backend/src/helpers/config.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,13 @@ const extractComponentConfigsFromEntities = async (
118118
if (clustersParsedYaml) {
119119
const subcomponentName = e.metadata.name;
120120
clustersParsedYaml.forEach(clusterConfig => {
121-
// filter out invalid configs (missing required field)
122-
if (
123-
clusterConfig.cluster &&
124-
clusterConfig.namespace &&
125-
clusterConfig.applications &&
126-
clusterConfig.applications.length > 0
127-
) {
121+
// filter out invalid configs (missing required fields)
122+
if (clusterConfig.cluster && clusterConfig.namespace) {
128123
subcomponentConfigs.push({
129124
subcomponent: subcomponentName,
130125
cluster: clusterConfig.cluster,
131126
namespace: clusterConfig.namespace,
132-
applications: clusterConfig.applications,
127+
applications: clusterConfig.applications || [],
133128
});
134129
}
135130
});

workspaces/konflux/plugins/konflux-backend/src/helpers/kubernetes.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,29 @@ import {
1919
} from '@red-hat-developer-hub/backstage-plugin-konflux-common';
2020

2121
/**
22-
* Filter resources by application names
22+
* Convert a glob pattern (e.g. "app-*", "*api*") to a RegExp
23+
*/
24+
const globToRegex = (pattern: string): RegExp => {
25+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
26+
const regexStr = escaped.replace(/\*/g, '.*');
27+
return new RegExp(`^${regexStr}$`);
28+
};
29+
30+
/**
31+
* Check if a name matches any of the given application patterns.
32+
* Supports exact matches and glob patterns with "*".
33+
*/
34+
export const matchesApplicationPattern = (
35+
name: string,
36+
patterns: string[],
37+
): boolean => {
38+
return patterns.some(pattern =>
39+
pattern.includes('*') ? globToRegex(pattern).test(name) : pattern === name,
40+
);
41+
};
42+
43+
/**
44+
* Filter resources by application names or glob patterns
2345
*/
2446
export const filterResourcesByApplication = (
2547
items: K8sResourceCommonWithClusterInfo[],
@@ -39,14 +61,21 @@ export const filterResourcesByApplication = (
3961
const applicationName = getApplicationFromResource(item);
4062
switch (resourceType) {
4163
case 'applications':
42-
return applicationNames.includes(item.metadata?.name || '');
64+
return matchesApplicationPattern(
65+
item.metadata?.name || '',
66+
applicationNames,
67+
);
4368
case 'components':
44-
return applicationNames.includes(
69+
return matchesApplicationPattern(
4570
(item.spec?.application as string) || '',
71+
applicationNames,
4672
);
4773
case 'releases':
4874
case 'pipelineruns':
49-
return applicationNames.includes(applicationName || '');
75+
return matchesApplicationPattern(
76+
applicationName || '',
77+
applicationNames,
78+
);
5079
default:
5180
return true;
5281
}

workspaces/konflux/plugins/konflux-backend/src/helpers/label-selector.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ export const buildLabelSelector = (
3434

3535
const labelSelectors: string[] = [];
3636

37-
// Add application filter
38-
if (
39-
combination.applications?.length &&
40-
combination.applications?.length > 0
41-
) {
37+
// Add application filter (skip if no applications or any contain glob patterns)
38+
const hasWildcard =
39+
!combination.applications?.length ||
40+
combination.applications.some(app => app.includes('*'));
41+
42+
if (!hasWildcard) {
4243
if (combination.applications.length === 1) {
4344
labelSelectors.push(
4445
`${PipelineRunLabel.APPLICATION}=${combination.applications[0]}`,

workspaces/konflux/plugins/konflux-backend/src/services/__tests__/konflux-service.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,9 @@ describe('KonfluxService', () => {
949949
mockDetermineClusterNamespaceCombinations.mockResolvedValue([
950950
combination,
951951
]);
952-
mockBuildLabelSelector.mockReturnValue('app=app1'); // label selector available
952+
mockBuildLabelSelector.mockReturnValue(
953+
'appstudio.openshift.io/application=app1',
954+
); // label selector includes application filter
953955
mockResourceFetcher.fetchFromSource.mockResolvedValue({
954956
items: [resource1],
955957
newPaginationState: {},

workspaces/konflux/plugins/konflux-backend/src/services/konflux-service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
PAGINATION_CONFIG,
3030
ClusterError,
3131
GroupVersionKind,
32+
PipelineRunLabel,
3233
} from '@red-hat-developer-hub/backstage-plugin-konflux-common';
3334

3435
import { Entity } from '@backstage/catalog-model';
@@ -606,9 +607,13 @@ export class KonfluxService {
606607
filters: Filters | undefined,
607608
labelSelector: string | undefined,
608609
): K8sResourceCommonWithClusterInfo[] {
609-
const needsInMemoryFiltering = !labelSelector;
610+
const applicationHandledByLabelSelector =
611+
labelSelector?.includes(PipelineRunLabel.APPLICATION) ?? false;
612+
const fetchesAllApplications =
613+
!combination.applications?.length ||
614+
combination.applications.includes('*');
610615

611-
if (!needsInMemoryFiltering || !combination.applications?.length) {
616+
if (applicationHandledByLabelSelector || fetchesAllApplications) {
612617
return items;
613618
}
614619

workspaces/konflux/plugins/konflux/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,40 @@ annotations:
8383
- application-2
8484
```
8585

86+
To fetch all applications from a namespace, you can either omit the `applications` field or use a wildcard:
87+
88+
```yaml
89+
annotations:
90+
konflux-ci.dev/clusters: |
91+
- cluster: cluster-name
92+
namespace: namespace-name
93+
applications:
94+
- "*"
95+
```
96+
97+
Glob patterns are also supported for partial matching:
98+
99+
```yaml
100+
annotations:
101+
konflux-ci.dev/clusters: |
102+
- cluster: cluster-name
103+
namespace: namespace-name
104+
applications:
105+
- "my-app-*"
106+
- "*-backend"
107+
```
108+
109+
> **Note:** Patterns starting with `*` (e.g., `*-backend`) **must be quoted** in YAML. An unquoted `*` at the start of a value is interpreted as a YAML alias reference, which will cause a parsing error. Always use quotes: `"*-backend"`, not `*-backend`.
110+
111+
Or simply omit `applications` to fetch everything:
112+
113+
```yaml
114+
annotations:
115+
konflux-ci.dev/clusters: |
116+
- cluster: cluster-name
117+
namespace: namespace-name
118+
```
119+
86120
### 2. Resource Fetching Flow
87121

88122
```
@@ -390,6 +424,24 @@ metadata:
390424
spec:
391425
subcomponentOf: my-component
392426
type: service
427+
---
428+
# Subcomponent C - all applications from a namespace (wildcard)
429+
apiVersion: backstage.io/v1alpha1
430+
kind: Component
431+
metadata:
432+
name: my-component-subcomponent-c
433+
description: Subcomponent C
434+
title: Subcomponent C
435+
annotations:
436+
konflux-ci.dev/overview: 'true'
437+
konflux-ci.dev/konflux: 'true'
438+
konflux-ci.dev/ci: 'true'
439+
konflux-ci.dev/clusters: |
440+
- cluster: cluster3
441+
namespace: namespace3
442+
spec:
443+
subcomponentOf: my-component
444+
type: service
393445
```
394446

395447
#### Internal Configuration Structure

0 commit comments

Comments
 (0)