hereya-cli 0.64.2 → 0.64.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -43
- package/dist/backend/cloud/cloud-backend.d.ts +70 -0
- package/dist/backend/cloud/cloud-backend.js +96 -0
- package/dist/backend/common.d.ts +4 -0
- package/dist/backend/common.js +1 -0
- package/dist/backend/index.d.ts +5 -1
- package/dist/backend/index.js +18 -2
- package/dist/commands/add/index.js +109 -2
- package/dist/commands/deploy/index.js +8 -2
- package/dist/commands/docker/run/index.js +1 -0
- package/dist/commands/down/index.js +111 -3
- package/dist/commands/env/index.js +1 -0
- package/dist/commands/executor/start/index.d.ts +11 -0
- package/dist/commands/executor/start/index.js +176 -0
- package/dist/commands/remove/index.js +138 -4
- package/dist/commands/run/index.js +1 -0
- package/dist/commands/undeploy/index.js +4 -1
- package/dist/commands/up/index.js +102 -5
- package/dist/commands/workspace/executor/install/index.d.ts +9 -0
- package/dist/commands/workspace/executor/install/index.js +110 -0
- package/dist/commands/workspace/executor/token/index.d.ts +8 -0
- package/dist/commands/workspace/executor/token/index.js +40 -0
- package/dist/commands/workspace/executor/uninstall/index.d.ts +9 -0
- package/dist/commands/workspace/executor/uninstall/index.js +102 -0
- package/dist/executor/context.d.ts +2 -0
- package/dist/executor/context.js +39 -0
- package/dist/executor/delegating.d.ts +15 -0
- package/dist/executor/delegating.js +50 -0
- package/dist/executor/index.d.ts +12 -3
- package/dist/executor/index.js +13 -2
- package/dist/executor/remote.d.ts +16 -0
- package/dist/executor/remote.js +168 -0
- package/dist/infrastructure/index.js +55 -22
- package/dist/lib/config/common.d.ts +5 -0
- package/dist/lib/config/simple.js +43 -24
- package/dist/lib/env/index.d.ts +9 -0
- package/dist/lib/env/index.js +101 -15
- package/dist/lib/package/index.d.ts +12 -0
- package/dist/lib/package/index.js +4 -0
- package/oclif.manifest.json +183 -25
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
|
|
3
|
+
import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
|
|
4
|
+
import { getBackend } from '../../../../backend/index.js';
|
|
5
|
+
import { getExecutor } from '../../../../executor/index.js';
|
|
6
|
+
import { getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
|
|
7
|
+
import { delay } from '../../../../lib/shell.js';
|
|
8
|
+
const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
|
|
9
|
+
export default class WorkspaceExecutorInstall extends Command {
|
|
10
|
+
static description = 'Install a remote executor into a workspace';
|
|
11
|
+
static flags = {
|
|
12
|
+
debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
|
|
13
|
+
workspace: Flags.string({
|
|
14
|
+
char: 'w',
|
|
15
|
+
description: 'name of the workspace',
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(WorkspaceExecutorInstall);
|
|
21
|
+
setDebug(flags.debug);
|
|
22
|
+
const myLogger = new ListrLogger({ useIcons: false });
|
|
23
|
+
const task = new Listr([
|
|
24
|
+
{
|
|
25
|
+
async task(_ctx, task) {
|
|
26
|
+
return task.newListr([
|
|
27
|
+
{
|
|
28
|
+
async task() {
|
|
29
|
+
const backend = await getBackend();
|
|
30
|
+
if (!(backend instanceof CloudBackend)) {
|
|
31
|
+
throw new TypeError('Remote executor requires cloud backend. Run `hereya login` first.');
|
|
32
|
+
}
|
|
33
|
+
const workspace$ = await backend.getWorkspace(flags.workspace);
|
|
34
|
+
if (!workspace$.found) {
|
|
35
|
+
throw new Error(`Workspace ${flags.workspace} not found`);
|
|
36
|
+
}
|
|
37
|
+
if (workspace$.hasError) {
|
|
38
|
+
throw new Error(workspace$.error);
|
|
39
|
+
}
|
|
40
|
+
if (workspace$.workspace.hasExecutor) {
|
|
41
|
+
throw new Error(`Workspace ${flags.workspace} already has an executor installed`);
|
|
42
|
+
}
|
|
43
|
+
await delay(500);
|
|
44
|
+
},
|
|
45
|
+
title: 'Validating workspace',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
async task(ctx) {
|
|
49
|
+
const backend = await getBackend();
|
|
50
|
+
const tokenResult = await backend.generateExecutorToken({
|
|
51
|
+
workspace: flags.workspace,
|
|
52
|
+
});
|
|
53
|
+
if (!tokenResult.success) {
|
|
54
|
+
throw new Error(`Failed to generate executor token: ${tokenResult.reason}`);
|
|
55
|
+
}
|
|
56
|
+
ctx.executorToken = tokenResult.token;
|
|
57
|
+
await delay(500);
|
|
58
|
+
},
|
|
59
|
+
title: 'Generating executor token',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
rendererOptions: {
|
|
63
|
+
persistentOutput: isDebug(),
|
|
64
|
+
},
|
|
65
|
+
async task(ctx, task) {
|
|
66
|
+
const executor$ = getExecutor();
|
|
67
|
+
if (!executor$.success) {
|
|
68
|
+
throw new Error(executor$.reason);
|
|
69
|
+
}
|
|
70
|
+
const { executor } = executor$;
|
|
71
|
+
const provisionOutput = await executor.provision({
|
|
72
|
+
logger: getLogger(task),
|
|
73
|
+
package: DEFAULT_EXECUTOR_PACKAGE,
|
|
74
|
+
parameters: {
|
|
75
|
+
EXECUTOR_TOKEN: ctx.executorToken,
|
|
76
|
+
WORKSPACE: flags.workspace,
|
|
77
|
+
},
|
|
78
|
+
skipDeploy: true,
|
|
79
|
+
});
|
|
80
|
+
if (!provisionOutput.success) {
|
|
81
|
+
throw new Error(provisionOutput.reason);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
title: 'Provisioning remote executor infrastructure',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
async task() {
|
|
88
|
+
const backend = await getBackend();
|
|
89
|
+
await backend.updateWorkspace({
|
|
90
|
+
hasExecutor: true,
|
|
91
|
+
name: flags.workspace,
|
|
92
|
+
});
|
|
93
|
+
await delay(500);
|
|
94
|
+
},
|
|
95
|
+
title: 'Registering executor on workspace',
|
|
96
|
+
},
|
|
97
|
+
], { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
|
|
98
|
+
},
|
|
99
|
+
title: `Installing executor on workspace ${flags.workspace}`,
|
|
100
|
+
},
|
|
101
|
+
], { concurrent: false });
|
|
102
|
+
try {
|
|
103
|
+
await task.run();
|
|
104
|
+
myLogger.log(ListrLogLevels.COMPLETED, 'Executor installed successfully');
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
this.error(`${error.message}\n\nSee ${getLogPath()} for more details`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class WorkspaceExecutorToken extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
};
|
|
7
|
+
run(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
|
|
3
|
+
import { getBackend } from '../../../../backend/index.js';
|
|
4
|
+
export default class WorkspaceExecutorToken extends Command {
|
|
5
|
+
static description = 'Generate a workspace-scoped executor token for testing the remote executor locally';
|
|
6
|
+
static flags = {
|
|
7
|
+
workspace: Flags.string({
|
|
8
|
+
char: 'w',
|
|
9
|
+
description: 'name of the workspace',
|
|
10
|
+
required: true,
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(WorkspaceExecutorToken);
|
|
15
|
+
const backend = await getBackend();
|
|
16
|
+
if (!(backend instanceof CloudBackend)) {
|
|
17
|
+
this.error('Remote executor requires cloud backend. Run `hereya login` first.');
|
|
18
|
+
}
|
|
19
|
+
const workspace$ = await backend.getWorkspace(flags.workspace);
|
|
20
|
+
if (!workspace$.found) {
|
|
21
|
+
this.error(`Workspace ${flags.workspace} not found`);
|
|
22
|
+
}
|
|
23
|
+
if (workspace$.hasError) {
|
|
24
|
+
this.error(workspace$.error);
|
|
25
|
+
}
|
|
26
|
+
const tokenResult = await backend.generateExecutorToken({
|
|
27
|
+
workspace: flags.workspace,
|
|
28
|
+
});
|
|
29
|
+
if (!tokenResult.success) {
|
|
30
|
+
this.error(`Failed to generate executor token: ${tokenResult.reason}`);
|
|
31
|
+
}
|
|
32
|
+
await backend.updateWorkspace({
|
|
33
|
+
hasExecutor: true,
|
|
34
|
+
name: flags.workspace,
|
|
35
|
+
});
|
|
36
|
+
this.log(tokenResult.token);
|
|
37
|
+
this.log('');
|
|
38
|
+
this.log(`Use this token with: hereya login --token ${tokenResult.token}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class WorkspaceExecutorUninstall extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
};
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
|
|
3
|
+
import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
|
|
4
|
+
import { getBackend } from '../../../../backend/index.js';
|
|
5
|
+
import { getExecutor } from '../../../../executor/index.js';
|
|
6
|
+
import { getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
|
|
7
|
+
import { delay } from '../../../../lib/shell.js';
|
|
8
|
+
const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
|
|
9
|
+
export default class WorkspaceExecutorUninstall extends Command {
|
|
10
|
+
static description = 'Uninstall the remote executor from a workspace';
|
|
11
|
+
static flags = {
|
|
12
|
+
debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
|
|
13
|
+
workspace: Flags.string({
|
|
14
|
+
char: 'w',
|
|
15
|
+
description: 'name of the workspace',
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(WorkspaceExecutorUninstall);
|
|
21
|
+
setDebug(flags.debug);
|
|
22
|
+
const myLogger = new ListrLogger({ useIcons: false });
|
|
23
|
+
const task = new Listr([
|
|
24
|
+
{
|
|
25
|
+
async task(_ctx, task) {
|
|
26
|
+
return task.newListr([
|
|
27
|
+
{
|
|
28
|
+
async task() {
|
|
29
|
+
const backend = await getBackend();
|
|
30
|
+
if (!(backend instanceof CloudBackend)) {
|
|
31
|
+
throw new TypeError('Remote executor requires cloud backend.');
|
|
32
|
+
}
|
|
33
|
+
const workspace$ = await backend.getWorkspace(flags.workspace);
|
|
34
|
+
if (!workspace$.found) {
|
|
35
|
+
throw new Error(`Workspace ${flags.workspace} not found`);
|
|
36
|
+
}
|
|
37
|
+
if (workspace$.hasError) {
|
|
38
|
+
throw new Error(workspace$.error);
|
|
39
|
+
}
|
|
40
|
+
if (!workspace$.workspace.hasExecutor) {
|
|
41
|
+
throw new Error(`Workspace ${flags.workspace} does not have an executor installed`);
|
|
42
|
+
}
|
|
43
|
+
await delay(500);
|
|
44
|
+
},
|
|
45
|
+
title: 'Validating workspace',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
rendererOptions: {
|
|
49
|
+
persistentOutput: isDebug(),
|
|
50
|
+
},
|
|
51
|
+
async task(_ctx, task) {
|
|
52
|
+
const executor$ = getExecutor();
|
|
53
|
+
if (!executor$.success) {
|
|
54
|
+
throw new Error(executor$.reason);
|
|
55
|
+
}
|
|
56
|
+
const { executor } = executor$;
|
|
57
|
+
const destroyOutput = await executor.destroy({
|
|
58
|
+
logger: getLogger(task),
|
|
59
|
+
package: DEFAULT_EXECUTOR_PACKAGE,
|
|
60
|
+
skipDeploy: true,
|
|
61
|
+
});
|
|
62
|
+
if (!destroyOutput.success) {
|
|
63
|
+
throw new Error(destroyOutput.reason);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
title: 'Destroying remote executor infrastructure',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
async task() {
|
|
70
|
+
const backend = await getBackend();
|
|
71
|
+
await backend.revokeExecutorToken({
|
|
72
|
+
workspace: flags.workspace,
|
|
73
|
+
});
|
|
74
|
+
await delay(500);
|
|
75
|
+
},
|
|
76
|
+
title: 'Revoking executor token',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
async task() {
|
|
80
|
+
const backend = await getBackend();
|
|
81
|
+
await backend.updateWorkspace({
|
|
82
|
+
hasExecutor: false,
|
|
83
|
+
name: flags.workspace,
|
|
84
|
+
});
|
|
85
|
+
await delay(500);
|
|
86
|
+
},
|
|
87
|
+
title: 'Removing executor from workspace',
|
|
88
|
+
},
|
|
89
|
+
], { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
|
|
90
|
+
},
|
|
91
|
+
title: `Uninstalling executor from workspace ${flags.workspace}`,
|
|
92
|
+
},
|
|
93
|
+
], { concurrent: false });
|
|
94
|
+
try {
|
|
95
|
+
await task.run();
|
|
96
|
+
myLogger.log(ListrLogLevels.COMPLETED, 'Executor uninstalled successfully');
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
this.error(`${error.message}\n\nSee ${getLogPath()} for more details`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getCloudCredentials, loadBackendConfig } from '../backend/config.js';
|
|
2
|
+
import { BackendType, getBackend } from '../backend/index.js';
|
|
3
|
+
import { resolveWorkspaceName } from '../lib/workspace-utils.js';
|
|
4
|
+
import { getExecutor } from './index.js';
|
|
5
|
+
export async function getExecutorForWorkspace(workspaceName, project) {
|
|
6
|
+
if (!workspaceName) {
|
|
7
|
+
return getExecutor();
|
|
8
|
+
}
|
|
9
|
+
const backendConfig = await loadBackendConfig();
|
|
10
|
+
if (backendConfig.current !== BackendType.Cloud) {
|
|
11
|
+
return getExecutor();
|
|
12
|
+
}
|
|
13
|
+
if (!backendConfig.cloud) {
|
|
14
|
+
return getExecutor();
|
|
15
|
+
}
|
|
16
|
+
const resolvedName = resolveWorkspaceName(workspaceName, project);
|
|
17
|
+
const backend = await getBackend();
|
|
18
|
+
const workspace$ = await backend.getWorkspace(resolvedName);
|
|
19
|
+
if (!workspace$.found || workspace$.hasError) {
|
|
20
|
+
return getExecutor();
|
|
21
|
+
}
|
|
22
|
+
const { workspace } = workspace$;
|
|
23
|
+
if (!workspace.hasExecutor) {
|
|
24
|
+
return getExecutor();
|
|
25
|
+
}
|
|
26
|
+
const credentials = await getCloudCredentials(backendConfig.cloud.clientId);
|
|
27
|
+
if (!credentials) {
|
|
28
|
+
return getExecutor();
|
|
29
|
+
}
|
|
30
|
+
return getExecutor({
|
|
31
|
+
cloudConfig: {
|
|
32
|
+
accessToken: credentials.accessToken,
|
|
33
|
+
clientId: backendConfig.cloud.clientId,
|
|
34
|
+
refreshToken: credentials.refreshToken,
|
|
35
|
+
url: backendConfig.cloud.url,
|
|
36
|
+
},
|
|
37
|
+
workspace: resolvedName,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Executor, ExecutorDestroyInput, ExecutorDestroyOutput, ExecutorImportInput, ExecutorImportOutput, ExecutorProvisionInput, ExecutorProvisionOutput, ExecutorResolveEnvValuesInput, ExecutorResolveEnvValuesOutput, ExecutorSetEnvVarInput, ExecutorSetEnvVarOutput, ExecutorUnsetEnvVarInput, ExecutorUnsetEnvVarOutput } from './interface.js';
|
|
2
|
+
import { LocalExecutor } from './local.js';
|
|
3
|
+
import { RemoteExecutor } from './remote.js';
|
|
4
|
+
export declare class DelegatingExecutor implements Executor {
|
|
5
|
+
private readonly localExecutor;
|
|
6
|
+
private readonly remoteExecutor;
|
|
7
|
+
constructor(localExecutor: LocalExecutor, remoteExecutor: RemoteExecutor);
|
|
8
|
+
destroy(input: ExecutorDestroyInput): Promise<ExecutorDestroyOutput>;
|
|
9
|
+
import(input: ExecutorImportInput): Promise<ExecutorImportOutput>;
|
|
10
|
+
provision(input: ExecutorProvisionInput): Promise<ExecutorProvisionOutput>;
|
|
11
|
+
resolveEnvValues(input: ExecutorResolveEnvValuesInput): Promise<ExecutorResolveEnvValuesOutput>;
|
|
12
|
+
setEnvVar(input: ExecutorSetEnvVarInput): Promise<ExecutorSetEnvVarOutput>;
|
|
13
|
+
unsetEnvVar(input: ExecutorUnsetEnvVarInput): Promise<ExecutorUnsetEnvVarOutput>;
|
|
14
|
+
private selectExecutor;
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { InfrastructureType } from '../infrastructure/common.js';
|
|
2
|
+
import { resolvePackage } from '../lib/package/index.js';
|
|
3
|
+
export class DelegatingExecutor {
|
|
4
|
+
localExecutor;
|
|
5
|
+
remoteExecutor;
|
|
6
|
+
constructor(localExecutor, remoteExecutor) {
|
|
7
|
+
this.localExecutor = localExecutor;
|
|
8
|
+
this.remoteExecutor = remoteExecutor;
|
|
9
|
+
}
|
|
10
|
+
async destroy(input) {
|
|
11
|
+
const executor = await this.selectExecutor(input.package, input.projectRootDir, input.isDeploying);
|
|
12
|
+
return executor.destroy(input);
|
|
13
|
+
}
|
|
14
|
+
async import(input) {
|
|
15
|
+
return this.localExecutor.import(input);
|
|
16
|
+
}
|
|
17
|
+
async provision(input) {
|
|
18
|
+
const executor = await this.selectExecutor(input.package, input.projectRootDir, input.isDeploying);
|
|
19
|
+
return executor.provision(input);
|
|
20
|
+
}
|
|
21
|
+
async resolveEnvValues(input) {
|
|
22
|
+
return this.remoteExecutor.resolveEnvValues(input);
|
|
23
|
+
}
|
|
24
|
+
async setEnvVar(input) {
|
|
25
|
+
return this.localExecutor.setEnvVar(input);
|
|
26
|
+
}
|
|
27
|
+
async unsetEnvVar(input) {
|
|
28
|
+
return this.localExecutor.unsetEnvVar(input);
|
|
29
|
+
}
|
|
30
|
+
async selectExecutor(pkg, projectRootDir, isDeploying) {
|
|
31
|
+
// Deploy operations always run locally
|
|
32
|
+
if (isDeploying) {
|
|
33
|
+
return this.localExecutor;
|
|
34
|
+
}
|
|
35
|
+
// Resolve package to check infra type
|
|
36
|
+
try {
|
|
37
|
+
const resolved = await resolvePackage({
|
|
38
|
+
package: pkg,
|
|
39
|
+
projectRootDir,
|
|
40
|
+
});
|
|
41
|
+
if (resolved.found && resolved.metadata.infra !== InfrastructureType.local) {
|
|
42
|
+
return this.remoteExecutor;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// If we can't resolve, fall back to local
|
|
47
|
+
}
|
|
48
|
+
return this.localExecutor;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/executor/index.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import { Executor } from
|
|
2
|
-
import { LocalExecutor } from
|
|
1
|
+
import { Executor } from './interface.js';
|
|
2
|
+
import { LocalExecutor } from './local.js';
|
|
3
3
|
export declare const localExecutor: LocalExecutor;
|
|
4
|
-
export
|
|
4
|
+
export type GetExecutorInput = {
|
|
5
|
+
cloudConfig?: {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
refreshToken: string;
|
|
9
|
+
url: string;
|
|
10
|
+
};
|
|
11
|
+
workspace?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function getExecutor(input?: GetExecutorInput): GetExecutorOutput;
|
|
5
14
|
export type GetExecutorOutput = {
|
|
6
15
|
executor: Executor;
|
|
7
16
|
success: true;
|
package/dist/executor/index.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CloudBackend } from '../backend/cloud/cloud-backend.js';
|
|
2
|
+
import { DelegatingExecutor } from './delegating.js';
|
|
3
|
+
import { LocalExecutor } from './local.js';
|
|
4
|
+
import { RemoteExecutor } from './remote.js';
|
|
2
5
|
export const localExecutor = new LocalExecutor();
|
|
3
|
-
export function getExecutor() {
|
|
6
|
+
export function getExecutor(input) {
|
|
7
|
+
if (input?.cloudConfig && input?.workspace) {
|
|
8
|
+
const cloudBackend = new CloudBackend(input.cloudConfig);
|
|
9
|
+
const remoteExecutor = new RemoteExecutor(cloudBackend, input.workspace);
|
|
10
|
+
return {
|
|
11
|
+
executor: new DelegatingExecutor(localExecutor, remoteExecutor),
|
|
12
|
+
success: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
4
15
|
return {
|
|
5
16
|
executor: localExecutor,
|
|
6
17
|
success: true,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CloudBackend } from '../backend/cloud/cloud-backend.js';
|
|
2
|
+
import { Executor, ExecutorDestroyInput, ExecutorDestroyOutput, ExecutorImportInput, ExecutorImportOutput, ExecutorProvisionInput, ExecutorProvisionOutput, ExecutorResolveEnvValuesInput, ExecutorResolveEnvValuesOutput, ExecutorSetEnvVarInput, ExecutorSetEnvVarOutput, ExecutorUnsetEnvVarInput, ExecutorUnsetEnvVarOutput } from './interface.js';
|
|
3
|
+
export declare class RemoteExecutor implements Executor {
|
|
4
|
+
private readonly cloudBackend;
|
|
5
|
+
private readonly workspace;
|
|
6
|
+
private localExecutor;
|
|
7
|
+
constructor(cloudBackend: CloudBackend, workspace: string);
|
|
8
|
+
destroy(input: ExecutorDestroyInput): Promise<ExecutorDestroyOutput>;
|
|
9
|
+
import(input: ExecutorImportInput): Promise<ExecutorImportOutput>;
|
|
10
|
+
provision(input: ExecutorProvisionInput): Promise<ExecutorProvisionOutput>;
|
|
11
|
+
resolveEnvValues(input: ExecutorResolveEnvValuesInput): Promise<ExecutorResolveEnvValuesOutput>;
|
|
12
|
+
setEnvVar(input: ExecutorSetEnvVarInput): Promise<ExecutorSetEnvVarOutput>;
|
|
13
|
+
unsetEnvVar(input: ExecutorUnsetEnvVarInput): Promise<ExecutorUnsetEnvVarOutput>;
|
|
14
|
+
private executeRemote;
|
|
15
|
+
private resolveEnvRemotely;
|
|
16
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { LocalExecutor } from './local.js';
|
|
2
|
+
export class RemoteExecutor {
|
|
3
|
+
cloudBackend;
|
|
4
|
+
workspace;
|
|
5
|
+
localExecutor = new LocalExecutor();
|
|
6
|
+
constructor(cloudBackend, workspace) {
|
|
7
|
+
this.cloudBackend = cloudBackend;
|
|
8
|
+
this.workspace = workspace;
|
|
9
|
+
}
|
|
10
|
+
async destroy(input) {
|
|
11
|
+
return this.executeRemote('destroy', input);
|
|
12
|
+
}
|
|
13
|
+
async import(input) {
|
|
14
|
+
return this.localExecutor.import(input);
|
|
15
|
+
}
|
|
16
|
+
async provision(input) {
|
|
17
|
+
return this.executeRemote('provision', input);
|
|
18
|
+
}
|
|
19
|
+
async resolveEnvValues(input) {
|
|
20
|
+
const localEntries = {};
|
|
21
|
+
const remoteEntries = {};
|
|
22
|
+
for (const [key, value] of Object.entries(input.env)) {
|
|
23
|
+
const colonIndex = value.indexOf(':');
|
|
24
|
+
if (colonIndex === -1) {
|
|
25
|
+
// No colon - plain value, resolve locally
|
|
26
|
+
localEntries[key] = value;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const prefix = value.slice(0, colonIndex);
|
|
30
|
+
if (prefix === 'local') {
|
|
31
|
+
localEntries[key] = value;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
remoteEntries[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Resolve local entries via local executor
|
|
39
|
+
const localResolved = Object.keys(localEntries).length > 0
|
|
40
|
+
? await this.localExecutor.resolveEnvValues({ env: localEntries, markSecret: input.markSecret })
|
|
41
|
+
: {};
|
|
42
|
+
// Resolve remote entries via cloud backend
|
|
43
|
+
const remoteResolved = Object.keys(remoteEntries).length > 0
|
|
44
|
+
? await this.resolveEnvRemotely(remoteEntries, input.markSecret)
|
|
45
|
+
: {};
|
|
46
|
+
return { ...localResolved, ...remoteResolved };
|
|
47
|
+
}
|
|
48
|
+
async setEnvVar(input) {
|
|
49
|
+
return this.localExecutor.setEnvVar(input);
|
|
50
|
+
}
|
|
51
|
+
async unsetEnvVar(input) {
|
|
52
|
+
return this.localExecutor.unsetEnvVar(input);
|
|
53
|
+
}
|
|
54
|
+
async executeRemote(type, input) {
|
|
55
|
+
// Submit job to cloud
|
|
56
|
+
const submitResult = await this.cloudBackend.submitExecutorJob({
|
|
57
|
+
payload: {
|
|
58
|
+
isDeploying: input.isDeploying,
|
|
59
|
+
package: input.package,
|
|
60
|
+
parameters: input.parameters,
|
|
61
|
+
project: input.project,
|
|
62
|
+
projectEnv: input.projectEnv,
|
|
63
|
+
skipDeploy: input.skipDeploy,
|
|
64
|
+
workspace: input.workspace,
|
|
65
|
+
},
|
|
66
|
+
type,
|
|
67
|
+
workspace: this.workspace,
|
|
68
|
+
});
|
|
69
|
+
if (!submitResult.success) {
|
|
70
|
+
return { reason: `Failed to submit remote executor job: ${submitResult.reason}`, success: false };
|
|
71
|
+
}
|
|
72
|
+
const { jobId } = submitResult;
|
|
73
|
+
input.logger?.info?.(`Remote executor job submitted (${jobId}). Waiting for result...`);
|
|
74
|
+
// Poll for result
|
|
75
|
+
const timeout = 10 * 60 * 1000; // 10 minutes
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
let lastLogLength = 0;
|
|
78
|
+
let lastStatus = 'pending';
|
|
79
|
+
while (Date.now() - startTime < timeout) {
|
|
80
|
+
// eslint-disable-next-line no-await-in-loop
|
|
81
|
+
const statusResult = await this.cloudBackend.getExecutorJobStatus({
|
|
82
|
+
jobId,
|
|
83
|
+
lastStatus,
|
|
84
|
+
poll: true,
|
|
85
|
+
workspace: this.workspace,
|
|
86
|
+
});
|
|
87
|
+
if (!statusResult.success) {
|
|
88
|
+
return { reason: `Failed to get job status: ${statusResult.reason}`, success: false };
|
|
89
|
+
}
|
|
90
|
+
const { job } = statusResult;
|
|
91
|
+
lastStatus = job.status;
|
|
92
|
+
// Forward new logs
|
|
93
|
+
if (job.logs && job.logs.length > lastLogLength) {
|
|
94
|
+
const newLogs = job.logs.slice(lastLogLength);
|
|
95
|
+
if (newLogs.trim()) {
|
|
96
|
+
input.logger?.info?.(newLogs);
|
|
97
|
+
}
|
|
98
|
+
lastLogLength = job.logs.length;
|
|
99
|
+
}
|
|
100
|
+
// Check if job is done
|
|
101
|
+
if (job.status === 'completed') {
|
|
102
|
+
if (job.result) {
|
|
103
|
+
return job.result;
|
|
104
|
+
}
|
|
105
|
+
return { reason: 'Job completed but no result returned', success: false };
|
|
106
|
+
}
|
|
107
|
+
if (job.status === 'failed') {
|
|
108
|
+
const reason = job.result?.reason || 'Remote executor job failed';
|
|
109
|
+
return { reason, success: false };
|
|
110
|
+
}
|
|
111
|
+
// Small delay between polls (server-side long polling handles most of the wait)
|
|
112
|
+
// eslint-disable-next-line no-await-in-loop
|
|
113
|
+
await new Promise(resolve => {
|
|
114
|
+
setTimeout(resolve, 1000);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { reason: 'Remote executor job timed out after 10 minutes', success: false };
|
|
118
|
+
}
|
|
119
|
+
async resolveEnvRemotely(env, markSecret) {
|
|
120
|
+
const submitResult = await this.cloudBackend.submitExecutorJob({
|
|
121
|
+
payload: { env, markSecret },
|
|
122
|
+
type: 'resolve-env',
|
|
123
|
+
workspace: this.workspace,
|
|
124
|
+
});
|
|
125
|
+
if (!submitResult.success) {
|
|
126
|
+
console.warn(`Failed to submit resolve-env job: ${submitResult.reason}. Returning unresolved values.`);
|
|
127
|
+
return env;
|
|
128
|
+
}
|
|
129
|
+
const { jobId } = submitResult;
|
|
130
|
+
// Poll for result with 2-minute timeout
|
|
131
|
+
const timeout = 2 * 60 * 1000;
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
let lastStatus = 'pending';
|
|
134
|
+
while (Date.now() - startTime < timeout) {
|
|
135
|
+
// eslint-disable-next-line no-await-in-loop
|
|
136
|
+
const statusResult = await this.cloudBackend.getExecutorJobStatus({
|
|
137
|
+
jobId,
|
|
138
|
+
lastStatus,
|
|
139
|
+
poll: true,
|
|
140
|
+
workspace: this.workspace,
|
|
141
|
+
});
|
|
142
|
+
if (!statusResult.success) {
|
|
143
|
+
console.warn(`Failed to get resolve-env job status: ${statusResult.reason}. Returning unresolved values.`);
|
|
144
|
+
return env;
|
|
145
|
+
}
|
|
146
|
+
const { job } = statusResult;
|
|
147
|
+
lastStatus = job.status;
|
|
148
|
+
if (job.status === 'completed') {
|
|
149
|
+
if (job.result?.env) {
|
|
150
|
+
return job.result.env;
|
|
151
|
+
}
|
|
152
|
+
console.warn('resolve-env job completed but no env in result. Returning unresolved values.');
|
|
153
|
+
return env;
|
|
154
|
+
}
|
|
155
|
+
if (job.status === 'failed') {
|
|
156
|
+
const reason = job.result?.reason || 'resolve-env job failed';
|
|
157
|
+
console.warn(`resolve-env job failed: ${reason}. Returning unresolved values.`);
|
|
158
|
+
return env;
|
|
159
|
+
}
|
|
160
|
+
// eslint-disable-next-line no-await-in-loop
|
|
161
|
+
await new Promise(resolve => {
|
|
162
|
+
setTimeout(resolve, 1000);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
console.warn('resolve-env job timed out after 2 minutes. Returning unresolved values.');
|
|
166
|
+
return env;
|
|
167
|
+
}
|
|
168
|
+
}
|