@enspirit/emb 0.15.0 → 0.17.5
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 +218 -43
- package/bin/release +122 -0
- package/dist/src/cli/abstract/BaseCommand.d.ts +1 -0
- package/dist/src/cli/abstract/BaseCommand.js +23 -4
- package/dist/src/cli/abstract/FlavouredCommand.d.ts +1 -0
- package/dist/src/cli/abstract/KubernetesCommand.d.ts +1 -0
- package/dist/src/cli/commands/components/logs.d.ts +2 -1
- package/dist/src/cli/commands/components/logs.js +21 -24
- package/dist/src/cli/commands/secrets/index.d.ts +14 -0
- package/dist/src/cli/commands/secrets/index.js +71 -0
- package/dist/src/cli/commands/secrets/providers.d.ts +12 -0
- package/dist/src/cli/commands/secrets/providers.js +50 -0
- package/dist/src/cli/commands/secrets/validate.d.ts +18 -0
- package/dist/src/cli/commands/secrets/validate.js +145 -0
- package/dist/src/cli/commands/tasks/run.js +6 -1
- package/dist/src/cli/hooks/init.js +7 -1
- package/dist/src/config/index.d.ts +10 -1
- package/dist/src/config/index.js +28 -3
- package/dist/src/config/schema.d.ts +7 -4
- package/dist/src/config/schema.json +173 -9
- package/dist/src/context.d.ts +9 -0
- package/dist/src/context.js +19 -0
- package/dist/src/docker/compose/operations/ComposeLogsOperation.d.ts +21 -0
- package/dist/src/docker/compose/operations/ComposeLogsOperation.js +85 -0
- package/dist/src/docker/compose/operations/index.d.ts +1 -0
- package/dist/src/docker/compose/operations/index.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/monorepo/monorepo.js +13 -5
- package/dist/src/monorepo/operations/shell/ExecuteLocalCommandOperation.js +40 -10
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.d.ts +1 -1
- package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +1 -1
- package/dist/src/monorepo/plugins/VaultPlugin.d.ts +46 -0
- package/dist/src/monorepo/plugins/VaultPlugin.js +91 -0
- package/dist/src/monorepo/plugins/index.d.ts +1 -0
- package/dist/src/monorepo/plugins/index.js +3 -0
- package/dist/src/secrets/SecretDiscovery.d.ts +46 -0
- package/dist/src/secrets/SecretDiscovery.js +82 -0
- package/dist/src/secrets/SecretManager.d.ts +52 -0
- package/dist/src/secrets/SecretManager.js +75 -0
- package/dist/src/secrets/SecretProvider.d.ts +45 -0
- package/dist/src/secrets/SecretProvider.js +38 -0
- package/dist/src/secrets/index.d.ts +3 -0
- package/dist/src/secrets/index.js +3 -0
- package/dist/src/secrets/providers/VaultOidcHelper.d.ts +39 -0
- package/dist/src/secrets/providers/VaultOidcHelper.js +226 -0
- package/dist/src/secrets/providers/VaultProvider.d.ts +74 -0
- package/dist/src/secrets/providers/VaultProvider.js +266 -0
- package/dist/src/secrets/providers/VaultTokenCache.d.ts +60 -0
- package/dist/src/secrets/providers/VaultTokenCache.js +188 -0
- package/dist/src/secrets/providers/index.d.ts +2 -0
- package/dist/src/secrets/providers/index.js +2 -0
- package/dist/src/types.d.ts +2 -0
- package/dist/src/utils/TemplateExpander.d.ts +13 -1
- package/dist/src/utils/TemplateExpander.js +68 -15
- package/oclif.manifest.json +578 -173
- package/package.json +12 -5
package/bin/release
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Colors for output
|
|
5
|
+
RED='\033[0;31m'
|
|
6
|
+
GREEN='\033[0;32m'
|
|
7
|
+
YELLOW='\033[1;33m'
|
|
8
|
+
NC='\033[0m' # No Color
|
|
9
|
+
|
|
10
|
+
error() {
|
|
11
|
+
echo -e "${RED}Error: $1${NC}" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
info() {
|
|
16
|
+
echo -e "${GREEN}$1${NC}"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
warn() {
|
|
20
|
+
echo -e "${YELLOW}$1${NC}"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Check argument
|
|
24
|
+
if [ $# -ne 1 ]; then
|
|
25
|
+
echo "Usage: $0 <patch|minor|major>"
|
|
26
|
+
echo ""
|
|
27
|
+
echo "Examples:"
|
|
28
|
+
echo " $0 patch # 0.17.0 -> 0.17.1"
|
|
29
|
+
echo " $0 minor # 0.17.0 -> 0.18.0"
|
|
30
|
+
echo " $0 major # 0.17.0 -> 1.0.0"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
BUMP_TYPE="$1"
|
|
35
|
+
|
|
36
|
+
if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "major" ]]; then
|
|
37
|
+
error "Invalid bump type: $BUMP_TYPE. Must be one of: patch, minor, major"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Check we're on master branch
|
|
41
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
42
|
+
if [ "$CURRENT_BRANCH" != "master" ]; then
|
|
43
|
+
error "Must be on master branch (currently on: $CURRENT_BRANCH)"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Check for pristine git status
|
|
47
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
48
|
+
error "Working directory is not clean. Commit or stash your changes first.\n$(git status --short)"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Check we're up to date with remote
|
|
52
|
+
git fetch origin master --quiet
|
|
53
|
+
LOCAL=$(git rev-parse HEAD)
|
|
54
|
+
REMOTE=$(git rev-parse origin/master)
|
|
55
|
+
if [ "$LOCAL" != "$REMOTE" ]; then
|
|
56
|
+
error "Local master is not up to date with origin/master. Run 'git pull' first."
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Get current version
|
|
60
|
+
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
61
|
+
info "Current version: $CURRENT_VERSION"
|
|
62
|
+
|
|
63
|
+
# Calculate new version
|
|
64
|
+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
|
65
|
+
|
|
66
|
+
case "$BUMP_TYPE" in
|
|
67
|
+
major)
|
|
68
|
+
NEW_VERSION="$((MAJOR + 1)).0.0"
|
|
69
|
+
;;
|
|
70
|
+
minor)
|
|
71
|
+
NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
|
|
72
|
+
;;
|
|
73
|
+
patch)
|
|
74
|
+
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
|
|
78
|
+
info "New version: $NEW_VERSION"
|
|
79
|
+
|
|
80
|
+
# Check tag doesn't already exist
|
|
81
|
+
if git rev-parse "$NEW_VERSION" >/dev/null 2>&1; then
|
|
82
|
+
error "Tag $NEW_VERSION already exists"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Confirm with user
|
|
86
|
+
echo ""
|
|
87
|
+
warn "This will:"
|
|
88
|
+
echo " 1. Update package.json version to $NEW_VERSION"
|
|
89
|
+
echo " 2. Commit the change"
|
|
90
|
+
echo " 3. Create tag $NEW_VERSION"
|
|
91
|
+
echo " 4. Push to origin (commit + tag)"
|
|
92
|
+
echo ""
|
|
93
|
+
read -p "Continue? [y/N] " -n 1 -r
|
|
94
|
+
echo ""
|
|
95
|
+
|
|
96
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
97
|
+
echo "Aborted."
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Update package.json version
|
|
102
|
+
info "Updating package.json..."
|
|
103
|
+
npm version "$NEW_VERSION" --no-git-tag-version --allow-same-version
|
|
104
|
+
|
|
105
|
+
# Commit
|
|
106
|
+
info "Committing..."
|
|
107
|
+
git add package.json
|
|
108
|
+
git commit -m "Bump to $NEW_VERSION"
|
|
109
|
+
|
|
110
|
+
# Create tag
|
|
111
|
+
info "Creating tag $NEW_VERSION..."
|
|
112
|
+
git tag "$NEW_VERSION"
|
|
113
|
+
|
|
114
|
+
# Push
|
|
115
|
+
info "Pushing to origin..."
|
|
116
|
+
git push origin master
|
|
117
|
+
git push origin "$NEW_VERSION"
|
|
118
|
+
|
|
119
|
+
echo ""
|
|
120
|
+
info "Release $NEW_VERSION complete!"
|
|
121
|
+
echo "GitHub Actions will now build, test, and publish to npm."
|
|
122
|
+
echo "Monitor the release at: https://github.com/enspirit/emb/actions"
|
|
@@ -4,6 +4,7 @@ export declare abstract class BaseCommand extends Command {
|
|
|
4
4
|
protected context: EmbContext;
|
|
5
5
|
static baseFlags: {
|
|
6
6
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
root: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
};
|
|
8
9
|
init(): Promise<void>;
|
|
9
10
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { DockerComposeClient, getContext, setContext } from '../../index.js';
|
|
1
|
+
import { DockerComposeClient, getContext, isContextStale, resetContext, setContext, } from '../../index.js';
|
|
2
2
|
import { Command, Flags } from '@oclif/core';
|
|
3
3
|
import Dockerode from 'dockerode';
|
|
4
4
|
import { loadConfig } from '../../config/index.js';
|
|
5
5
|
import { createKubernetesClient } from '../../kubernetes/client.js';
|
|
6
6
|
import { Monorepo } from '../../monorepo/monorepo.js';
|
|
7
|
+
import { SecretManager } from '../../secrets/index.js';
|
|
7
8
|
import { withMarker } from '../utils.js';
|
|
8
9
|
export class BaseCommand extends Command {
|
|
9
10
|
context;
|
|
@@ -12,15 +13,34 @@ export class BaseCommand extends Command {
|
|
|
12
13
|
name: 'verbose',
|
|
13
14
|
allowNo: true,
|
|
14
15
|
}),
|
|
16
|
+
root: Flags.string({
|
|
17
|
+
char: 'C',
|
|
18
|
+
description: 'Run as if emb was started in <path>. Can also be set via EMB_ROOT env var.',
|
|
19
|
+
name: 'root',
|
|
20
|
+
required: false,
|
|
21
|
+
}),
|
|
15
22
|
};
|
|
16
23
|
async init() {
|
|
17
24
|
const { flags } = await this.parse();
|
|
18
25
|
await super.init();
|
|
26
|
+
// Reset context if EMB_ROOT changed (e.g., in tests switching between examples)
|
|
27
|
+
if (isContextStale()) {
|
|
28
|
+
resetContext();
|
|
29
|
+
}
|
|
19
30
|
if (getContext()) {
|
|
20
31
|
return;
|
|
21
32
|
}
|
|
22
33
|
try {
|
|
23
|
-
const { rootDir, config } = await withMarker('emb:config', 'load', () => loadConfig());
|
|
34
|
+
const { rootDir, config } = await withMarker('emb:config', 'load', () => loadConfig({ root: flags.root }));
|
|
35
|
+
// Create SecretManager early so plugins can register providers during init
|
|
36
|
+
const secrets = new SecretManager();
|
|
37
|
+
// Set a partial context before monorepo init so plugins can access secrets
|
|
38
|
+
const partialContext = {
|
|
39
|
+
docker: new Dockerode(),
|
|
40
|
+
kubernetes: createKubernetesClient(),
|
|
41
|
+
secrets,
|
|
42
|
+
};
|
|
43
|
+
setContext(partialContext);
|
|
24
44
|
const monorepo = await withMarker('emb:monorepo', 'init', () => {
|
|
25
45
|
return new Monorepo(config, rootDir).init();
|
|
26
46
|
});
|
|
@@ -29,10 +49,9 @@ export class BaseCommand extends Command {
|
|
|
29
49
|
}
|
|
30
50
|
const compose = new DockerComposeClient(monorepo);
|
|
31
51
|
this.context = setContext({
|
|
32
|
-
|
|
52
|
+
...partialContext,
|
|
33
53
|
monorepo,
|
|
34
54
|
compose,
|
|
35
|
-
kubernetes: createKubernetesClient(),
|
|
36
55
|
});
|
|
37
56
|
}
|
|
38
57
|
catch (error) {
|
|
@@ -6,6 +6,7 @@ export declare abstract class FlavoredCommand<T extends typeof Command> extends
|
|
|
6
6
|
static baseFlags: {
|
|
7
7
|
flavor: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
8
8
|
verbose: Interfaces.BooleanFlag<boolean>;
|
|
9
|
+
root: Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>;
|
|
9
10
|
};
|
|
10
11
|
static enableJsonFlag: boolean;
|
|
11
12
|
protected args: Args<T>;
|
|
@@ -3,5 +3,6 @@ export declare abstract class KubernetesCommand extends BaseCommand {
|
|
|
3
3
|
static baseFlags: {
|
|
4
4
|
namespace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
5
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
root: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
};
|
|
7
8
|
}
|
|
@@ -4,11 +4,12 @@ export default class ComponentsLogs extends BaseCommand {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static enableJsonFlag: boolean;
|
|
6
6
|
static examples: string[];
|
|
7
|
+
static strict: boolean;
|
|
7
8
|
static flags: {
|
|
8
9
|
follow: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
};
|
|
10
11
|
static args: {
|
|
11
|
-
component: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
|
+
component: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
12
13
|
};
|
|
13
14
|
run(): Promise<void>;
|
|
14
15
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand, getContext } from '../../index.js';
|
|
3
|
+
import { ComposeLogsOperation } from '../../../docker/index.js';
|
|
3
4
|
export default class ComponentsLogs extends BaseCommand {
|
|
4
5
|
static aliases = ['logs'];
|
|
5
6
|
static description = 'Get components logs.';
|
|
6
7
|
static enableJsonFlag = false;
|
|
7
|
-
static examples = [
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> backend',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> backend frontend',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --no-follow backend',
|
|
13
|
+
];
|
|
14
|
+
static strict = false;
|
|
8
15
|
static flags = {
|
|
9
16
|
follow: Flags.boolean({
|
|
10
17
|
name: 'follow',
|
|
@@ -17,32 +24,22 @@ export default class ComponentsLogs extends BaseCommand {
|
|
|
17
24
|
static args = {
|
|
18
25
|
component: Args.string({
|
|
19
26
|
name: 'component',
|
|
20
|
-
description: 'The component you want to see the logs of',
|
|
21
|
-
required:
|
|
27
|
+
description: 'The component(s) you want to see the logs of (all if omitted)',
|
|
28
|
+
required: false,
|
|
22
29
|
}),
|
|
23
30
|
};
|
|
24
31
|
async run() {
|
|
25
|
-
const { flags,
|
|
26
|
-
const { monorepo
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const { flags, argv } = await this.parse(ComponentsLogs);
|
|
33
|
+
const { monorepo } = await getContext();
|
|
34
|
+
const componentNames = argv;
|
|
35
|
+
// Validate that all specified components exist
|
|
36
|
+
const services = componentNames.map((name) => {
|
|
37
|
+
const component = monorepo.component(name);
|
|
38
|
+
return component.name;
|
|
39
|
+
});
|
|
40
|
+
await monorepo.run(new ComposeLogsOperation(), {
|
|
41
|
+
services: services.length > 0 ? services : undefined,
|
|
42
|
+
follow: flags.follow,
|
|
30
43
|
});
|
|
31
|
-
const container = await docker.getContainer(containerId);
|
|
32
|
-
if (flags.follow) {
|
|
33
|
-
const stream = await container.logs({
|
|
34
|
-
follow: true,
|
|
35
|
-
stderr: true,
|
|
36
|
-
stdout: true,
|
|
37
|
-
});
|
|
38
|
-
docker.modem.demuxStream(stream, process.stdout, process.stderr);
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
const res = await container.logs({
|
|
42
|
-
stderr: true,
|
|
43
|
-
stdout: true,
|
|
44
|
-
});
|
|
45
|
-
this.log(res.toString());
|
|
46
|
-
}
|
|
47
44
|
}
|
|
48
45
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FlavoredCommand } from '../../index.js';
|
|
2
|
+
export interface SecretInfo {
|
|
3
|
+
component?: string;
|
|
4
|
+
key?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
usageCount: number;
|
|
8
|
+
}
|
|
9
|
+
export default class SecretsIndex extends FlavoredCommand<typeof SecretsIndex> {
|
|
10
|
+
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
run(): Promise<SecretInfo[]>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
2
|
+
import { printTable } from '@oclif/table';
|
|
3
|
+
import { FlavoredCommand, TABLE_DEFAULTS } from '../../index.js';
|
|
4
|
+
import { aggregateSecrets, discoverSecrets, } from '../../../secrets/SecretDiscovery.js';
|
|
5
|
+
export default class SecretsIndex extends FlavoredCommand {
|
|
6
|
+
static description = 'List all secret references in the configuration.';
|
|
7
|
+
static enableJsonFlag = true;
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %>',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
11
|
+
];
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(SecretsIndex);
|
|
14
|
+
const context = getContext();
|
|
15
|
+
const { monorepo, secrets } = context;
|
|
16
|
+
// Get registered secret providers dynamically
|
|
17
|
+
const secretProviders = new Set(secrets.getProviderNames());
|
|
18
|
+
// Collect secrets from all configuration sources
|
|
19
|
+
const allSecrets = [];
|
|
20
|
+
// Scan monorepo-level config (env, vars, tasks, defaults, flavors)
|
|
21
|
+
allSecrets.push(...discoverSecrets({
|
|
22
|
+
env: monorepo.config.env,
|
|
23
|
+
vars: monorepo.config.vars,
|
|
24
|
+
tasks: monorepo.config.tasks,
|
|
25
|
+
defaults: monorepo.config.defaults,
|
|
26
|
+
flavors: monorepo.config.flavors,
|
|
27
|
+
}, { file: '.emb.yml' }, secretProviders));
|
|
28
|
+
// Scan each component's config
|
|
29
|
+
for (const component of monorepo.components) {
|
|
30
|
+
allSecrets.push(...discoverSecrets({
|
|
31
|
+
tasks: component.config.tasks,
|
|
32
|
+
resources: component.config.resources,
|
|
33
|
+
}, {
|
|
34
|
+
file: `${component.name}/Embfile.yml`,
|
|
35
|
+
component: component.name,
|
|
36
|
+
}, secretProviders));
|
|
37
|
+
}
|
|
38
|
+
// Aggregate by unique secret reference
|
|
39
|
+
const aggregated = aggregateSecrets(allSecrets);
|
|
40
|
+
// Convert to output format
|
|
41
|
+
const result = aggregated.map((secret) => ({
|
|
42
|
+
provider: secret.provider,
|
|
43
|
+
path: secret.path,
|
|
44
|
+
key: secret.key,
|
|
45
|
+
component: secret.locations
|
|
46
|
+
.map((l) => l.component)
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(', ') || undefined,
|
|
49
|
+
usageCount: secret.locations.length,
|
|
50
|
+
}));
|
|
51
|
+
if (!flags.json) {
|
|
52
|
+
if (result.length === 0) {
|
|
53
|
+
this.log('No secret references found in configuration.');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
printTable({
|
|
57
|
+
...TABLE_DEFAULTS,
|
|
58
|
+
columns: ['provider', 'path', 'key', 'component', 'usageCount'],
|
|
59
|
+
data: result.map((r) => ({
|
|
60
|
+
...r,
|
|
61
|
+
key: r.key || '-',
|
|
62
|
+
component: r.component || '-',
|
|
63
|
+
})),
|
|
64
|
+
});
|
|
65
|
+
const providerCount = new Set(result.map((r) => r.provider)).size;
|
|
66
|
+
this.log(`\nFound ${result.length} secret reference(s) using ${providerCount} provider(s).`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FlavoredCommand } from '../../index.js';
|
|
2
|
+
export interface ProviderInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
status: 'connected' | 'not_configured';
|
|
5
|
+
type: string;
|
|
6
|
+
}
|
|
7
|
+
export default class SecretsProviders extends FlavoredCommand<typeof SecretsProviders> {
|
|
8
|
+
static description: string;
|
|
9
|
+
static enableJsonFlag: boolean;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
run(): Promise<ProviderInfo[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
2
|
+
import { printTable } from '@oclif/table';
|
|
3
|
+
import { FlavoredCommand, TABLE_DEFAULTS } from '../../index.js';
|
|
4
|
+
export default class SecretsProviders extends FlavoredCommand {
|
|
5
|
+
static description = 'Show configured secret providers and their status.';
|
|
6
|
+
static enableJsonFlag = true;
|
|
7
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
8
|
+
async run() {
|
|
9
|
+
const { flags } = await this.parse(SecretsProviders);
|
|
10
|
+
const context = getContext();
|
|
11
|
+
const { secrets } = context;
|
|
12
|
+
const providerNames = secrets.getProviderNames();
|
|
13
|
+
if (providerNames.length === 0) {
|
|
14
|
+
if (!flags.json) {
|
|
15
|
+
this.log('No secret providers configured.');
|
|
16
|
+
this.log('\nTo configure a provider, add it to your .emb.yml:');
|
|
17
|
+
this.log(`
|
|
18
|
+
plugins:
|
|
19
|
+
- name: vault
|
|
20
|
+
config:
|
|
21
|
+
address: https://vault.example.com
|
|
22
|
+
auth:
|
|
23
|
+
method: oidc
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const results = providerNames.map((name) => {
|
|
29
|
+
const provider = secrets.get(name);
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
type: provider?.constructor.name || 'Unknown',
|
|
33
|
+
status: provider ? 'connected' : 'not_configured',
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
if (!flags.json) {
|
|
37
|
+
printTable({
|
|
38
|
+
...TABLE_DEFAULTS,
|
|
39
|
+
columns: ['name', 'type', 'status'],
|
|
40
|
+
data: results.map((r) => ({
|
|
41
|
+
name: r.name,
|
|
42
|
+
type: r.type,
|
|
43
|
+
status: r.status === 'connected' ? '✔ Connected' : '✖ Not configured',
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
this.log(`\n${results.length} provider(s) configured.`);
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FlavoredCommand } from '../../index.js';
|
|
2
|
+
export interface ValidationResult {
|
|
3
|
+
error?: string;
|
|
4
|
+
key?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
status: 'error' | 'ok';
|
|
8
|
+
}
|
|
9
|
+
export default class SecretsValidate extends FlavoredCommand<typeof SecretsValidate> {
|
|
10
|
+
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
static flags: {
|
|
14
|
+
'fail-fast': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<ValidationResult[]>;
|
|
17
|
+
private validateSecret;
|
|
18
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { getContext } from '../../../index.js';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { printTable } from '@oclif/table';
|
|
4
|
+
import { FlavoredCommand, TABLE_DEFAULTS } from '../../index.js';
|
|
5
|
+
import { aggregateSecrets, discoverSecrets, } from '../../../secrets/SecretDiscovery.js';
|
|
6
|
+
export default class SecretsValidate extends FlavoredCommand {
|
|
7
|
+
static description = 'Validate that all secret references can be resolved (without showing values).';
|
|
8
|
+
static enableJsonFlag = true;
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --fail-fast',
|
|
12
|
+
'<%= config.bin %> <%= command.id %> --json',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
'fail-fast': Flags.boolean({
|
|
16
|
+
default: false,
|
|
17
|
+
description: 'Stop on first validation error',
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { flags } = await this.parse(SecretsValidate);
|
|
22
|
+
const context = getContext();
|
|
23
|
+
const { monorepo, secrets } = context;
|
|
24
|
+
// Get registered secret providers dynamically
|
|
25
|
+
const secretProviders = new Set(secrets.getProviderNames());
|
|
26
|
+
// Collect secrets from all configuration sources
|
|
27
|
+
const allSecrets = [];
|
|
28
|
+
// Scan monorepo-level config (env, vars, tasks, defaults, flavors)
|
|
29
|
+
allSecrets.push(...discoverSecrets({
|
|
30
|
+
env: monorepo.config.env,
|
|
31
|
+
vars: monorepo.config.vars,
|
|
32
|
+
tasks: monorepo.config.tasks,
|
|
33
|
+
defaults: monorepo.config.defaults,
|
|
34
|
+
flavors: monorepo.config.flavors,
|
|
35
|
+
}, { file: '.emb.yml' }, secretProviders));
|
|
36
|
+
// Scan each component's config
|
|
37
|
+
for (const component of monorepo.components) {
|
|
38
|
+
allSecrets.push(...discoverSecrets({
|
|
39
|
+
tasks: component.config.tasks,
|
|
40
|
+
resources: component.config.resources,
|
|
41
|
+
}, {
|
|
42
|
+
file: `${component.name}/Embfile.yml`,
|
|
43
|
+
component: component.name,
|
|
44
|
+
}, secretProviders));
|
|
45
|
+
}
|
|
46
|
+
// Aggregate by unique secret reference
|
|
47
|
+
const aggregated = aggregateSecrets(allSecrets);
|
|
48
|
+
if (aggregated.length === 0) {
|
|
49
|
+
if (!flags.json) {
|
|
50
|
+
this.log('No secret references found in configuration.');
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
// Validate each secret
|
|
55
|
+
const results = [];
|
|
56
|
+
let hasErrors = false;
|
|
57
|
+
for (const secret of aggregated) {
|
|
58
|
+
// Sequential validation is intentional for fail-fast support
|
|
59
|
+
// eslint-disable-next-line no-await-in-loop
|
|
60
|
+
const result = await this.validateSecret(secret, secrets);
|
|
61
|
+
results.push(result);
|
|
62
|
+
if (result.status === 'error') {
|
|
63
|
+
hasErrors = true;
|
|
64
|
+
if (flags['fail-fast']) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!flags.json) {
|
|
70
|
+
printTable({
|
|
71
|
+
...TABLE_DEFAULTS,
|
|
72
|
+
columns: ['status', 'provider', 'path', 'key'],
|
|
73
|
+
data: results.map((r) => ({
|
|
74
|
+
status: r.status === 'ok' ? '✔' : '✖',
|
|
75
|
+
provider: r.provider,
|
|
76
|
+
path: r.path,
|
|
77
|
+
key: r.key || '-',
|
|
78
|
+
})),
|
|
79
|
+
});
|
|
80
|
+
const passed = results.filter((r) => r.status === 'ok').length;
|
|
81
|
+
const failed = results.filter((r) => r.status === 'error').length;
|
|
82
|
+
this.log(`\nValidation: ${passed} passed, ${failed} failed`);
|
|
83
|
+
// Show error details
|
|
84
|
+
const errors = results.filter((r) => r.status === 'error');
|
|
85
|
+
if (errors.length > 0) {
|
|
86
|
+
this.log('\nError details:');
|
|
87
|
+
for (const error of errors) {
|
|
88
|
+
const ref = error.key
|
|
89
|
+
? `${error.provider}:${error.path}#${error.key}`
|
|
90
|
+
: `${error.provider}:${error.path}`;
|
|
91
|
+
this.log(` - ${ref}: ${error.error}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Exit with error code if validation failed
|
|
96
|
+
if (hasErrors) {
|
|
97
|
+
this.exit(1);
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
async validateSecret(secret, secrets) {
|
|
102
|
+
const provider = secrets.get(secret.provider);
|
|
103
|
+
if (!provider) {
|
|
104
|
+
return {
|
|
105
|
+
provider: secret.provider,
|
|
106
|
+
path: secret.path,
|
|
107
|
+
key: secret.key,
|
|
108
|
+
status: 'error',
|
|
109
|
+
error: `Provider '${secret.provider}' not configured`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
// Actually fetch the secret to verify access
|
|
114
|
+
const result = await provider.get({
|
|
115
|
+
path: secret.path,
|
|
116
|
+
key: secret.key,
|
|
117
|
+
});
|
|
118
|
+
// If a key was specified, verify it exists
|
|
119
|
+
if (secret.key && result === undefined) {
|
|
120
|
+
return {
|
|
121
|
+
provider: secret.provider,
|
|
122
|
+
path: secret.path,
|
|
123
|
+
key: secret.key,
|
|
124
|
+
status: 'error',
|
|
125
|
+
error: `Key '${secret.key}' not found in secret`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
provider: secret.provider,
|
|
130
|
+
path: secret.path,
|
|
131
|
+
key: secret.key,
|
|
132
|
+
status: 'ok',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
provider: secret.provider,
|
|
138
|
+
path: secret.path,
|
|
139
|
+
key: secret.key,
|
|
140
|
+
status: 'error',
|
|
141
|
+
error: error instanceof Error ? error.message : String(error),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AmbiguousReferenceError, getContext, UnkownReferenceError } from '../../../index.js';
|
|
1
|
+
import { AmbiguousReferenceError, CommandExecError, getContext, UnkownReferenceError, } from '../../../index.js';
|
|
2
2
|
import { Args, Flags } from '@oclif/core';
|
|
3
3
|
import { BaseCommand } from '../../index.js';
|
|
4
4
|
import { ExecutorType, RunTasksOperation } from '../../../monorepo/index.js';
|
|
@@ -51,6 +51,11 @@ export default class RunTask extends BaseCommand {
|
|
|
51
51
|
`Check the list of tasks available by running: \`emb tasks\``,
|
|
52
52
|
]);
|
|
53
53
|
}
|
|
54
|
+
if (error instanceof CommandExecError) {
|
|
55
|
+
throw error.toCliError([
|
|
56
|
+
`Task command exited with code ${error.exitCode}`,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
54
59
|
throw error;
|
|
55
60
|
}
|
|
56
61
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { settings } from '@oclif/core';
|
|
2
|
+
const hook = async function (_options) {
|
|
3
|
+
// Disable oclif's auto-transpilation to avoid spurious warnings when npm-linked.
|
|
4
|
+
// We always run from compiled JS in dist/, so auto-transpilation is not needed.
|
|
5
|
+
// This prevents a double tsPath() call that produces "Could not find source" warnings.
|
|
6
|
+
settings.enableAutoTranspile = false;
|
|
7
|
+
};
|
|
2
8
|
export default hook;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export * from './validation.js';
|
|
3
|
-
export
|
|
3
|
+
export interface LoadConfigOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Explicit root directory path. Takes precedence over EMB_ROOT env var.
|
|
6
|
+
* Can be either:
|
|
7
|
+
* - A directory containing .emb.yml
|
|
8
|
+
* - A direct path to a .emb.yml file
|
|
9
|
+
*/
|
|
10
|
+
root?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const loadConfig: (options?: LoadConfigOptions) => Promise<{
|
|
4
13
|
rootDir: string;
|
|
5
14
|
config: import("./schema.js").EMBConfig;
|
|
6
15
|
}>;
|