From bbed9a041643e1c4fc46a468d590ed0fb419529c Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Tue, 15 Sep 2026 10:14:45 +0200 Subject: [PATCH 1/2] Rename parseGoDuration.test.ts to utils.test.ts The file tests a helper from common/utils.ts and is about to cover a second one. Name it after the module rather than after a single function, and rename its case list accordingly. Issue: ZENKO-5356 --- .../ctst/common/{parseGoDuration.test.ts => utils.test.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/functional/ctst/common/{parseGoDuration.test.ts => utils.test.ts} (89%) diff --git a/tests/functional/ctst/common/parseGoDuration.test.ts b/tests/functional/ctst/common/utils.test.ts similarity index 89% rename from tests/functional/ctst/common/parseGoDuration.test.ts rename to tests/functional/ctst/common/utils.test.ts index 195ec91934..56f7c13b72 100644 --- a/tests/functional/ctst/common/parseGoDuration.test.ts +++ b/tests/functional/ctst/common/utils.test.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import { parseGoDuration } from './utils'; -const cases: [string, number][] = [ +const durations: [string, number][] = [ ['1m', 60], ['2h', 7200], ['30s', 30], @@ -15,7 +15,7 @@ const cases: [string, number][] = [ ['0s', 0], ]; -for (const [input, expected] of cases) { +for (const [input, expected] of durations) { const result = parseGoDuration(input); assert.strictEqual( Math.abs(result - expected) < 1e-12, true, From 303c4e116c8dca0b8bfe875b4661fc5be7da8792 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Tue, 15 Sep 2026 10:14:54 +0200 Subject: [PATCH 2/2] Serialise drctl boolean flags with the "=" form CTST built every zenko-drctl command line with "--flag value". spf13/pflag gives boolean flags NoOptDefVal = "true", so a bool flag does not consume the next argv token: "--force-rotate-service-credentials false" set the flag to true and left "false" as a positional argument, silently accepted because no drctl leaf command declares an Args validator. Every "Given a DR installed" therefore force-rotated the sink's service credentials, regardless of the install counter. On run 34585935213 the six installs produced six rotations on the sink against one on the source, leaving the CR at service-users-seq 7 while pods still mounted user-creds.v1.5. Issue: ZENKO-5356 --- tests/functional/ctst/common/utils.test.ts | 28 +++++++++++++++++- tests/functional/ctst/common/utils.ts | 24 ++++++++++++++++ tests/functional/ctst/steps/dr/drctl.ts | 33 ++++++++-------------- 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/tests/functional/ctst/common/utils.test.ts b/tests/functional/ctst/common/utils.test.ts index 56f7c13b72..f44245e4c4 100644 --- a/tests/functional/ctst/common/utils.test.ts +++ b/tests/functional/ctst/common/utils.test.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import { parseGoDuration } from './utils'; +import { paramToCli, parseGoDuration } from './utils'; const durations: [string, number][] = [ ['1m', 60], @@ -32,3 +32,29 @@ for (const input of invalid) { ); } +const cliParams: [Record, string][] = [ + // pflag boolean flags must use the "=" form: "--flag false" would set the + // flag to true and drop "false" as a positional argument. + [{ forceRotateServiceCredentials: false }, '--force-rotate-service-credentials=false'], + [{ forceRotateServiceCredentials: true }, '--force-rotate-service-credentials=true'], + [{ wait: true }, '--wait=true'], + [{ sinkZenkoInstance: 'end2end-pra' }, '--sink-zenko-instance end2end-pra'], + [{ kafkaExternalPort: 9092 }, '--kafka-external-port 9092'], + [{ kafkaPersistenceSelector: 'app=kafka-dr-sink' }, '--kafka-persistence-selector app=kafka-dr-sink'], + [{ mongodbHosts: ['host-a', 'host-b'] }, '--mongodb-hosts host-a,host-b'], + [{ timeout: undefined }, ''], + [{ timeout: null }, ''], + [{}, ''], + [ + { sinkZenkoInstance: 'end2end-pra', wait: false, timeout: '30m' }, + '--sink-zenko-instance end2end-pra --wait=false --timeout 30m', + ], +]; + +for (const [params, expected] of cliParams) { + const result = paramToCli(params); + assert.strictEqual( + result, expected, + `paramToCli(${JSON.stringify(params)}) = "${result}", expected "${expected}"`, + ); +} diff --git a/tests/functional/ctst/common/utils.ts b/tests/functional/ctst/common/utils.ts index 3b1f036344..ef3ad2f7e7 100644 --- a/tests/functional/ctst/common/utils.ts +++ b/tests/functional/ctst/common/utils.ts @@ -135,6 +135,30 @@ export function parseGoDuration(duration: string): number { return totalSeconds; } +/** + * Serialises an options object into arguments for a Go CLI built with cobra/pflag. + * @param {Record} params - the options to serialise + * @return {string} - the arguments, space-separated + */ +export function paramToCli(params: Record): string { + const command: string[] = []; + Object.keys(params).forEach(key => { + const value = params[key]; + if (value == null) { + return; + } + const flag = `--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; + if (typeof value === 'boolean') { + // pflag boolean flags do not consume the next argument: "--flag false" + // sets the flag to true and drops "false" as a positional argument. + command.push(`${flag}=${String(value)}`); + } else { + command.push(flag, String(value)); + } + }); + return command.join(' '); +} + export function safeJsonParse(jsonString: string): { ok: boolean, result: T | null, error?: Error | null } { let result: T; try { diff --git a/tests/functional/ctst/steps/dr/drctl.ts b/tests/functional/ctst/steps/dr/drctl.ts index 67338d329b..a6af84ca51 100644 --- a/tests/functional/ctst/steps/dr/drctl.ts +++ b/tests/functional/ctst/steps/dr/drctl.ts @@ -2,6 +2,7 @@ import util from 'util'; import { exec } from 'child_process'; import Zenko from 'world/Zenko'; +import { paramToCli } from 'common/utils'; type InstallConfig = { sourceZenkoDrInstance?: string; @@ -241,54 +242,42 @@ export default class ZenkoDrctl { } async install(config: InstallConfig) { - return this.runCommand('install', this.paramToCli(this.withSourceSinkKubeconfig(config)), true); + return this.runCommand('install', paramToCli(this.withSourceSinkKubeconfig(config)), true); } async uninstall(config: UninstallConfig) { - return this.runCommand('uninstall', this.paramToCli(this.withSourceSinkKubeconfig(config)), true); + return this.runCommand('uninstall', paramToCli(this.withSourceSinkKubeconfig(config)), true); } async bootstrapDump(config: BootstrapDumpConfig) { - return this.runCommand('bootstrap dump', this.paramToCli(config)); + return this.runCommand('bootstrap dump', paramToCli(config)); } async bootstrapLoad(config: BootstrapLoadConfig) { - return this.runCommand('bootstrap load', this.paramToCli(config)); + return this.runCommand('bootstrap load', paramToCli(config)); } async failover(config: FailoverConfig) { - return this.runCommand('failover', this.paramToCli(this.withSinkKubeconfig(config))); + return this.runCommand('failover', paramToCli(this.withSinkKubeconfig(config))); } async failback(config: FailbackConfig) { - return this.runCommand('failback', this.paramToCli(this.withSinkKubeconfig(config))); + return this.runCommand('failback', paramToCli(this.withSinkKubeconfig(config))); } async status(config: StatusConfig) { - return this.runCommand('status', this.paramToCli(this.withSourceSinkKubeconfig(config))); + return this.runCommand('status', paramToCli(this.withSourceSinkKubeconfig(config))); } async volumeGet(config: VolumeGetConfig) { - return this.runCommand('volume get', this.paramToCli(this.withTargetKubeconfig(config))); + return this.runCommand('volume get', paramToCli(this.withTargetKubeconfig(config))); } async replicationPause(config: ReplicationPauseConfig) { - return this.runCommand('replication pause', this.paramToCli(this.withSourceSinkKubeconfig(config))); + return this.runCommand('replication pause', paramToCli(this.withSourceSinkKubeconfig(config))); } async replicationResume(config: ReplicationResumeConfig) { - return this.runCommand('replication resume', this.paramToCli(this.withSourceSinkKubeconfig(config))); - } - - paramToCli(params: Record): string { - const command: string[] = []; - Object.keys(params).forEach(key => { - const value = params[key]; - if (value !== undefined && value !== null) { - command.push(`--${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`); - command.push(String(value)); - } - }); - return command.join(' '); + return this.runCommand('replication resume', paramToCli(this.withSourceSinkKubeconfig(config))); } }