backend-app-api: add createAllowBootFailurePredicate

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-11-18 12:24:53 +01:00
parent 766215437e
commit 595f99a1ea
2 changed files with 574 additions and 0 deletions
@@ -0,0 +1,482 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { createAllowBootFailurePredicate } from './createAllowBootFailurePredicate';
describe('createAllowBootFailurePredicate', () => {
describe('when no config is provided', () => {
it('should default to abort for plugins', () => {
const predicate = createAllowBootFailurePredicate();
expect(predicate('test-plugin')).toBe(false);
});
it('should default to abort for modules', () => {
const predicate = createAllowBootFailurePredicate();
expect(predicate('test-plugin', 'test-module')).toBe(false);
});
});
describe('default plugin boot failure configuration', () => {
it('should use abort as default when not configured', () => {
const config = new ConfigReader({});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(false);
});
it('should use continue when default is set to continue', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'continue',
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(true);
});
it('should use abort when default is set to abort', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'abort',
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(false);
});
});
describe('default module boot failure configuration', () => {
it('should use abort as default when not configured', () => {
const config = new ConfigReader({});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(false);
});
it('should use continue when default is set to continue', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'continue',
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(true);
});
it('should use abort when default is set to abort', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'abort',
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(false);
});
});
describe('plugin-specific overrides', () => {
it('should override default with plugin-specific continue', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'abort',
},
plugins: {
'test-plugin': {
onPluginBootFailure: 'continue',
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(true);
});
it('should override default with plugin-specific abort', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'continue',
},
plugins: {
'test-plugin': {
onPluginBootFailure: 'abort',
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(false);
});
it('should use default when plugin-specific config is not set', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'continue',
},
plugins: {
'other-plugin': {
onPluginBootFailure: 'abort',
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(true);
});
it('should handle multiple plugins independently', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'abort',
},
plugins: {
'plugin-a': {
onPluginBootFailure: 'continue',
},
'plugin-b': {
onPluginBootFailure: 'abort',
},
'plugin-c': {
// No override, uses default
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('plugin-a')).toBe(true);
expect(predicate('plugin-b')).toBe(false);
expect(predicate('plugin-c')).toBe(false);
});
});
describe('module-specific overrides', () => {
it('should override default with module-specific continue', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'abort',
},
plugins: {
'test-plugin': {
modules: {
'test-module': {
onPluginModuleBootFailure: 'continue',
},
},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(true);
});
it('should override default with module-specific abort', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'continue',
},
plugins: {
'test-plugin': {
modules: {
'test-module': {
onPluginModuleBootFailure: 'abort',
},
},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(false);
});
it('should use default when module-specific config is not set', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'continue',
},
plugins: {
'test-plugin': {
modules: {
'other-module': {
onPluginModuleBootFailure: 'abort',
},
},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(true);
});
it('should handle multiple modules independently', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'abort',
},
plugins: {
'test-plugin': {
modules: {
'module-a': {
onPluginModuleBootFailure: 'continue',
},
'module-b': {
onPluginModuleBootFailure: 'abort',
},
'module-c': {
// No override, uses default
},
},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'module-a')).toBe(true);
expect(predicate('test-plugin', 'module-b')).toBe(false);
expect(predicate('test-plugin', 'module-c')).toBe(false);
});
it('should handle modules across different plugins', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'abort',
},
plugins: {
'plugin-a': {
modules: {
'module-x': {
onPluginModuleBootFailure: 'continue',
},
},
},
'plugin-b': {
modules: {
'module-y': {
onPluginModuleBootFailure: 'abort',
},
},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('plugin-a', 'module-x')).toBe(true);
expect(predicate('plugin-b', 'module-y')).toBe(false);
});
});
describe('combined plugin and module configurations', () => {
it('should use module config when both plugin and module configs exist', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'abort',
onPluginModuleBootFailure: 'abort',
},
plugins: {
'test-plugin': {
onPluginBootFailure: 'continue',
modules: {
'test-module': {
onPluginModuleBootFailure: 'abort',
},
},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(true);
expect(predicate('test-plugin', 'test-module')).toBe(false);
});
it('should use plugin default when module config does not exist', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'abort',
onPluginModuleBootFailure: 'abort',
},
plugins: {
'test-plugin': {
onPluginBootFailure: 'continue',
modules: {
'other-module': {
onPluginModuleBootFailure: 'abort',
},
},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(true);
expect(predicate('test-plugin', 'test-module')).toBe(false); // Uses default module config
});
});
describe('edge cases', () => {
it('should handle empty plugins config', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginBootFailure: 'continue',
},
plugins: {},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin')).toBe(true);
});
it('should handle plugin with empty modules config', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'continue',
},
plugins: {
'test-plugin': {
modules: {},
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(true);
});
it('should handle plugin without modules config', () => {
const config = new ConfigReader({
backend: {
startup: {
default: {
onPluginModuleBootFailure: 'continue',
},
plugins: {
'test-plugin': {
onPluginBootFailure: 'abort',
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('test-plugin', 'test-module')).toBe(true);
});
it('should handle case sensitivity correctly', () => {
const config = new ConfigReader({
backend: {
startup: {
plugins: {
'Test-Plugin': {
onPluginBootFailure: 'continue',
},
},
},
},
});
const predicate = createAllowBootFailurePredicate(config);
expect(predicate('Test-Plugin')).toBe(true);
expect(predicate('test-plugin')).toBe(false); // Different case
});
});
describe('performance - configuration read upfront', () => {
it('should read configuration only once', () => {
const configData = {
backend: {
startup: {
default: {
onPluginBootFailure: 'continue',
},
plugins: {
'plugin-a': {
onPluginBootFailure: 'abort',
modules: {
'module-x': {
onPluginModuleBootFailure: 'continue',
},
},
},
},
},
},
};
const config = new ConfigReader(configData);
const predicate = createAllowBootFailurePredicate(config);
// Call predicate multiple times - should use cached values
expect(predicate('plugin-a')).toBe(false);
expect(predicate('plugin-a')).toBe(false);
expect(predicate('plugin-a', 'module-x')).toBe(true);
expect(predicate('plugin-a', 'module-x')).toBe(true);
expect(predicate('plugin-b')).toBe(true); // Uses default
});
});
});
@@ -0,0 +1,92 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Config } from '@backstage/config';
export type AllowBootFailurePredicate = (
pluginId: string,
moduleId?: string,
) => boolean;
/**
* Creates a predicate function that determines whether a boot failure should be
* allowed for a given plugin or module based on configuration.
*
* @param config - The configuration object to read boot failure settings from
* @returns A predicate function that accepts a pluginId and optional moduleId,
* and returns true if boot failures should be allowed, false otherwise.
*/
export function createAllowBootFailurePredicate(
config?: Config,
): AllowBootFailurePredicate {
// Read default values upfront
const defaultPluginBootFailure =
config?.getOptionalString('backend.startup.default.onPluginBootFailure') ??
'abort';
const defaultModuleBootFailure =
config?.getOptionalString(
'backend.startup.default.onPluginModuleBootFailure',
) ?? 'abort';
// Read plugin-specific overrides upfront
const pluginOverrides = new Map<string, string>();
const moduleOverrides = new Map<string, Map<string, string>>();
const pluginsConfig = config?.getOptionalConfig('backend.startup.plugins');
if (pluginsConfig) {
for (const pluginId of pluginsConfig.keys()) {
const pluginConfig = pluginsConfig.getConfig(pluginId);
const pluginBootFailure = pluginConfig.getOptionalString(
'onPluginBootFailure',
);
if (pluginBootFailure) {
pluginOverrides.set(pluginId, pluginBootFailure);
}
// Read module-specific overrides
const modulesConfig = pluginConfig.getOptionalConfig('modules');
if (modulesConfig) {
const moduleMap = new Map<string, string>();
for (const moduleId of modulesConfig.keys()) {
const moduleConfig = modulesConfig.getConfig(moduleId);
const moduleBootFailure = moduleConfig.getOptionalString(
'onPluginModuleBootFailure',
);
if (moduleBootFailure) {
moduleMap.set(moduleId, moduleBootFailure);
}
}
if (moduleMap.size > 0) {
moduleOverrides.set(pluginId, moduleMap);
}
}
}
}
return (pluginId: string, moduleId?: string): boolean => {
if (moduleId !== undefined) {
// Module-specific boot failure predicate
const moduleMap = moduleOverrides.get(pluginId);
const moduleBootFailure =
moduleMap?.get(moduleId) ?? defaultModuleBootFailure;
return moduleBootFailure === 'continue';
}
// Plugin-specific boot failure predicate
const pluginBootFailure =
pluginOverrides.get(pluginId) ?? defaultPluginBootFailure;
return pluginBootFailure === 'continue';
};
}