k6ctl 1.1.0 → 1.3.0

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.
Files changed (52) hide show
  1. package/README.md +168 -4
  2. package/dist/cli.js +10 -5
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/delete.d.ts +4 -1
  5. package/dist/commands/delete.d.ts.map +1 -1
  6. package/dist/commands/delete.js +26 -5
  7. package/dist/commands/delete.js.map +1 -1
  8. package/dist/commands/list.js +6 -6
  9. package/dist/commands/list.js.map +1 -1
  10. package/dist/commands/run.d.ts +2 -0
  11. package/dist/commands/run.d.ts.map +1 -1
  12. package/dist/commands/run.js +191 -1
  13. package/dist/commands/run.js.map +1 -1
  14. package/dist/commands/status.d.ts.map +1 -1
  15. package/dist/commands/status.js +4 -0
  16. package/dist/commands/status.js.map +1 -1
  17. package/dist/services/kubernetes.service.d.ts +12 -5
  18. package/dist/services/kubernetes.service.d.ts.map +1 -1
  19. package/dist/services/kubernetes.service.js +134 -6
  20. package/dist/services/kubernetes.service.js.map +1 -1
  21. package/dist/services/script.service.d.ts +3 -1
  22. package/dist/services/script.service.d.ts.map +1 -1
  23. package/dist/services/script.service.js +115 -1
  24. package/dist/services/script.service.js.map +1 -1
  25. package/dist/types/kubernetes.types.d.ts +5 -0
  26. package/dist/types/kubernetes.types.d.ts.map +1 -1
  27. package/dist/types/lastRun.types.d.ts +2 -1
  28. package/dist/types/lastRun.types.d.ts.map +1 -1
  29. package/dist/types/script.types.d.ts +39 -0
  30. package/dist/types/script.types.d.ts.map +1 -1
  31. package/dist/types/testRunManifest.types.d.ts +2 -4
  32. package/dist/types/testRunManifest.types.d.ts.map +1 -1
  33. package/dist/utils/testRunManifestBuilder.d.ts +14 -1
  34. package/dist/utils/testRunManifestBuilder.d.ts.map +1 -1
  35. package/dist/utils/testRunManifestBuilder.js +70 -14
  36. package/dist/utils/testRunManifestBuilder.js.map +1 -1
  37. package/package.json +2 -1
  38. package/src/cli.ts +10 -5
  39. package/src/commands/delete.ts +35 -8
  40. package/src/commands/list.ts +7 -7
  41. package/src/commands/run.ts +216 -2
  42. package/src/commands/status.ts +4 -0
  43. package/src/services/kubernetes.service.ts +161 -16
  44. package/src/services/script.service.ts +102 -4
  45. package/src/types/kubernetes.types.ts +6 -0
  46. package/src/types/lastRun.types.ts +2 -1
  47. package/src/types/script.types.ts +40 -0
  48. package/src/types/testRunManifest.types.ts +3 -4
  49. package/src/utils/testRunManifestBuilder.ts +80 -18
  50. package/test/integration/kubernetes.service.test.ts +63 -9
  51. package/test/unit/kubernetes.service.test.ts +15 -7
  52. package/test/unit/script.service.test.ts +11 -4
@@ -1,5 +1,18 @@
1
- import type { ArchivedFile, ConfigMapResult } from '../types/kubernetes.types';
1
+ import type { ArchivedFile, ConfigMapResult, VolumeClaimResult } from '../types/kubernetes.types';
2
2
  import { K6Config } from '../types/config.types';
3
3
  import { TestRunManifest } from '../types/testRunManifest.types';
4
+ export type RunnerEnvVar = {
5
+ name: string;
6
+ value: string;
7
+ } | {
8
+ name: string;
9
+ valueFrom: {
10
+ secretKeyRef: {
11
+ name: string;
12
+ key: string;
13
+ };
14
+ };
15
+ };
4
16
  export declare function buildTestRunManifest(configMapResult: ConfigMapResult, archiveOutput: ArchivedFile, cfg: K6Config, envFromLoader?: Record<string, string>): TestRunManifest;
17
+ export declare function buildTestRunManifestWithVolumeClaim(volumeClaimResult: VolumeClaimResult, cfg: K6Config, envFromLoader?: Record<string, string>): TestRunManifest;
5
18
  //# sourceMappingURL=testRunManifestBuilder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testRunManifestBuilder.d.ts","sourceRoot":"","sources":["../../src/utils/testRunManifestBuilder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAajE,wBAAgB,oBAAoB,CAChC,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,YAAY,EAC3B,GAAG,EAAE,QAAQ,EACb,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,eAAe,CAoCjB"}
1
+ {"version":3,"file":"testRunManifestBuilder.d.ts","sourceRoot":"","sources":["../../src/utils/testRunManifestBuilder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEjE,MAAM,MAAM,YAAY,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE;QAAE,YAAY,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;CAAE,CAAC;AAWnF,wBAAgB,oBAAoB,CAChC,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,YAAY,EAC3B,GAAG,EAAE,QAAQ,EACb,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,eAAe,CAoCjB;AAoED,wBAAgB,mCAAmC,CAC/C,iBAAiB,EAAE,iBAAiB,EACpC,GAAG,EAAE,QAAQ,EACb,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,eAAe,CAgCjB"}
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.buildTestRunManifest = buildTestRunManifest;
7
+ exports.buildTestRunManifestWithVolumeClaim = buildTestRunManifestWithVolumeClaim;
7
8
  const logger_1 = __importDefault(require("../utils/logger"));
8
9
  const K6_GROUP = "k6.io";
9
10
  const K6_VERSION = "v1alpha1";
@@ -50,22 +51,14 @@ function buildTestRunManifest(configMapResult, archiveOutput, cfg, envFromLoader
50
51
  function buildRunnerEnv(cfg, envFromLoader) {
51
52
  const envMap = new Map();
52
53
  if (envFromLoader) {
53
- for (const [key, value] of Object.entries(envFromLoader)) {
54
- if (value !== undefined && value !== null) {
55
- envMap.set(key, String(value));
56
- }
54
+ for (const [key, raw] of Object.entries(envFromLoader)) {
55
+ if (raw === undefined || raw === null)
56
+ continue;
57
+ addEnvToMap(key, raw, envMap);
57
58
  }
58
59
  }
59
- if (cfg.prometheus?.serverUrl) {
60
- envMap.set(K6_PROMETHEUS_RW_SERVER_URL, cfg.prometheus.serverUrl);
61
- if (cfg.prometheus.trendStats?.length) {
62
- envMap.set(K6_PROMETHEUS_RW_TREND_STATS, cfg.prometheus.trendStats.join(","));
63
- }
64
- }
65
- const envArray = Array.from(envMap.entries()).map(([name, value]) => ({
66
- name,
67
- value,
68
- }));
60
+ addPrometheusEnvVars(cfg, envMap);
61
+ const envArray = Array.from(envMap.values());
69
62
  return envArray.length > 0 ? envArray : undefined;
70
63
  }
71
64
  function buildArgumentsString(args, cfg, name) {
@@ -86,4 +79,67 @@ function buildArgumentsString(args, cfg, name) {
86
79
  }
87
80
  return finalArgs.join(" ");
88
81
  }
82
+ function parseSecretPlaceholder(value) {
83
+ const m = /^\{\{SECRETS\.([^.}]+)\.([^.}]+)\}\}$/.exec(value.trim());
84
+ if (!m)
85
+ return null;
86
+ return { secretName: m[1], secretKey: m[2] };
87
+ }
88
+ function addEnvToMap(key, raw, envMap) {
89
+ const value = String(raw);
90
+ const secret = parseSecretPlaceholder(value);
91
+ if (secret) {
92
+ envMap.set(key, { name: key, valueFrom: { secretKeyRef: { name: secret.secretName, key: secret.secretKey } } });
93
+ }
94
+ else {
95
+ envMap.set(key, { name: key, value });
96
+ }
97
+ }
98
+ function addPrometheusEnvVars(cfg, envMap) {
99
+ if (cfg.prometheus?.serverUrl) {
100
+ envMap.set(K6_PROMETHEUS_RW_SERVER_URL, {
101
+ name: K6_PROMETHEUS_RW_SERVER_URL,
102
+ value: cfg.prometheus.serverUrl,
103
+ });
104
+ if (cfg.prometheus.trendStats?.length) {
105
+ envMap.set(K6_PROMETHEUS_RW_TREND_STATS, {
106
+ name: K6_PROMETHEUS_RW_TREND_STATS,
107
+ value: cfg.prometheus.trendStats.join(","),
108
+ });
109
+ }
110
+ }
111
+ }
112
+ function buildTestRunManifestWithVolumeClaim(volumeClaimResult, cfg, envFromLoader) {
113
+ const testName = volumeClaimResult.volumeClaimName.replace('archive-', 'test-');
114
+ const argumentsString = buildArgumentsString(cfg.arguments, cfg, testName);
115
+ logger_1.default.debug('Constructed arguments string for TestRun manifest (volume):', argumentsString);
116
+ const testRun = {
117
+ apiVersion: `${K6_GROUP}/${K6_VERSION}`,
118
+ kind: K6_KIND,
119
+ metadata: {
120
+ name: testName,
121
+ namespace: volumeClaimResult.namespace,
122
+ },
123
+ spec: {
124
+ parallelism: cfg.parallelism,
125
+ arguments: argumentsString,
126
+ quiet: String(cfg.quiet),
127
+ ...(cfg.cleanup === true ? { cleanup: K6_CLEANUP_LABEL } : {}),
128
+ separate: cfg.separate,
129
+ runner: {
130
+ image: cfg.runner?.image,
131
+ env: buildRunnerEnv(cfg, envFromLoader),
132
+ ...(cfg.runner?.resources ? { resources: cfg.runner.resources } : {}),
133
+ },
134
+ script: {
135
+ volumeClaim: {
136
+ name: volumeClaimResult.volumeClaimName,
137
+ file: volumeClaimResult.archiveFilename,
138
+ },
139
+ },
140
+ },
141
+ };
142
+ logger_1.default.debug('Constructed TestRun manifest (volume):', JSON.stringify(testRun, null, 2));
143
+ return testRun;
144
+ }
89
145
  //# sourceMappingURL=testRunManifestBuilder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testRunManifestBuilder.js","sourceRoot":"","sources":["../../src/utils/testRunManifestBuilder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testRunManifestBuilder.js","sourceRoot":"","sources":["../../src/utils/testRunManifestBuilder.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k6ctl",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI tool to run k6 tests on Kubernetes using k6-operator",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -28,6 +28,7 @@
28
28
  "commander": "^14.0.3",
29
29
  "dotenv": "^17.3.1",
30
30
  "js-yaml": "^4.1.1",
31
+ "tar": "^7.5.13",
31
32
  "winston": "^3.19.0",
32
33
  "zod": "^4.3.6"
33
34
  },
package/src/cli.ts CHANGED
@@ -16,12 +16,14 @@ program
16
16
  .version(version);
17
17
 
18
18
  program
19
- .command('run <script>')
20
- .description('Run a k6 test script')
21
- .option('-c, --config <path>', 'Path to config file', 'k6ctl.config.js')
19
+ .command('run [script]')
20
+ .description('Run a k6 test script (omit to select interactively from ${pwd}/dist/tests)')
21
+ .option('-c, --config <path>', 'Path to config file', 'k6ctl.config.json')
22
22
  .option('-n, --namespace <namespace>', 'Kubernetes namespace')
23
23
  .option('-p, --parallelism <number>', 'Number of parallel test pods')
24
24
  .option('-v, --verbose', 'enable debug logging')
25
+ .option('-d, --dir <path>', 'Folder to search for .js test files', 'dist/tests')
26
+ .option('--smart', 'Enable smart scenario analysis')
25
27
  .action(runTest);
26
28
 
27
29
  program
@@ -46,9 +48,12 @@ program
46
48
 
47
49
  program
48
50
  .command('delete')
49
- .description('Delete the last test run (TestRun + ConfigMap)')
51
+ .description('Delete the last test run (TestRun + script)')
50
52
  .option('-n, --namespace <namespace>', 'Kubernetes namespace (overrides saved value)')
51
- .option('--keep-configmap', 'Skip deletion of the associated ConfigMap')
53
+ .option('--keep-script', 'Skip deletion of the associated script in ConfigMap or PVC')
54
+ .option('-p, --pod <name>', 'Delete a specific pod instead of those associated with the last TestRun')
55
+ .option('-t, --testrun <name>', 'Delete a specific TestRun by name instead of the last one')
56
+ .option('-c, --configmap <name>', 'Delete a specific ConfigMap by name instead of the one associated with the last TestRun')
52
57
  .action(deleteLastRun);
53
58
 
54
59
  program.parse();
@@ -4,26 +4,53 @@ import logger from '../utils/logger';
4
4
 
5
5
  interface DeleteOptions {
6
6
  namespace?: string;
7
- keepConfigmap?: boolean;
7
+ keepScript?: boolean;
8
+ pod?: string;
9
+ testrun?: string;
10
+ configmap?: string;
8
11
  }
9
12
 
10
13
  export async function deleteLastRun(options: DeleteOptions) {
14
+ const kubernetesService = createDefaultKubernetesService();
15
+
11
16
  const lastRun = await loadLastRun();
17
+ const namespace = options.namespace ?? lastRun?.namespace ?? 'default';
18
+
19
+ if (options.pod) {
20
+ logger.info(`Deleting Pod: ${options.pod} (namespace: ${namespace})`);
21
+ await kubernetesService.deletePodByName(options.pod, namespace);
22
+ return;
23
+ }
24
+
25
+ if (options.testrun) {
26
+ logger.info(`Deleting TestRun: ${options.testrun} (namespace: ${namespace})`);
27
+ await kubernetesService.deleteTestRunByName(options.testrun, namespace);
28
+ return;
29
+ }
30
+
31
+ if (options.configmap) {
32
+ logger.info(`Deleting ConfigMap: ${options.configmap} (namespace: ${namespace})`);
33
+ await kubernetesService.deleteConfigMap(options.configmap, namespace);
34
+ return;
35
+ }
36
+
12
37
  if (!lastRun) {
13
38
  logger.error('No last run found. Run a test first with: k6ctl run <script>');
14
39
  process.exit(1);
15
40
  }
16
41
 
17
- const namespace = options.namespace ?? lastRun.namespace;
18
42
  logger.info(`Deleting TestRun: ${lastRun.testRunName} (namespace: ${namespace})`);
19
-
20
- const kubernetesService = createDefaultKubernetesService();
21
-
22
43
  await kubernetesService.deleteTestRunByName(lastRun.testRunName, namespace);
23
44
 
24
- if (!options.keepConfigmap) {
25
- logger.info(`Deleting ConfigMap: ${lastRun.configMapName} (namespace: ${namespace})`);
26
- await kubernetesService.deleteConfigMap(lastRun.configMapName, namespace);
45
+ if (!options.keepScript) {
46
+ if (lastRun.configMapName) {
47
+ logger.info(`Deleting ConfigMap: ${lastRun.configMapName} (namespace: ${namespace})`);
48
+ await kubernetesService.deleteConfigMap(lastRun.configMapName, namespace);
49
+ }
50
+ if (lastRun.volumeClaimName) {
51
+ logger.info(`Deleting PVC: ${lastRun.volumeClaimName} (namespace: ${namespace})`);
52
+ await kubernetesService.deleteVolumeClaimByName(lastRun.volumeClaimName, namespace);
53
+ }
27
54
  }
28
55
 
29
56
  await clearLastRun();
@@ -1,26 +1,26 @@
1
1
  import logger, { setLogLevel } from '../utils/logger';
2
- import { createDefaultKubernetesService } from '../services/kubernetes.service';
2
+ import { createDefaultKubernetesService, printPodsTable, printTestRunsTable, printConfigMapsTable } from '../services/kubernetes.service';
3
3
 
4
4
  export async function list(type: string, options: { namespace?: string }) {
5
5
  const kubernetesService = createDefaultKubernetesService();
6
6
  switch (type) {
7
7
  case 'all':
8
8
  logger.debug(`Listing all resources in namespace: ${options.namespace}`);
9
- await kubernetesService.listTestRuns(options.namespace);
10
- await kubernetesService.listPods(options.namespace);
11
- await kubernetesService.listConfigMaps(options.namespace);
9
+ printTestRunsTable(await kubernetesService.listTestRuns(options.namespace));
10
+ printPodsTable(await kubernetesService.listPods(options.namespace));
11
+ printConfigMapsTable(await kubernetesService.listConfigMaps(options.namespace));
12
12
  break;
13
13
  case 'pods':
14
14
  logger.debug(`Listing Pods in namespace: ${options.namespace}`);
15
- await kubernetesService.listPods(options.namespace);
15
+ printPodsTable(await kubernetesService.listPods(options.namespace));
16
16
  break;
17
17
  case 'testruns':
18
18
  logger.debug(`Listing TestRuns in namespace: ${options.namespace}`);
19
- await kubernetesService.listTestRuns(options.namespace);
19
+ printTestRunsTable(await kubernetesService.listTestRuns(options.namespace));
20
20
  break;
21
21
  case 'configmaps':
22
22
  logger.debug(`Listing ConfigMaps in namespace: ${options.namespace}`);
23
- await kubernetesService.listConfigMaps(options.namespace);
23
+ printConfigMapsTable(await kubernetesService.listConfigMaps(options.namespace));
24
24
  break;
25
25
  default:
26
26
  logger.debug(`Listing ${type} in namespace: ${options.namespace}`);
@@ -1,20 +1,84 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as readline from 'readline';
1
4
  import { createDefaultKubernetesService } from '../services/kubernetes.service';
2
5
  import { ScriptService } from '../services/script.service';
3
6
  import { loadK6Config } from '../utils/configLoader';
4
7
  import { loadAndValidateEnv } from '../utils/env';
5
8
  import logger, { setLogLevel } from '../utils/logger';
6
- import { buildTestRunManifest } from '../utils/testRunManifestBuilder';
9
+ import { buildTestRunManifest, buildTestRunManifestWithVolumeClaim } from '../utils/testRunManifestBuilder';
7
10
  import { saveLastRun } from '../utils/lastRunStore';
11
+ import { K6StageMetrics, K6ScenarioOptions, K6ScenarioMetrics, K6InspectResult } from "../types/script.types";
12
+ import { ConfigMapResult } from '../types/kubernetes.types';
13
+
14
+ const DEFAULT_VUS_PER_POD = process.env.DEFAULT_VUS_PER_POD ? parseInt(process.env.DEFAULT_VUS_PER_POD, 10) : 100;
15
+ const MAX_ITERATION_DURATION_SECONDS = process.env.MAX_ITERATION_DURATION_SECONDS ? parseInt(process.env.MAX_ITERATION_DURATION_SECONDS, 10) : 60;
16
+
17
+ function listTestFiles(dir: string): string[] {
18
+ if (!fs.existsSync(dir)) {
19
+ return [];
20
+ }
21
+ const results: string[] = [];
22
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
23
+ const fullPath = path.join(dir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ results.push(...listTestFiles(fullPath));
26
+ } else if (entry.isFile() && entry.name.endsWith('.js')) {
27
+ results.push(fullPath);
28
+ }
29
+ }
30
+ return results;
31
+ }
32
+
33
+ function promptSelection(files: string[], dir: string): Promise<string> {
34
+ return new Promise((resolve, reject) => {
35
+ console.log('\nAvailable test scripts:');
36
+ files.forEach((file, i) => {
37
+ const relative = path.relative(dir, file);
38
+ console.log(` [${String(i + 1).padStart(2)}] ${relative}`);
39
+ });
40
+
41
+ console.log('');
42
+
43
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
44
+ rl.question(`Select a test (1-${files.length}): `, answer => {
45
+ rl.close();
46
+ const index = parseInt(answer.trim(), 10) - 1;
47
+ if (isNaN(index) || index < 0 || index >= files.length) {
48
+ reject(new Error(`Invalid selection: "${answer}"`));
49
+ } else {
50
+ resolve(files[index]);
51
+ }
52
+ });
53
+ });
54
+ }
8
55
 
9
56
  interface RunOptions {
10
57
  config: string;
11
58
  namespace?: string;
12
59
  parallelism?: number;
13
60
  verbose?: boolean;
61
+ dir: string;
62
+ smart?: boolean;
14
63
  }
15
64
 
16
65
  export async function runTest(scriptPath: string, options: RunOptions) {
17
66
  if (options.verbose) setLogLevel('debug');
67
+
68
+ if (!scriptPath) {
69
+ const files = listTestFiles(options.dir);
70
+ if (files.length === 0) {
71
+ logger.error(`No .js test files found in ${options.dir}`);
72
+ process.exit(1);
73
+ }
74
+ try {
75
+ scriptPath = await promptSelection(files, options.dir);
76
+ } catch (error) {
77
+ logger.error(error instanceof Error ? error.message : String(error));
78
+ process.exit(1);
79
+ }
80
+ }
81
+
18
82
  logger.debug(`Running k6 test: ${scriptPath}`);
19
83
  logger.debug(`Using config: ${JSON.stringify(options.config, null, 2)}`);
20
84
 
@@ -24,11 +88,45 @@ export async function runTest(scriptPath: string, options: RunOptions) {
24
88
  const kubernetesService = createDefaultKubernetesService();
25
89
 
26
90
  // Load test script
27
- const archive = await scriptService.archiveTest(scriptPath);
91
+ let archive = await scriptService.archiveTest(scriptPath);
28
92
 
29
93
  // Load config
30
94
  const config = loadK6Config(options.config);
31
95
 
96
+
97
+ // Analyze script if smart option is enabled
98
+ if (options.smart) {
99
+ logger.info(`Analyzing script: ${scriptPath}`);
100
+ const inspectResult = await scriptService.inspectScript(scriptPath);
101
+ const scenarioMetrics: K6ScenarioMetrics[] = await analyzeScript(inspectResult);
102
+ if (scenarioMetrics) {
103
+ const estimatedTotalIterations = scenarioMetrics.reduce((sum, metrics) => sum + (metrics?.totalIterations ?? 0), 0);
104
+ const totalRecommendedMaxVUs = scenarioMetrics.reduce((sum, metrics) => sum + (metrics?.recommendedMaxVUs ?? 0), 0);
105
+ const parallelism = Math.ceil(totalRecommendedMaxVUs / DEFAULT_VUS_PER_POD) || 1;
106
+ const peakTps = scenarioMetrics.reduce((max, metrics) => Math.max(max, metrics?.peakTps ?? 0), 0);
107
+
108
+ config.parallelism = parallelism;
109
+
110
+ logger.info(`Total recommendedMaxVUs: ${Math.ceil(totalRecommendedMaxVUs)}
111
+ based on scenario analysis and MAX_ITERATION_DURATION_SECONDS=${MAX_ITERATION_DURATION_SECONDS} seconds
112
+ per iteration assumption with a safety factor of 1.2 applied to account for variability in iteration duration and ensure we have enough VUs
113
+ to meet the target TPS. This is an estimate and actual resource needs may vary based on the specific workload and environment.
114
+ Consider monitoring the test run and adjusting resources as needed.`);
115
+
116
+ logger.info(`Calculated parallelism: ${parallelism} (based on DEFAULT_VUS_PER_POD=${DEFAULT_VUS_PER_POD})`);
117
+ logger.info(`Peak TPS: ${peakTps.toFixed(2)}`);
118
+ logger.info(`Estimated Total Iterations: ${Math.round(estimatedTotalIterations)}`);
119
+ logger.info(`Using MAX_ITERATION_DURATION_SECONDS=${MAX_ITERATION_DURATION_SECONDS} seconds`);
120
+ logger.info(`Using safety factor of 1.2 to calculate recommended VUs.`);
121
+ logger.info(`Using DEFAULT_VUS_PER_POD=${DEFAULT_VUS_PER_POD} to calculate parallelism.`);
122
+
123
+ // Extract, modify, and recompress the archive
124
+ archive = await scriptService.extractModifyAndRecompress(archive, scenarioMetrics);
125
+ }
126
+ } else {
127
+ logger.info('Smart scenario analysis is disabled. Running test without previous analysis.');
128
+ }
129
+
32
130
  // Load environment variables
33
131
  let envVars;
34
132
  try {
@@ -38,6 +136,25 @@ export async function runTest(scriptPath: string, options: RunOptions) {
38
136
  logger.debug(`Error loading environment variables: ${error instanceof Error ? error.message : String(error)}`);
39
137
  }
40
138
 
139
+ if (archive.archiveSize > 1024 * 1024) {
140
+ // Volume flow
141
+ logger.info(`Test script archive size (${(archive.archiveSize / (1024 * 1024)).toFixed(2)} MB) exceeds 1 MB, using volume flow.`);
142
+
143
+ const volumeClaimResult = await kubernetesService.createPVCWithArchive(archive, config.namespace);
144
+ const testRunManifest = buildTestRunManifestWithVolumeClaim(volumeClaimResult, config, envVars);
145
+ await kubernetesService.createTestRun(testRunManifest);
146
+
147
+ await saveLastRun({
148
+ testRunName: testRunManifest.metadata.name,
149
+ namespace: testRunManifest.metadata.namespace,
150
+ volumeClaimName: volumeClaimResult.volumeClaimName,
151
+ scriptPath,
152
+ createdAt: new Date().toISOString(),
153
+ });
154
+ logger.info(`Last run saved: ${testRunManifest.metadata.name} (namespace: ${testRunManifest.metadata.namespace})`);
155
+ return;
156
+ }
157
+
41
158
  // Create configmap for test script
42
159
  const configMap = await kubernetesService.createConfigMap(archive, config.namespace);
43
160
 
@@ -61,3 +178,100 @@ export async function runTest(scriptPath: string, options: RunOptions) {
61
178
  logger.error(`Error running test: ${error instanceof Error ? error.message : String(error)}`);
62
179
  }
63
180
  }
181
+
182
+ async function analyzeScript(inspectResult: K6InspectResult): Promise<K6ScenarioMetrics[]> {
183
+ if (!inspectResult.scenarios) {
184
+ logger.info('No scenarios found in the script. Smart analysis cannot be performed.');
185
+ return [];
186
+ }
187
+ const allMetrics: K6ScenarioMetrics[] = Object.entries(inspectResult.scenarios).map(([name, scenario]) => {
188
+ return calculateScenarioMetrics(name, scenario) as K6ScenarioMetrics;
189
+ });
190
+ return allMetrics;
191
+ }
192
+
193
+ function calculateScenarioMetrics(
194
+ name: string,
195
+ scenario: K6ScenarioOptions,
196
+ avgIterationDurationSeconds: number = MAX_ITERATION_DURATION_SECONDS,
197
+ safetyFactor: number = 1.2
198
+ ): K6ScenarioMetrics | null {
199
+ if (scenario.executor !== 'ramping-arrival-rate') {
200
+ return null;
201
+ }
202
+ if (!scenario.stages || scenario.stages.length === 0) {
203
+ return null;
204
+ }
205
+ const timeUnitSeconds = parseTimeUnitToSeconds(scenario.timeUnit);
206
+ let previousRate = scenario.startRate ?? 0;
207
+ let peakTps = previousRate / timeUnitSeconds;
208
+ let totalIterations = 0;
209
+ const stageMetrics: K6StageMetrics[] = scenario.stages.map((stage, index) => {
210
+ const durationSeconds = parseDurationToSeconds(stage.duration);
211
+ const fromTps = previousRate / timeUnitSeconds;
212
+ const toTps = stage.target / timeUnitSeconds;
213
+ const avgTps = (fromTps + toTps) / 2;
214
+ const estimatedIterations = avgTps * durationSeconds;
215
+ totalIterations += estimatedIterations;
216
+ peakTps = Math.max(peakTps, toTps);
217
+ const requiredVUsAtTarget = toTps * avgIterationDurationSeconds;
218
+ const recommendedVUsAtTarget = requiredVUsAtTarget * safetyFactor;
219
+ previousRate = stage.target;
220
+ return {
221
+ stageIndex: index + 1,
222
+ duration: stage.duration,
223
+ durationSeconds,
224
+ fromTps,
225
+ toTps,
226
+ avgTps,
227
+ estimatedIterations,
228
+ requiredVUsAtTarget,
229
+ recommendedVUsAtTarget,
230
+ };
231
+ });
232
+ const requiredMaxVUs = peakTps * avgIterationDurationSeconds;
233
+ const recommendedMaxVUs = requiredMaxVUs * safetyFactor;
234
+ return {
235
+ name,
236
+ peakTps,
237
+ totalIterations,
238
+ requiredMaxVUs,
239
+ recommendedMaxVUs,
240
+ stageMetrics,
241
+ };
242
+ }
243
+
244
+ function parseDurationToSeconds(duration: string): number {
245
+ const regex = /(\d+)(ms|s|m|h)/g;
246
+ let match: RegExpExecArray | null;
247
+ let totalMs = 0;
248
+ while ((match = regex.exec(duration)) !== null) {
249
+ const value = Number(match[1]);
250
+ const unit = match[2];
251
+ switch (unit) {
252
+ case 'h':
253
+ totalMs += value * 60 * 60 * 1000;
254
+ break;
255
+ case 'm':
256
+ totalMs += value * 60 * 1000;
257
+ break;
258
+ case 's':
259
+ totalMs += value * 1000;
260
+ break;
261
+ case 'ms':
262
+ totalMs += value;
263
+ break;
264
+ default:
265
+ throw new Error(`Unsupported duration unit: ${unit}`);
266
+ }
267
+ }
268
+ if (totalMs === 0) {
269
+ throw new Error(`Invalid duration format: ${duration}`);
270
+ }
271
+ return totalMs / 1000;
272
+ }
273
+
274
+ function parseTimeUnitToSeconds(timeUnit?: string): number {
275
+ if (!timeUnit) return 1;
276
+ return parseDurationToSeconds(timeUnit);
277
+ }
@@ -21,6 +21,10 @@ export async function status(options: StatusOptions) {
21
21
  const kubernetesService = createDefaultKubernetesService();
22
22
 
23
23
  const testRun = await kubernetesService.getTestRun(lastRun.testRunName, namespace);
24
+ if (!testRun) {
25
+ logger.error(`TestRun ${lastRun.testRunName} not found in namespace ${namespace}. It may have been deleted externally.`);
26
+ process.exit(1);
27
+ }
24
28
  printTestRunsTable([testRun]);
25
29
 
26
30
  const podList = await kubernetesService.getPodsForTestRun(lastRun.testRunName, namespace);