From ebb637d57edd2162a286a4f05e18ade717ecb9d2 Mon Sep 17 00:00:00 2001 From: Frostebite Date: Wed, 3 Sep 2025 20:34:08 +0100 Subject: [PATCH] feat: add dynamic provider loader --- src/model/cloud-runner/cloud-runner.ts | 12 ++++- .../providers/fixtures/invalid-provider.ts | 1 + .../providers/provider-loader.test.ts | 19 ++++++++ .../cloud-runner/providers/provider-loader.ts | 48 +++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 src/model/cloud-runner/providers/fixtures/invalid-provider.ts create mode 100644 src/model/cloud-runner/providers/provider-loader.test.ts create mode 100644 src/model/cloud-runner/providers/provider-loader.ts diff --git a/src/model/cloud-runner/cloud-runner.ts b/src/model/cloud-runner/cloud-runner.ts index d011f235..ccf2d9fb 100644 --- a/src/model/cloud-runner/cloud-runner.ts +++ b/src/model/cloud-runner/cloud-runner.ts @@ -13,6 +13,7 @@ import CloudRunnerEnvironmentVariable from './options/cloud-runner-environment-v import TestCloudRunner from './providers/test'; import LocalCloudRunner from './providers/local'; import LocalDockerCloudRunner from './providers/docker'; +import loadProvider from './providers/provider-loader'; import GitHub from '../github'; import SharedWorkspaceLocking from './services/core/shared-workspace-locking'; import { FollowLogStreamService } from './services/core/follow-log-stream-service'; @@ -38,7 +39,7 @@ class CloudRunner { if (CloudRunner.buildParameters.githubCheckId === ``) { CloudRunner.buildParameters.githubCheckId = await GitHub.createGitHubCheck(CloudRunner.buildParameters.buildGuid); } - CloudRunner.setupSelectedBuildPlatform(); + await CloudRunner.setupSelectedBuildPlatform(); CloudRunner.defaultSecrets = TaskParameterSerializer.readDefaultSecrets(); CloudRunner.cloudRunnerEnvironmentVariables = TaskParameterSerializer.createCloudRunnerEnvironmentVariables(buildParameters); @@ -62,7 +63,7 @@ class CloudRunner { FollowLogStreamService.Reset(); } - private static setupSelectedBuildPlatform() { + private static async setupSelectedBuildPlatform() { CloudRunnerLogger.log(`Cloud Runner platform selected ${CloudRunner.buildParameters.providerStrategy}`); switch (CloudRunner.buildParameters.providerStrategy) { case 'k8s': @@ -80,6 +81,13 @@ class CloudRunner { case 'local-system': CloudRunner.Provider = new LocalCloudRunner(); break; + default: + if (CloudRunner.buildParameters.providerStrategy !== 'local') { + CloudRunner.Provider = await loadProvider( + CloudRunner.buildParameters.providerStrategy, + CloudRunner.buildParameters, + ); + } } } diff --git a/src/model/cloud-runner/providers/fixtures/invalid-provider.ts b/src/model/cloud-runner/providers/fixtures/invalid-provider.ts new file mode 100644 index 00000000..aab2665f --- /dev/null +++ b/src/model/cloud-runner/providers/fixtures/invalid-provider.ts @@ -0,0 +1 @@ +export default class InvalidProvider {} diff --git a/src/model/cloud-runner/providers/provider-loader.test.ts b/src/model/cloud-runner/providers/provider-loader.test.ts new file mode 100644 index 00000000..a69d9a4d --- /dev/null +++ b/src/model/cloud-runner/providers/provider-loader.test.ts @@ -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', + ); + }); +}); diff --git a/src/model/cloud-runner/providers/provider-loader.ts b/src/model/cloud-runner/providers/provider-loader.ts new file mode 100644 index 00000000..0b730993 --- /dev/null +++ b/src/model/cloud-runner/providers/provider-loader.ts @@ -0,0 +1,48 @@ +import { ProviderInterface } from './provider-interface'; +import BuildParameters from '../../build-parameters'; + +/** + * 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 { + let importedModule: any; + try { + importedModule = await import(providerName); + } 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}'.`, + ); + } + } + + return instance as ProviderInterface; +}