feat: Add dynamic provider loader with improved error handling

- Create provider-loader.ts with function-based dynamic import functionality
- Update CloudRunner.setupSelectedBuildPlatform to use dynamic loader for unknown providers
- Add comprehensive error handling for missing packages and interface validation
- Include test coverage for successful loading and error scenarios
- Maintain backward compatibility with existing built-in providers
- Add ProviderLoader class wrapper for backward compatibility
- Support both built-in providers (via switch) and external providers (via dynamic import)
pull/734/head
Frostebite 2025-09-10 19:46:36 +01:00
parent d6cc45383d
commit be0139ec6d
8 changed files with 296 additions and 8 deletions

View File

@ -0,0 +1,18 @@
{
"files.autoSave": "on",
"files.autoSaveWhen": "on",
"files.autoSaveDelay": 1000,
"editor.formatOnSave": false,
"editor.formatOnPaste": false,
"editor.formatOnType": false,
"editor.codeActionsOnSave": {},
"git.autorefresh": false,
"git.confirmSync": false,
"git.autofetch": false,
"editor.defaultFormatter": null
}

157
dist/index.js generated vendored
View File

@ -759,6 +759,7 @@ const core = __importStar(__nccwpck_require__(42186));
const test_1 = __importDefault(__nccwpck_require__(63007)); const test_1 = __importDefault(__nccwpck_require__(63007));
const local_1 = __importDefault(__nccwpck_require__(66575)); const local_1 = __importDefault(__nccwpck_require__(66575));
const docker_1 = __importDefault(__nccwpck_require__(42802)); const docker_1 = __importDefault(__nccwpck_require__(42802));
const provider_loader_1 = __importDefault(__nccwpck_require__(45788));
const github_1 = __importDefault(__nccwpck_require__(83654)); const github_1 = __importDefault(__nccwpck_require__(83654));
const shared_workspace_locking_1 = __importDefault(__nccwpck_require__(71372)); const shared_workspace_locking_1 = __importDefault(__nccwpck_require__(71372));
const follow_log_stream_service_1 = __nccwpck_require__(40266); const follow_log_stream_service_1 = __nccwpck_require__(40266);
@ -778,7 +779,7 @@ class CloudRunner {
if (CloudRunner.buildParameters.githubCheckId === ``) { if (CloudRunner.buildParameters.githubCheckId === ``) {
CloudRunner.buildParameters.githubCheckId = await github_1.default.createGitHubCheck(CloudRunner.buildParameters.buildGuid); CloudRunner.buildParameters.githubCheckId = await github_1.default.createGitHubCheck(CloudRunner.buildParameters.buildGuid);
} }
CloudRunner.setupSelectedBuildPlatform(); await CloudRunner.setupSelectedBuildPlatform();
CloudRunner.defaultSecrets = task_parameter_serializer_1.TaskParameterSerializer.readDefaultSecrets(); CloudRunner.defaultSecrets = task_parameter_serializer_1.TaskParameterSerializer.readDefaultSecrets();
CloudRunner.cloudRunnerEnvironmentVariables = CloudRunner.cloudRunnerEnvironmentVariables =
task_parameter_serializer_1.TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters); task_parameter_serializer_1.TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters);
@ -796,7 +797,7 @@ class CloudRunner {
} }
follow_log_stream_service_1.FollowLogStreamService.Reset(); follow_log_stream_service_1.FollowLogStreamService.Reset();
} }
static setupSelectedBuildPlatform() { static async setupSelectedBuildPlatform() {
cloud_runner_logger_1.default.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`); cloud_runner_logger_1.default.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`);
// Detect LocalStack endpoints and reroute AWS provider to local-docker for CI tests that only need S3 // Detect LocalStack endpoints and reroute AWS provider to local-docker for CI tests that only need S3
const endpointsToCheck = [ const endpointsToCheck = [
@ -838,9 +839,19 @@ class CloudRunner {
CloudRunner.Provider = new local_1.default(); CloudRunner.Provider = new local_1.default();
break; break;
case 'local': case 'local':
default:
CloudRunner.Provider = new local_1.default(); CloudRunner.Provider = new local_1.default();
break; break;
default:
// Try to load provider using the dynamic loader for unknown providers
try {
CloudRunner.Provider = await (0, provider_loader_1.default)(provider, CloudRunner.buildParameters);
}
catch (error) {
cloud_runner_logger_1.default.log(`Failed to load provider '${provider}' using dynamic loader: ${error.message}`);
cloud_runner_logger_1.default.log('Falling back to local provider...');
CloudRunner.Provider = new local_1.default();
}
break;
} }
} }
static async run(buildParameters, baseImage) { static async run(buildParameters, baseImage) {
@ -4425,6 +4436,118 @@ class LocalCloudRunner {
exports["default"] = LocalCloudRunner; exports["default"] = LocalCloudRunner;
/***/ }),
/***/ 45788:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ProviderLoader = void 0;
const cloud_runner_logger_1 = __importDefault(__nccwpck_require__(42864));
/**
* Dynamically load a provider package by name.
* @param providerName Name of the provider package to load
* @param buildParameters Build parameters passed to the provider constructor
* @throws Error when the provider cannot be loaded or does not implement ProviderInterface
*/
async function loadProvider(providerName, buildParameters) {
cloud_runner_logger_1.default.log(`Loading provider: ${providerName}`);
let importedModule;
try {
// Map provider names to their module paths for built-in providers
const providerModuleMap = {
'aws': './aws',
'k8s': './k8s',
'test': './test',
'local-docker': './docker',
'local-system': './local',
'local': './local'
};
const modulePath = providerModuleMap[providerName] || providerName;
importedModule = await Promise.resolve().then(() => __importStar(require(modulePath)));
}
catch (error) {
throw new Error(`Failed to load provider package '${providerName}': ${error.message}`);
}
const Provider = importedModule.default || importedModule;
let instance;
try {
instance = new Provider(buildParameters);
}
catch (error) {
throw new Error(`Failed to instantiate provider '${providerName}': ${error.message}`);
}
const requiredMethods = [
'cleanupWorkflow',
'setupWorkflow',
'runTaskInWorkflow',
'garbageCollect',
'listResources',
'listWorkflow',
'watchWorkflow',
];
for (const method of requiredMethods) {
if (typeof instance[method] !== 'function') {
throw new Error(`Provider package '${providerName}' does not implement ProviderInterface. Missing method '${method}'.`);
}
}
cloud_runner_logger_1.default.log(`Successfully loaded provider: ${providerName}`);
return instance;
}
exports["default"] = loadProvider;
/**
* ProviderLoader class for backward compatibility and additional utilities
*/
class ProviderLoader {
/**
* Dynamically loads a provider by name (wrapper around loadProvider function)
* @param providerName - The name of the provider to load
* @param buildParameters - Build parameters to pass to the provider constructor
* @returns Promise<ProviderInterface> - The loaded provider instance
* @throws Error if provider package is missing or doesn't implement ProviderInterface
*/
static async loadProvider(providerName, buildParameters) {
return loadProvider(providerName, buildParameters);
}
/**
* Gets a list of available provider names
* @returns string[] - Array of available provider names
*/
static getAvailableProviders() {
return ['aws', 'k8s', 'test', 'local-docker', 'local-system', 'local'];
}
}
exports.ProviderLoader = ProviderLoader;
/***/ }), /***/ }),
/***/ 63007: /***/ 63007:
@ -7367,11 +7490,34 @@ exports["default"] = ImageTag;
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) { var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.CloudRunner = exports.Versioning = exports.Unity = exports.Project = exports.Platform = exports.Output = exports.ImageTag = exports.Input = exports.Docker = exports.Cache = exports.BuildParameters = exports.Action = void 0; exports.ProviderLoader = exports.loadProvider = exports.CloudRunner = exports.Versioning = exports.Unity = exports.Project = exports.Platform = exports.Output = exports.ImageTag = exports.Input = exports.Docker = exports.Cache = exports.BuildParameters = exports.Action = void 0;
const action_1 = __importDefault(__nccwpck_require__(89088)); const action_1 = __importDefault(__nccwpck_require__(89088));
exports.Action = action_1.default; exports.Action = action_1.default;
const build_parameters_1 = __importDefault(__nccwpck_require__(80787)); const build_parameters_1 = __importDefault(__nccwpck_require__(80787));
@ -7396,6 +7542,9 @@ const versioning_1 = __importDefault(__nccwpck_require__(88729));
exports.Versioning = versioning_1.default; exports.Versioning = versioning_1.default;
const cloud_runner_1 = __importDefault(__nccwpck_require__(79144)); const cloud_runner_1 = __importDefault(__nccwpck_require__(79144));
exports.CloudRunner = cloud_runner_1.default; exports.CloudRunner = cloud_runner_1.default;
const provider_loader_1 = __importStar(__nccwpck_require__(45788));
exports.loadProvider = provider_loader_1.default;
Object.defineProperty(exports, "ProviderLoader", ({ enumerable: true, get: function () { return provider_loader_1.ProviderLoader; } }));
/***/ }), /***/ }),

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,7 @@ import CloudRunnerEnvironmentVariable from './options/cloud-runner-environment-v
import TestCloudRunner from './providers/test'; import TestCloudRunner from './providers/test';
import LocalCloudRunner from './providers/local'; import LocalCloudRunner from './providers/local';
import LocalDockerCloudRunner from './providers/docker'; import LocalDockerCloudRunner from './providers/docker';
import loadProvider from './providers/provider-loader';
import GitHub from '../github'; import GitHub from '../github';
import SharedWorkspaceLocking from './services/core/shared-workspace-locking'; import SharedWorkspaceLocking from './services/core/shared-workspace-locking';
import { FollowLogStreamService } from './services/core/follow-log-stream-service'; import { FollowLogStreamService } from './services/core/follow-log-stream-service';
@ -39,7 +40,7 @@ class CloudRunner {
if (CloudRunner.buildParameters.githubCheckId === ``) { if (CloudRunner.buildParameters.githubCheckId === ``) {
CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid); CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid);
} }
CloudRunner.setupSelectedBuildPlatform(); await CloudRunner.setupSelectedBuildPlatform();
CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets(); CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets();
CloudRunner.cloudRunnerEnvironmentVariables = CloudRunner.cloudRunnerEnvironmentVariables =
TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters); TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters);
@ -63,7 +64,7 @@ class CloudRunner {
FollowLogStreamService.Reset(); FollowLogStreamService.Reset();
} }
private static setupSelectedBuildPlatform() { private static async setupSelectedBuildPlatform() {
CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`); CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`);
// Detect LocalStack endpoints and reroute AWS provider to local-docker for CI tests that only need S3 // Detect LocalStack endpoints and reroute AWS provider to local-docker for CI tests that only need S3
const endpointsToCheck = [ const endpointsToCheck = [
@ -88,6 +89,7 @@ class CloudRunner {
CloudRunnerLogger.log('LocalStack endpoints detected; routing provider to local-docker for this run'); CloudRunnerLogger.log('LocalStack endpoints detected; routing provider to local-docker for this run');
provider = 'local-docker'; provider = 'local-docker';
} }
switch (provider) { switch (provider) {
case 'k8s': case 'k8s':
CloudRunner.Provider = new Kubernetes(CloudRunner.buildParameters); CloudRunner.Provider = new Kubernetes(CloudRunner.buildParameters);
@ -105,9 +107,18 @@ class CloudRunner {
CloudRunner.Provider = new LocalCloudRunner(); CloudRunner.Provider = new LocalCloudRunner();
break; break;
case 'local': case 'local':
default:
CloudRunner.Provider = new LocalCloudRunner(); CloudRunner.Provider = new LocalCloudRunner();
break; break;
default:
// Try to load provider using the dynamic loader for unknown providers
try {
CloudRunner.Provider = await loadProvider(provider, CloudRunner.buildParameters);
} catch (error: any) {
CloudRunnerLogger.log(`Failed to load provider '${provider}' using dynamic loader: ${error.message}`);
CloudRunnerLogger.log('Falling back to local provider...');
CloudRunner.Provider = new LocalCloudRunner();
}
break;
} }
} }

View File

@ -0,0 +1 @@
export default class InvalidProvider {}

View File

@ -0,0 +1,19 @@
import loadProvider from './provider-loader';
import { ProviderInterface } from './provider-interface';
describe('provider-loader', () => {
it('loads a provider dynamically', async () => {
const provider: ProviderInterface = await loadProvider('./test', {} as any);
expect(typeof provider.runTaskInWorkflow).toBe('function');
});
it('throws when provider package is missing', async () => {
await expect(loadProvider('non-existent-package', {} as any)).rejects.toThrow('non-existent-package');
});
it('throws when provider does not implement ProviderInterface', async () => {
await expect(loadProvider('./fixtures/invalid-provider', {} as any)).rejects.toThrow(
'does not implement ProviderInterface',
);
});
});

View File

@ -0,0 +1,87 @@
import { ProviderInterface } from './provider-interface';
import BuildParameters from '../../build-parameters';
import CloudRunnerLogger from '../services/core/cloud-runner-logger';
/**
* Dynamically load a provider package by name.
* @param providerName Name of the provider package to load
* @param buildParameters Build parameters passed to the provider constructor
* @throws Error when the provider cannot be loaded or does not implement ProviderInterface
*/
export default async function loadProvider(
providerName: string,
buildParameters: BuildParameters,
): Promise<ProviderInterface> {
CloudRunnerLogger.log(`Loading provider: ${providerName}`);
let importedModule: any;
try {
// Map provider names to their module paths for built-in providers
const providerModuleMap: Record<string, string> = {
'aws': './aws',
'k8s': './k8s',
'test': './test',
'local-docker': './docker',
'local-system': './local',
'local': './local'
};
const modulePath = providerModuleMap[providerName] || providerName;
importedModule = await import(modulePath);
} catch (error) {
throw new Error(`Failed to load provider package '${providerName}': ${(error as Error).message}`);
}
const Provider = importedModule.default || importedModule;
let instance: any;
try {
instance = new Provider(buildParameters);
} catch (error) {
throw new Error(`Failed to instantiate provider '${providerName}': ${(error as Error).message}`);
}
const requiredMethods = [
'cleanupWorkflow',
'setupWorkflow',
'runTaskInWorkflow',
'garbageCollect',
'listResources',
'listWorkflow',
'watchWorkflow',
];
for (const method of requiredMethods) {
if (typeof instance[method] !== 'function') {
throw new Error(
`Provider package '${providerName}' does not implement ProviderInterface. Missing method '${method}'.`,
);
}
}
CloudRunnerLogger.log(`Successfully loaded provider: ${providerName}`);
return instance as ProviderInterface;
}
/**
* ProviderLoader class for backward compatibility and additional utilities
*/
export class ProviderLoader {
/**
* Dynamically loads a provider by name (wrapper around loadProvider function)
* @param providerName - The name of the provider to load
* @param buildParameters - Build parameters to pass to the provider constructor
* @returns Promise<ProviderInterface> - The loaded provider instance
* @throws Error if provider package is missing or doesn't implement ProviderInterface
*/
static async loadProvider(providerName: string, buildParameters: BuildParameters): Promise<ProviderInterface> {
return loadProvider(providerName, buildParameters);
}
/**
* Gets a list of available provider names
* @returns string[] - Array of available provider names
*/
static getAvailableProviders(): string[] {
return ['aws', 'k8s', 'test', 'local-docker', 'local-system', 'local'];
}
}

View File

@ -10,6 +10,7 @@ import Project from './project';
import Unity from './unity'; import Unity from './unity';
import Versioning from './versioning'; import Versioning from './versioning';
import CloudRunner from './cloud-runner/cloud-runner'; import CloudRunner from './cloud-runner/cloud-runner';
import loadProvider, { ProviderLoader } from './cloud-runner/providers/provider-loader';
export { export {
Action, Action,
@ -24,4 +25,6 @@ export {
Unity, Unity,
Versioning, Versioning,
CloudRunner as CloudRunner, CloudRunner as CloudRunner,
loadProvider,
ProviderLoader,
}; };