Skip to content

Commit 3e63c08

Browse files
authored
CLI: support credential maps (#1191)
* cli: added credential map utility * integrate credential map into cli * project: map project_credential_id to configuration * delete test * add test * tweak error messages * tidy * changeset * integration test * fix test * support yaml credential map * skip flaky engine test
1 parent 8ad490f commit 3e63c08

File tree

22 files changed

+386
-648
lines changed

22 files changed

+386
-648
lines changed

.changeset/orange-pigs-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openfn/cli': minor
3+
---
4+
5+
Allow credential map, as json or yaml, to be passed via --credentials

.changeset/public-dragons-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openfn/project': patch
3+
---
4+
5+
Map project_credential_id to configuration

integration-tests/cli/test/execute-workflow.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ test.serial(
147147
}
148148
);
149149

150+
test.serial(
151+
`openfn ${jobsPath}/wf-creds.json --credentials ${jobsPath}/creds.json`,
152+
async (t) => {
153+
const { err, stdout, stderr } = await run(t.title);
154+
console.log({ stdout, stderr });
155+
t.falsy(err);
156+
157+
const out = getJSON();
158+
t.is(out.value, 'admin:admin');
159+
}
160+
);
161+
150162
test.serial(
151163
`openfn ${jobsPath}/wf-errors.json -S "{ \\"data\\": { \\"number\\": 2 } }"`,
152164
async (t) => {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fn((s) => {
2+
s.value = `${s.configuration.user}:${s.configuration.password}`;
3+
return s;
4+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"08089249-0890-4a73-8799-e2ec2b9e5d77": {
3+
"user": "admin",
4+
"password": "admin"
5+
}
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"workflow": {
3+
"steps": [
4+
{
5+
"adaptor": "common",
6+
"configuration": "08089249-0890-4a73-8799-e2ec2b9e5d77",
7+
"expression": "creds.js"
8+
}
9+
]
10+
}
11+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* utility to take a workflow and a credential map
3+
* and apply credentials to each step
4+
*/
5+
6+
import { ExecutionPlan } from '@openfn/lexicon';
7+
import { Logger } from '../util';
8+
9+
type JobId = string;
10+
11+
export type CredentialMap = Record<JobId, any>;
12+
13+
const applyCredentialMap = (
14+
plan: ExecutionPlan,
15+
map: CredentialMap = {},
16+
logger?: Logger
17+
) => {
18+
const stepsWithCredentialIds = plan.workflow.steps.filter(
19+
(step: any) =>
20+
typeof step.configuration === 'string' &&
21+
!step.configuration.endsWith('.json')
22+
) as { configuration: string; name?: string; id: string }[];
23+
24+
const unmapped: Record<string, true> = {};
25+
26+
for (const step of stepsWithCredentialIds) {
27+
if (map[step.configuration]) {
28+
logger?.debug(
29+
`Applying credential ${step.configuration} to "${step.name ?? step.id}"`
30+
);
31+
step.configuration = map[step.configuration];
32+
} else {
33+
unmapped[step.configuration] = true;
34+
// @ts-ignore
35+
delete step.configuration;
36+
}
37+
}
38+
39+
if (Object.keys(unmapped).length) {
40+
logger?.warn(
41+
`WARNING: credential IDs were found in the workflow, but values have not been provided:`
42+
);
43+
logger?.warn(' ', Object.keys(unmapped).join(','));
44+
if (map) {
45+
logger?.warn(
46+
'If the workflow fails, add these credentials to the credential map'
47+
);
48+
} else {
49+
// TODO if running from project file this might be bad advice
50+
logger?.warn('Pass a credential map with --credentials');
51+
}
52+
}
53+
};
54+
55+
export default applyCredentialMap;

packages/cli/src/execute/command.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type ExecuteOptions = Required<
1313
| 'cacheSteps'
1414
| 'command'
1515
| 'compile'
16+
| 'credentials'
1617
| 'expandAdaptors'
1718
| 'end'
1819
| 'immutable'
@@ -46,6 +47,7 @@ const options = [
4647
o.autoinstall,
4748
o.cacheSteps,
4849
o.compile,
50+
o.credentials,
4951
o.end,
5052
o.ignoreImports,
5153
o.immutable,

packages/cli/src/execute/handler.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import type { ExecutionPlan } from '@openfn/lexicon';
2+
import { yamlToJson } from '@openfn/project';
3+
import { readFile } from 'node:fs/promises';
4+
import path from 'node:path';
25

36
import type { ExecuteOptions } from './command';
47
import execute from './execute';
58
import serializeOutput from './serialize-output';
69
import getAutoinstallTargets from './get-autoinstall-targets';
10+
import applyCredentialMap from './apply-credential-map';
711

812
import { install } from '../repo/handler';
913
import compile from '../compile/compile';
@@ -44,14 +48,43 @@ const matchStep = (
4448
return '';
4549
};
4650

51+
const loadAndApplyCredentialMap = async (
52+
plan: ExecutionPlan,
53+
options: ExecuteOptions,
54+
logger: Logger
55+
) => {
56+
let creds = {};
57+
if (options.credentials) {
58+
try {
59+
const credsRaw = await readFile(
60+
path.resolve(options.credentials),
61+
'utf8'
62+
);
63+
if (options.credentials.endsWith('.json')) {
64+
creds = JSON.parse(credsRaw);
65+
} else {
66+
creds = yamlToJson(credsRaw);
67+
}
68+
} catch (e) {
69+
logger.error('Error processing credential map:');
70+
logger.error(e);
71+
// probably want to exist if the credential map is invalid
72+
process.exitCode = 1;
73+
return;
74+
}
75+
logger.info('Credential map loaded ');
76+
}
77+
return applyCredentialMap(plan, creds, logger);
78+
};
79+
4780
const executeHandler = async (options: ExecuteOptions, logger: Logger) => {
4881
const start = new Date().getTime();
4982
assertPath(options.path);
5083
await validateAdaptors(options, logger);
5184

5285
let plan = await loadPlan(options, logger);
5386
validatePlan(plan, logger);
54-
87+
await loadAndApplyCredentialMap(plan, options, logger);
5588
if (options.cacheSteps) {
5689
await clearCache(plan, options, logger);
5790
}

packages/cli/src/options.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type Opts = {
3131
compile?: boolean;
3232
configPath?: string;
3333
confirm?: boolean;
34+
credentials?: string;
3435
describe?: string;
3536
end?: string; // workflow end node
3637
env?: string;
@@ -245,6 +246,14 @@ export const configPath: CLIOption = {
245246
},
246247
};
247248

249+
export const credentials: CLIOption = {
250+
name: 'credentials',
251+
yargs: {
252+
alias: ['creds'],
253+
description: 'A path which points to a credential map',
254+
},
255+
};
256+
248257
export const describe: CLIOption = {
249258
name: 'describe',
250259
yargs: {

0 commit comments

Comments
 (0)