feat(catalog/bitbucketCloud): Add option to configure schedule via app-config.yaml
Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': minor
|
||||
---
|
||||
|
||||
Bitbucket Cloud provider: Add option to configure schedule via `app-config.yaml` instead of in code.
|
||||
|
||||
Please find how to configure the schedule at the config at
|
||||
https://backstage.io/docs/integrations/bitbucketCloud/discovery
|
||||
@@ -37,6 +37,7 @@ And then add the entity provider to your catalog builder:
|
||||
+ builder.addEntityProvider(
|
||||
+ BitbucketCloudEntityProvider.fromConfig(env.config, {
|
||||
+ logger: env.logger,
|
||||
+ // optional: alternatively, configure via app-config.yaml
|
||||
+ schedule: env.scheduler.createScheduledTaskRunner({
|
||||
+ frequency: { minutes: 30 },
|
||||
+ timeout: { minutes: 3 },
|
||||
@@ -67,24 +68,38 @@ catalog:
|
||||
filters: # optional
|
||||
projectKey: '^apis-.*$' # optional; RegExp
|
||||
repoSlug: '^service-.*$' # optional; RegExp
|
||||
schedule: # optional; same options as in TaskScheduleDefinition
|
||||
# supports cron, ISO duration, "human duration" as used in code
|
||||
frequency: { minutes: 30 }
|
||||
# supports ISO duration, "human duration" as used in code
|
||||
timeout: { minutes: 3 }
|
||||
workspace: workspace-name
|
||||
```
|
||||
|
||||
> **Note:** It is possible but certainly not recommended to skip the provider ID level.
|
||||
> If you do so, `default` will be used as provider ID.
|
||||
|
||||
- **catalogPath** _(optional)_:
|
||||
- **`catalogPath`** _(optional)_:
|
||||
Default: `/catalog-info.yaml`.
|
||||
Path where to look for `catalog-info.yaml` files.
|
||||
When started with `/`, it is an absolute path from the repo root.
|
||||
It supports values as allowed by the `path` filter/modifier
|
||||
[at Bitbucket Cloud's code search](https://confluence.atlassian.com/bitbucket/code-search-in-bitbucket-873876782.html#Search-Pathmodifier).
|
||||
- **filters** _(optional)_:
|
||||
- **projectKey** _(optional)_:
|
||||
- **`filters`** _(optional)_:
|
||||
- **`projectKey`** _(optional)_:
|
||||
Regular expression used to filter results based on the project key.
|
||||
- **repoSlug** _(optional)_:
|
||||
- **`repoSlug`** _(optional)_:
|
||||
Regular expression used to filter results based on the repo slug.
|
||||
- **workspace**:
|
||||
- **`schedule`** _(optional)_:
|
||||
- **`frequency`**:
|
||||
How often you want the task to run. The system does its best to avoid overlapping invocations.
|
||||
- **`timeout`**:
|
||||
The maximum amount of time that a single task invocation can take.
|
||||
- **`initialDelay`** _(optional)_:
|
||||
The amount of time that should pass before the first invocation happens.
|
||||
- **`scope`** _(optional)_:
|
||||
`'global'` or `'local'`. Sets the scope of concurrency control.
|
||||
- **`workspace`**:
|
||||
Name of your organization account/workspace.
|
||||
If you want to add multiple workspaces, you need to add one provider config each.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Config } from '@backstage/config';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-backend';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginTaskScheduler } from '@backstage/backend-tasks';
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
|
||||
// @public
|
||||
@@ -18,7 +19,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
schedule: TaskRunner;
|
||||
schedule?: TaskRunner;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
},
|
||||
): BitbucketCloudEntityProvider[];
|
||||
// (undocumented)
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskScheduleDefinitionConfig } from '@backstage/backend-tasks';
|
||||
|
||||
export interface Config {
|
||||
catalog?: {
|
||||
/**
|
||||
@@ -53,6 +55,10 @@ export interface Config {
|
||||
*/
|
||||
projectKey?: RegExp;
|
||||
};
|
||||
/**
|
||||
* (Optional) TaskScheduleDefinition for the discovery.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinitionConfig;
|
||||
}
|
||||
| Record<
|
||||
string,
|
||||
@@ -83,6 +89,10 @@ export interface Config {
|
||||
*/
|
||||
projectKey?: RegExp;
|
||||
};
|
||||
/**
|
||||
* (Optional) TaskScheduleDefinition for the discovery.
|
||||
*/
|
||||
schedule?: TaskScheduleDefinitionConfig;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"luxon": "^3.0.0",
|
||||
"msw": "^0.47.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
+74
-1
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
|
||||
import {
|
||||
PluginTaskScheduler,
|
||||
TaskInvocationDefinition,
|
||||
TaskRunner,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { EntityProviderConnection } from '@backstage/plugin-catalog-backend';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
@@ -77,6 +81,75 @@ describe('BitbucketCloudEntityProvider', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('fail without schedule and scheduler', () => {
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketCloud: {
|
||||
workspace: 'test-ws',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
BitbucketCloudEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
}),
|
||||
).toThrow('Either schedule or scheduler must be provided.');
|
||||
});
|
||||
|
||||
it('fail with scheduler but no schedule config', () => {
|
||||
const scheduler = jest.fn() as unknown as PluginTaskScheduler;
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketCloud: {
|
||||
workspace: 'test-ws',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
BitbucketCloudEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
}),
|
||||
).toThrow(
|
||||
'No schedule provided neither via code nor config for bitbucketCloud-provider:default.',
|
||||
);
|
||||
});
|
||||
|
||||
it('single simple provider config with schedule in config', () => {
|
||||
const scheduler = {
|
||||
createScheduledTaskRunner: (_: any) => jest.fn(),
|
||||
} as unknown as PluginTaskScheduler;
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketCloud: {
|
||||
workspace: 'test-ws',
|
||||
schedule: {
|
||||
frequency: 'PT30M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const providers = BitbucketCloudEntityProvider.fromConfig(config, {
|
||||
logger,
|
||||
scheduler,
|
||||
});
|
||||
|
||||
expect(providers).toHaveLength(1);
|
||||
expect(providers[0].getProviderName()).toEqual(
|
||||
'bitbucketCloud-provider:default',
|
||||
);
|
||||
});
|
||||
|
||||
it('multiple provider configs', () => {
|
||||
const schedule = new PersistingTaskRunner();
|
||||
const config = new ConfigReader({
|
||||
|
||||
+27
-13
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TaskRunner } from '@backstage/backend-tasks';
|
||||
import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
BitbucketCloudIntegration,
|
||||
@@ -58,7 +58,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
schedule: TaskRunner;
|
||||
schedule?: TaskRunner;
|
||||
scheduler?: PluginTaskScheduler;
|
||||
},
|
||||
): BitbucketCloudEntityProvider[] {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -69,29 +70,42 @@ export class BitbucketCloudEntityProvider implements EntityProvider {
|
||||
throw new Error('No integration for bitbucket.org available');
|
||||
}
|
||||
|
||||
return readProviderConfigs(config).map(
|
||||
providerConfig =>
|
||||
new BitbucketCloudEntityProvider(
|
||||
providerConfig,
|
||||
integration,
|
||||
options.logger,
|
||||
options.schedule,
|
||||
),
|
||||
);
|
||||
if (!options.schedule && !options.scheduler) {
|
||||
throw new Error('Either schedule or scheduler must be provided.');
|
||||
}
|
||||
|
||||
return readProviderConfigs(config).map(providerConfig => {
|
||||
if (!options.schedule && !providerConfig.schedule) {
|
||||
throw new Error(
|
||||
`No schedule provided neither via code nor config for bitbucketCloud-provider:${providerConfig.id}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const taskRunner =
|
||||
options.schedule ??
|
||||
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
|
||||
|
||||
return new BitbucketCloudEntityProvider(
|
||||
providerConfig,
|
||||
integration,
|
||||
options.logger,
|
||||
taskRunner,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private constructor(
|
||||
config: BitbucketCloudEntityProviderConfig,
|
||||
integration: BitbucketCloudIntegration,
|
||||
logger: Logger,
|
||||
schedule: TaskRunner,
|
||||
taskRunner: TaskRunner,
|
||||
) {
|
||||
this.client = BitbucketCloudClient.fromConfig(integration.config);
|
||||
this.config = config;
|
||||
this.logger = logger.child({
|
||||
target: this.getProviderName(),
|
||||
});
|
||||
this.scheduleFn = this.createScheduleFn(schedule);
|
||||
this.scheduleFn = this.createScheduleFn(taskRunner);
|
||||
}
|
||||
|
||||
private createScheduleFn(schedule: TaskRunner): () => Promise<void> {
|
||||
|
||||
+26
-1
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { Duration } from 'luxon';
|
||||
import { readProviderConfigs } from './BitbucketCloudEntityProviderConfig';
|
||||
|
||||
describe('readProviderConfigs', () => {
|
||||
@@ -68,13 +69,22 @@ describe('readProviderConfigs', () => {
|
||||
repoSlug: 'repoSlug.*filter',
|
||||
},
|
||||
},
|
||||
providerWithSchedule: {
|
||||
workspace: 'test-ws5',
|
||||
schedule: {
|
||||
frequency: 'PT30M',
|
||||
timeout: {
|
||||
minutes: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const providerConfigs = readProviderConfigs(config);
|
||||
|
||||
expect(providerConfigs).toHaveLength(4);
|
||||
expect(providerConfigs).toHaveLength(5);
|
||||
expect(providerConfigs[0]).toEqual({
|
||||
id: 'providerWorkspaceOnly',
|
||||
workspace: 'test-ws1',
|
||||
@@ -111,5 +121,20 @@ describe('readProviderConfigs', () => {
|
||||
repoSlug: /^repoSlug.*filter$/,
|
||||
},
|
||||
});
|
||||
expect(providerConfigs[4]).toEqual({
|
||||
id: 'providerWithSchedule',
|
||||
workspace: 'test-ws5',
|
||||
catalogPath: '/catalog-info.yaml',
|
||||
filters: {
|
||||
projectKey: undefined,
|
||||
repoSlug: undefined,
|
||||
},
|
||||
schedule: {
|
||||
frequency: Duration.fromISO('PT30M'),
|
||||
timeout: {
|
||||
minutes: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
@@ -14,6 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
readTaskScheduleDefinitionFromConfig,
|
||||
TaskScheduleDefinition,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
const DEFAULT_CATALOG_PATH = '/catalog-info.yaml';
|
||||
@@ -27,6 +31,7 @@ export type BitbucketCloudEntityProviderConfig = {
|
||||
projectKey?: RegExp;
|
||||
repoSlug?: RegExp;
|
||||
};
|
||||
schedule?: TaskScheduleDefinition;
|
||||
};
|
||||
|
||||
export function readProviderConfigs(
|
||||
@@ -61,6 +66,10 @@ function readProviderConfig(
|
||||
const projectKeyPattern = config.getOptionalString('filters.projectKey');
|
||||
const repoSlugPattern = config.getOptionalString('filters.repoSlug');
|
||||
|
||||
const schedule = config.has('schedule')
|
||||
? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule'))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
catalogPath,
|
||||
@@ -71,6 +80,7 @@ function readProviderConfig(
|
||||
: undefined,
|
||||
repoSlug: repoSlugPattern ? compileRegExp(repoSlugPattern) : undefined,
|
||||
},
|
||||
schedule,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user