@blogic-cz/agent-tools 0.14.12 → 0.14.14
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/package.json +1 -1
- package/schemas/agent-tools.schema.json +27 -1
- package/src/config/index.ts +3 -0
- package/src/config/loader.ts +6 -0
- package/src/config/types.ts +12 -0
- package/src/db-tool/security.ts +12 -1
- package/src/db-tool/service.ts +50 -7
- package/src/db-tool/types.ts +4 -0
- package/src/k8s-tool/service.ts +41 -11
- package/src/shared/env-template.ts +38 -0
- package/src/shared/exec.ts +2 -0
package/package.json
CHANGED
|
@@ -123,6 +123,10 @@
|
|
|
123
123
|
"type": "object",
|
|
124
124
|
"additionalProperties": false,
|
|
125
125
|
"properties": {
|
|
126
|
+
"kubeconfig": {
|
|
127
|
+
"description": "Optional kubeconfig path. Supports ${ENV_VAR} templates.",
|
|
128
|
+
"type": "string"
|
|
129
|
+
},
|
|
126
130
|
"clusterId": {
|
|
127
131
|
"description": "Cluster identifier.",
|
|
128
132
|
"type": "string"
|
|
@@ -202,6 +206,10 @@
|
|
|
202
206
|
"type": "object",
|
|
203
207
|
"additionalProperties": false,
|
|
204
208
|
"properties": {
|
|
209
|
+
"kubeconfig": {
|
|
210
|
+
"description": "Optional kubeconfig path. Supports ${ENV_VAR} templates.",
|
|
211
|
+
"type": "string"
|
|
212
|
+
},
|
|
205
213
|
"context": {
|
|
206
214
|
"description": "Kubectl context name.",
|
|
207
215
|
"type": "string"
|
|
@@ -235,6 +243,16 @@
|
|
|
235
243
|
"vpn": {
|
|
236
244
|
"type": "string",
|
|
237
245
|
"description": "Convenience sugar for a VPN prerequisite key. Runtime normalizes this to prerequisites."
|
|
246
|
+
},
|
|
247
|
+
"allowedMutations": {
|
|
248
|
+
"description": "Explicitly allowed SQL mutation operations per environment. Non-local environments default to read-only.",
|
|
249
|
+
"type": "object",
|
|
250
|
+
"additionalProperties": {
|
|
251
|
+
"type": "array",
|
|
252
|
+
"items": {
|
|
253
|
+
"$ref": "#/definitions/DbMutationOperation"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
238
256
|
}
|
|
239
257
|
},
|
|
240
258
|
"required": ["environments"]
|
|
@@ -530,6 +548,11 @@
|
|
|
530
548
|
}
|
|
531
549
|
},
|
|
532
550
|
"required": ["name"]
|
|
551
|
+
},
|
|
552
|
+
"DbMutationOperation": {
|
|
553
|
+
"description": "SQL mutation operation that can be explicitly allowed for a database environment.",
|
|
554
|
+
"type": "string",
|
|
555
|
+
"enum": ["insert", "update", "delete"]
|
|
533
556
|
}
|
|
534
557
|
},
|
|
535
558
|
"examples": [
|
|
@@ -576,7 +599,10 @@
|
|
|
576
599
|
"namespace": "system"
|
|
577
600
|
},
|
|
578
601
|
"tunnelTimeoutMs": 5000,
|
|
579
|
-
"remotePort": 5432
|
|
602
|
+
"remotePort": 5432,
|
|
603
|
+
"allowedMutations": {
|
|
604
|
+
"test": ["insert"]
|
|
605
|
+
}
|
|
580
606
|
}
|
|
581
607
|
},
|
|
582
608
|
"logs": {
|
package/src/config/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type {
|
|
|
3
3
|
AzureConfig,
|
|
4
4
|
K8sConfig,
|
|
5
5
|
DbEnvConfig,
|
|
6
|
+
DbMutationOperation,
|
|
6
7
|
DatabaseConfig,
|
|
7
8
|
ObservabilityConfig,
|
|
8
9
|
ObservabilityEnvTarget,
|
|
@@ -13,6 +14,8 @@ export type {
|
|
|
13
14
|
GitHubRepoConfig,
|
|
14
15
|
} from "./types";
|
|
15
16
|
|
|
17
|
+
export { DbMutationOperationSchema } from "./types";
|
|
18
|
+
|
|
16
19
|
export {
|
|
17
20
|
ConfigService,
|
|
18
21
|
ConfigServiceLayer,
|
package/src/config/loader.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { Context, Data, Effect, Layer, Schema } from "effect";
|
|
4
4
|
|
|
5
5
|
import type { AgentToolsConfig, GitHubRepoConfig } from "./types";
|
|
6
|
+
import { DbMutationOperationSchema } from "./types";
|
|
6
7
|
|
|
7
8
|
const CliToolOverrideSchema = Schema.Struct({
|
|
8
9
|
tool: Schema.String,
|
|
@@ -75,6 +76,7 @@ const AzureConfigSchema = Schema.Struct({
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
const K8sConfigSchema = Schema.Struct({
|
|
79
|
+
kubeconfig: Schema.optionalKey(Schema.String),
|
|
78
80
|
clusterId: Schema.String,
|
|
79
81
|
namespaces: Schema.Record(Schema.String, Schema.String),
|
|
80
82
|
timeoutMs: Schema.optionalKey(Schema.Number),
|
|
@@ -93,8 +95,12 @@ const DbEnvConfigSchema = Schema.Struct({
|
|
|
93
95
|
|
|
94
96
|
const DatabaseConfigSchema = Schema.Struct({
|
|
95
97
|
environments: Schema.Record(Schema.String, DbEnvConfigSchema),
|
|
98
|
+
allowedMutations: Schema.optionalKey(
|
|
99
|
+
Schema.Record(Schema.String, Schema.Array(DbMutationOperationSchema)),
|
|
100
|
+
),
|
|
96
101
|
kubectl: Schema.optionalKey(
|
|
97
102
|
Schema.Struct({
|
|
103
|
+
kubeconfig: Schema.optionalKey(Schema.String),
|
|
98
104
|
context: Schema.String,
|
|
99
105
|
namespace: Schema.String,
|
|
100
106
|
service: Schema.optionalKey(Schema.String),
|
package/src/config/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
1
3
|
/** Azure DevOps profile configuration */
|
|
2
4
|
export type AzureConfig = {
|
|
3
5
|
organization: string;
|
|
@@ -63,6 +65,8 @@ export type ProfilePrerequisites = {
|
|
|
63
65
|
|
|
64
66
|
/** Kubernetes cluster profile configuration */
|
|
65
67
|
export type K8sConfig = ProfilePrerequisites & {
|
|
68
|
+
/** Optional kubeconfig path. Supports ${ENV_VAR} templates. */
|
|
69
|
+
kubeconfig?: string;
|
|
66
70
|
clusterId: string;
|
|
67
71
|
/** Named namespaces, e.g. { test: "my-app-test", prod: "my-app-prod" } */
|
|
68
72
|
namespaces: Record<string, string>;
|
|
@@ -81,11 +85,19 @@ export type DbEnvConfig = {
|
|
|
81
85
|
passwordEnvVar?: string;
|
|
82
86
|
};
|
|
83
87
|
|
|
88
|
+
/** SQL mutation operation that can be explicitly allowed for a database environment. */
|
|
89
|
+
export const DbMutationOperationSchema = Schema.Literals(["insert", "update", "delete"]);
|
|
90
|
+
export type DbMutationOperation = Schema.Schema.Type<typeof DbMutationOperationSchema>;
|
|
91
|
+
|
|
84
92
|
/** Database profile configuration */
|
|
85
93
|
export type DatabaseConfig = ProfilePrerequisites & {
|
|
86
94
|
/** Named database environments, e.g. { local: {...}, test: {...}, prod: {...} } */
|
|
87
95
|
environments: Record<string, DbEnvConfig>;
|
|
96
|
+
/** Explicitly allowed SQL mutation operations per environment. Non-local environments default to read-only. */
|
|
97
|
+
allowedMutations?: Record<string, readonly DbMutationOperation[]>;
|
|
88
98
|
kubectl?: {
|
|
99
|
+
/** Optional kubeconfig path. Supports ${ENV_VAR} templates. */
|
|
100
|
+
kubeconfig?: string;
|
|
89
101
|
context: string;
|
|
90
102
|
namespace: string;
|
|
91
103
|
service?: string;
|
package/src/db-tool/security.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SchemaErrorInfo } from "./types";
|
|
1
|
+
import type { DbMutationOperation, SchemaErrorInfo } from "./types";
|
|
2
2
|
|
|
3
3
|
const MUTATION_PATTERNS = [
|
|
4
4
|
/^\s*UPDATE\s+/i,
|
|
@@ -10,6 +10,12 @@ const MUTATION_PATTERNS = [
|
|
|
10
10
|
/^\s*CREATE\s+/i,
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
+
const ALLOWABLE_MUTATION_PATTERNS: Array<[DbMutationOperation, RegExp]> = [
|
|
14
|
+
["insert", /^\s*INSERT\s+/i],
|
|
15
|
+
["update", /^\s*UPDATE\s+/i],
|
|
16
|
+
["delete", /^\s*DELETE\s+/i],
|
|
17
|
+
];
|
|
18
|
+
|
|
13
19
|
const TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
|
|
14
20
|
|
|
15
21
|
/**
|
|
@@ -78,6 +84,11 @@ export function isMutationQuery(sql: string): boolean {
|
|
|
78
84
|
return MUTATION_PATTERNS.some((pattern) => pattern.test(stripped));
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
export function getAllowedMutationOperation(sql: string): DbMutationOperation | undefined {
|
|
88
|
+
const stripped = stripSqlComments(sql);
|
|
89
|
+
return ALLOWABLE_MUTATION_PATTERNS.find(([, pattern]) => pattern.test(stripped))?.[0];
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
export function isValidTableName(tableName: string): boolean {
|
|
82
93
|
return TABLE_NAME_PATTERN.test(tableName);
|
|
83
94
|
}
|
package/src/db-tool/service.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
2
2
|
import { Clock, Context, Duration, Effect, Layer, Ref, Stream } from "effect";
|
|
3
3
|
|
|
4
|
-
import type { DbConfig, QueryResult, SchemaMode } from "./types";
|
|
4
|
+
import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./types";
|
|
5
5
|
|
|
6
6
|
import { ConfigService } from "#config";
|
|
7
7
|
import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
8
|
+
import { resolveEnvTemplate } from "#shared/env-template";
|
|
8
9
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
9
10
|
import { DbConfigService, DbConfigServiceLayer, TUNNEL_CHECK_INTERVAL_MS } from "./config-service";
|
|
10
11
|
import {
|
|
@@ -22,7 +23,12 @@ import {
|
|
|
22
23
|
parseTableReference,
|
|
23
24
|
SYSTEM_SCHEMAS_SQL,
|
|
24
25
|
} from "./schema";
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
detectSchemaError,
|
|
28
|
+
getAllowedMutationOperation,
|
|
29
|
+
isValidTableName,
|
|
30
|
+
isMutationQuery,
|
|
31
|
+
} from "./security";
|
|
26
32
|
import { transformQueryResult } from "./transformers";
|
|
27
33
|
|
|
28
34
|
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1"]);
|
|
@@ -31,7 +37,8 @@ export function resolveDbAccessMode(
|
|
|
31
37
|
env: string,
|
|
32
38
|
host: string,
|
|
33
39
|
hasKubectlConfig: boolean,
|
|
34
|
-
|
|
40
|
+
allowedMutations: readonly DbMutationOperation[] = [],
|
|
41
|
+
): Pick<DbConfig, "allowMutations" | "allowedMutations" | "host" | "needsTunnel"> {
|
|
35
42
|
const isLocalHost = LOCALHOST_HOSTS.has(host);
|
|
36
43
|
const isLocalEnvironment = env === "local";
|
|
37
44
|
|
|
@@ -39,6 +46,7 @@ export function resolveDbAccessMode(
|
|
|
39
46
|
host,
|
|
40
47
|
needsTunnel: hasKubectlConfig && !isLocalEnvironment && isLocalHost,
|
|
41
48
|
allowMutations: isLocalEnvironment,
|
|
49
|
+
allowedMutations: isLocalEnvironment ? ["insert", "update", "delete"] : allowedMutations,
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -75,6 +83,7 @@ export class DbService extends Context.Service<
|
|
|
75
83
|
};
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
const kubectlKubeconfig = dbConfig.kubectl?.kubeconfig;
|
|
78
87
|
const kubectlContext = dbConfig.kubectl?.context;
|
|
79
88
|
const kubectlNamespace = dbConfig.kubectl?.namespace;
|
|
80
89
|
const kubectlService = dbConfig.kubectl?.service ?? "postgresql";
|
|
@@ -262,6 +271,24 @@ export class DbService extends Context.Service<
|
|
|
262
271
|
}
|
|
263
272
|
});
|
|
264
273
|
|
|
274
|
+
const resolveKubeconfig = Effect.fn("DbService.resolveKubeconfig")(function* (
|
|
275
|
+
port: number,
|
|
276
|
+
) {
|
|
277
|
+
if (!kubectlKubeconfig) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return yield* resolveEnvTemplate(kubectlKubeconfig).pipe(
|
|
282
|
+
Effect.mapError(
|
|
283
|
+
({ envVar }) =>
|
|
284
|
+
new DbTunnelError({
|
|
285
|
+
message: `Environment variable ${envVar} (required for kubeconfig) is not set.`,
|
|
286
|
+
port,
|
|
287
|
+
}),
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
265
292
|
const startTunnelProcess = (config: DbConfig) =>
|
|
266
293
|
Effect.gen(function* () {
|
|
267
294
|
if (!kubectlContext || !kubectlNamespace) {
|
|
@@ -274,10 +301,14 @@ export class DbService extends Context.Service<
|
|
|
274
301
|
);
|
|
275
302
|
}
|
|
276
303
|
|
|
304
|
+
const kubeconfig = yield* resolveKubeconfig(config.port);
|
|
305
|
+
const kubeconfigArgs = kubeconfig ? ["--kubeconfig", kubeconfig] : [];
|
|
306
|
+
|
|
277
307
|
const proc = yield* executor.spawn(
|
|
278
308
|
ChildProcess.make(
|
|
279
309
|
"kubectl",
|
|
280
310
|
[
|
|
311
|
+
...kubeconfigArgs,
|
|
281
312
|
"port-forward",
|
|
282
313
|
"--context",
|
|
283
314
|
kubectlContext,
|
|
@@ -616,6 +647,7 @@ export class DbService extends Context.Service<
|
|
|
616
647
|
env,
|
|
617
648
|
envConfig.host,
|
|
618
649
|
dbConfig.kubectl !== undefined,
|
|
650
|
+
dbConfig.allowedMutations?.[env] ?? [],
|
|
619
651
|
);
|
|
620
652
|
|
|
621
653
|
return {
|
|
@@ -627,6 +659,7 @@ export class DbService extends Context.Service<
|
|
|
627
659
|
port: envConfig.port,
|
|
628
660
|
needsTunnel: accessMode.needsTunnel,
|
|
629
661
|
allowMutations: accessMode.allowMutations,
|
|
662
|
+
allowedMutations: accessMode.allowedMutations,
|
|
630
663
|
};
|
|
631
664
|
};
|
|
632
665
|
|
|
@@ -639,12 +672,22 @@ export class DbService extends Context.Service<
|
|
|
639
672
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
|
640
673
|
const password = yield* resolvePassword(resolvedConfig, env);
|
|
641
674
|
const mutation = isMutationQuery(sql);
|
|
642
|
-
|
|
643
|
-
|
|
675
|
+
const mutationOperation = mutation ? getAllowedMutationOperation(sql) : undefined;
|
|
676
|
+
const mutationAllowed =
|
|
677
|
+
!mutation ||
|
|
678
|
+
resolvedConfig.allowMutations ||
|
|
679
|
+
(mutationOperation !== undefined &&
|
|
680
|
+
resolvedConfig.allowedMutations.includes(mutationOperation));
|
|
681
|
+
|
|
682
|
+
if (!mutationAllowed) {
|
|
683
|
+
const allowed =
|
|
684
|
+
resolvedConfig.allowedMutations.length > 0
|
|
685
|
+
? resolvedConfig.allowedMutations.join(", ")
|
|
686
|
+
: "none";
|
|
644
687
|
return yield* new DbMutationBlockedError({
|
|
645
|
-
message:
|
|
646
|
-
"Mutation queries (UPDATE, INSERT, DELETE, etc.) are not allowed on this environment. Use a local environment for mutations.",
|
|
688
|
+
message: `Mutation queries are not allowed on environment ${env}. Allowed mutation operations: ${allowed}.`,
|
|
647
689
|
environment: env,
|
|
690
|
+
hint: 'Configure database.<profile>.allowedMutations.<env> with explicit operations such as ["insert"] if this environment should allow controlled mutations.',
|
|
648
691
|
});
|
|
649
692
|
}
|
|
650
693
|
|
package/src/db-tool/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { DbMutationOperation } from "#config";
|
|
1
2
|
import type { Environment, OutputFormat } from "#shared";
|
|
3
|
+
|
|
4
|
+
export type { DbMutationOperation };
|
|
2
5
|
export type { Environment, OutputFormat };
|
|
3
6
|
|
|
4
7
|
export type SchemaMode = "tables" | "columns" | "full" | "relationships";
|
|
@@ -12,6 +15,7 @@ export type DbConfig = {
|
|
|
12
15
|
port: number;
|
|
13
16
|
needsTunnel: boolean;
|
|
14
17
|
allowMutations: boolean;
|
|
18
|
+
allowedMutations: readonly DbMutationOperation[];
|
|
15
19
|
};
|
|
16
20
|
|
|
17
21
|
export type QueryResult = {
|
package/src/k8s-tool/service.ts
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
} from "./errors";
|
|
12
12
|
import { ConfigService, getToolConfig } from "#config";
|
|
13
13
|
import type { K8sConfig } from "#config";
|
|
14
|
-
import { collectProcessOutput } from "#shared/exec";
|
|
14
|
+
import { collectProcessOutput, quoteShellArg } from "#shared/exec";
|
|
15
|
+
import { resolveEnvTemplate } from "#shared/env-template";
|
|
15
16
|
import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
16
17
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
17
18
|
import { isKubectlCommandAllowed } from "./security";
|
|
@@ -63,6 +64,28 @@ export class K8sService extends Context.Service<
|
|
|
63
64
|
return k8sConfig;
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
const resolveKubeconfig = Effect.fn("K8sService.resolveKubeconfig")(function* (
|
|
68
|
+
k8sConfig: K8sConfig,
|
|
69
|
+
) {
|
|
70
|
+
const kubeconfig = k8sConfig.kubeconfig;
|
|
71
|
+
if (!kubeconfig) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return yield* resolveEnvTemplate(kubeconfig).pipe(
|
|
76
|
+
Effect.mapError(
|
|
77
|
+
({ envVar }) =>
|
|
78
|
+
new K8sContextError({
|
|
79
|
+
message: `Environment variable ${envVar} (required for kubeconfig) is not set.`,
|
|
80
|
+
clusterId: k8sConfig.clusterId,
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const withKubeconfig = (command: string, kubeconfig: string | undefined) =>
|
|
87
|
+
kubeconfig ? `KUBECONFIG=${quoteShellArg(kubeconfig)} ${command}` : command;
|
|
88
|
+
|
|
66
89
|
// Cache context by selected profile/cluster instead of a single default profile.
|
|
67
90
|
const contextRef = yield* Ref.make<Record<string, string>>({});
|
|
68
91
|
|
|
@@ -128,14 +151,18 @@ export class K8sService extends Context.Service<
|
|
|
128
151
|
k8sConfig: K8sConfig,
|
|
129
152
|
) {
|
|
130
153
|
const timeoutMs = k8sConfig.timeoutMs ?? 60000;
|
|
131
|
-
const
|
|
154
|
+
const kubeconfig = yield* resolveKubeconfig(k8sConfig);
|
|
155
|
+
const cacheKey = profile ?? `cluster:${k8sConfig.clusterId}:${kubeconfig ?? "default"}`;
|
|
132
156
|
const cached = yield* Ref.get(contextRef);
|
|
133
157
|
const cachedContext = cached[cacheKey];
|
|
134
158
|
if (cachedContext !== undefined) {
|
|
135
|
-
return cachedContext;
|
|
159
|
+
return { context: cachedContext, kubeconfig };
|
|
136
160
|
}
|
|
137
161
|
|
|
138
|
-
const jqCommand =
|
|
162
|
+
const jqCommand = withKubeconfig(
|
|
163
|
+
`kubectl config view -o json | jq -r '.contexts[] | select(.context.cluster == "${k8sConfig.clusterId}") | .name' | head -1`,
|
|
164
|
+
kubeconfig,
|
|
165
|
+
);
|
|
139
166
|
|
|
140
167
|
const contextResultOption = yield* runShellCommand(jqCommand, timeoutMs);
|
|
141
168
|
|
|
@@ -155,10 +182,13 @@ export class K8sService extends Context.Service<
|
|
|
155
182
|
...contexts,
|
|
156
183
|
[cacheKey]: resolvedContextValue,
|
|
157
184
|
}));
|
|
158
|
-
return resolvedContextValue;
|
|
185
|
+
return { context: resolvedContextValue, kubeconfig };
|
|
159
186
|
}
|
|
160
187
|
|
|
161
|
-
const fallbackCommand =
|
|
188
|
+
const fallbackCommand = withKubeconfig(
|
|
189
|
+
`kubectl config view -o json | jq -r '.contexts[] as $ctx | .clusters[] | select(.name == $ctx.context.cluster and (.cluster.server | contains("${k8sConfig.clusterId}"))) | $ctx.name' | head -1`,
|
|
190
|
+
kubeconfig,
|
|
191
|
+
);
|
|
162
192
|
|
|
163
193
|
const fallbackResultOption = yield* runShellCommand(fallbackCommand, timeoutMs);
|
|
164
194
|
|
|
@@ -178,7 +208,7 @@ export class K8sService extends Context.Service<
|
|
|
178
208
|
...contexts,
|
|
179
209
|
[cacheKey]: resolvedContextValue,
|
|
180
210
|
}));
|
|
181
|
-
return resolvedContextValue;
|
|
211
|
+
return { context: resolvedContextValue, kubeconfig };
|
|
182
212
|
}
|
|
183
213
|
|
|
184
214
|
return yield* new K8sContextError({
|
|
@@ -198,8 +228,8 @@ export class K8sService extends Context.Service<
|
|
|
198
228
|
k8sConfig,
|
|
199
229
|
runPrerequisiteCommand,
|
|
200
230
|
Effect.gen(function* () {
|
|
201
|
-
const context = yield* resolveContext(profile, k8sConfig);
|
|
202
|
-
const fullCommand = `kubectl --context ${context} ${cmd}
|
|
231
|
+
const { context, kubeconfig } = yield* resolveContext(profile, k8sConfig);
|
|
232
|
+
const fullCommand = withKubeconfig(`kubectl --context ${context} ${cmd}`, kubeconfig);
|
|
203
233
|
|
|
204
234
|
const resultOption = yield* runShellCommand(fullCommand, timeoutMs);
|
|
205
235
|
|
|
@@ -282,8 +312,8 @@ export class K8sService extends Context.Service<
|
|
|
282
312
|
const startTime = Date.now();
|
|
283
313
|
if (dryRun) {
|
|
284
314
|
const k8sConfig = yield* requireK8sConfig(profile);
|
|
285
|
-
const context = yield* resolveContext(profile, k8sConfig);
|
|
286
|
-
const fullCommand = `kubectl --context ${context} ${cmd}
|
|
315
|
+
const { context, kubeconfig } = yield* resolveContext(profile, k8sConfig);
|
|
316
|
+
const fullCommand = withKubeconfig(`kubectl --context ${context} ${cmd}`, kubeconfig);
|
|
287
317
|
return {
|
|
288
318
|
success: true,
|
|
289
319
|
command: fullCommand,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
const envTemplateRegex = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
4
|
+
|
|
5
|
+
export class EnvTemplateError extends Schema.TaggedErrorClass<EnvTemplateError>()(
|
|
6
|
+
"@agent-tools/EnvTemplateError",
|
|
7
|
+
{ envVar: Schema.String },
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
export const resolveEnvTemplate = Effect.fn("resolveEnvTemplate")(function* (value: string) {
|
|
11
|
+
let resolved = "";
|
|
12
|
+
let lastIndex = 0;
|
|
13
|
+
const env = globalThis.Bun?.env ?? process.env;
|
|
14
|
+
|
|
15
|
+
for (const match of value.matchAll(envTemplateRegex)) {
|
|
16
|
+
const fullMatch = match[0];
|
|
17
|
+
const envVar = match[1];
|
|
18
|
+
const index = match.index;
|
|
19
|
+
if (index === undefined) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resolved += value.slice(lastIndex, index);
|
|
24
|
+
const fromEnv = env[envVar];
|
|
25
|
+
if (fromEnv === undefined) {
|
|
26
|
+
return yield* new EnvTemplateError({ envVar });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resolved += fromEnv;
|
|
30
|
+
lastIndex = index + fullMatch.length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (lastIndex === 0) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return resolved + value.slice(lastIndex);
|
|
38
|
+
});
|