-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update argument parsing to remove dependency on yargs and make more robust #1019
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.opensearch.migrations.jcommander; | ||
|
||
import java.util.List; | ||
|
||
import com.beust.jcommander.converters.IParameterSplitter; | ||
|
||
public class NoSplitter implements IParameterSplitter { | ||
@Override | ||
public List<String> split(String value) { | ||
return List.of(value); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,87 +4,66 @@ import {CpuArchitecture} from "aws-cdk-lib/aws-ecs"; | |
import {RemovalPolicy, Stack} from "aws-cdk-lib"; | ||
import { IStringParameter, StringParameter } from "aws-cdk-lib/aws-ssm"; | ||
import * as forge from 'node-forge'; | ||
import * as yargs from 'yargs'; | ||
import { ClusterYaml } from "./migration-services-yaml"; | ||
|
||
export function getTargetPasswordAccessPolicy(targetPasswordSecretArn: string): PolicyStatement { | ||
return new PolicyStatement({ | ||
effect: Effect.ALLOW, | ||
resources: [targetPasswordSecretArn], | ||
actions: [ | ||
"secretsmanager:GetSecretValue" | ||
] | ||
}) | ||
} | ||
|
||
// parseAndMergeArgs, see @common-utilities.test.ts for an example of different cases | ||
export function parseAndMergeArgs(baseCommand: string, extraArgs?: string): string { | ||
if (!extraArgs) { | ||
return baseCommand; | ||
export function appendArgIfNotInExtraArgs( | ||
baseCommand: string, | ||
extraArgsDict: Record<string, string[]>, | ||
arg: string, | ||
value: string | null = null, | ||
): string { | ||
if (extraArgsDict[arg] === undefined) { | ||
// If not present, append the argument and value (only append value if it exists) | ||
baseCommand = value !== null ? baseCommand.concat(" ", arg, " ", value) : baseCommand.concat(" ", arg); | ||
} | ||
return baseCommand; | ||
} | ||
|
||
// Extract command prefix | ||
const commandPrefix = baseCommand.substring(0, baseCommand.indexOf('--')).trim(); | ||
const baseArgs = baseCommand.substring(baseCommand.indexOf('--')); | ||
export function parseArgsToDict(argString: string | undefined): Record<string, string[]> { | ||
const args: Record<string, string[]> = {}; | ||
if (argString === undefined) { | ||
return args; | ||
} | ||
// Split based on '--' at the start of the string or preceded by whitespace, use non-capturing groups to include -- in parts | ||
const parts = argString.split(/(?=\s--|^--)/).filter(Boolean); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worst case scenario here is that you could misparse something like
and you'd then drop any arguments that already existed that were named '--alpine' or '--classic', right? I'd like to see the map eventually supported as just a map of keys and values (possibly the empty string) - and all of it is just concatenated together w/ spaces between. That way, it's clear what keys are being used and what to strike. Given that the code was even more problematic before, I'm fine w/ this being a follow up task. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be a change in the customer cdk.context.json to have extra args be a map and not a string? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's the worst case, something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct - to accept the value as a dictionary/map and not just as a string (I'd support both using instanceof). For later though. |
||
|
||
// Parse base command | ||
const baseYargsConfig = { | ||
parserConfiguration: { | ||
'camel-case-expansion': false, | ||
'boolean-negation': false, | ||
} | ||
}; | ||
parts.forEach(part => { | ||
const trimmedPart = part.trim(); | ||
if (trimmedPart.length === 0) return; // Skip empty parts | ||
|
||
const baseArgv = yargs(baseArgs) | ||
.parserConfiguration(baseYargsConfig.parserConfiguration) | ||
.parse(); | ||
// Use a regular expression to find the first whitespace character | ||
const firstWhitespaceMatch = trimmedPart.match(/\s/); | ||
const firstWhitespaceIndex = firstWhitespaceMatch?.index; | ||
|
||
// Parse extra args if provided | ||
const extraYargsConfig = { | ||
parserConfiguration: { | ||
'camel-case-expansion': false, | ||
'boolean-negation': true, | ||
} | ||
}; | ||
|
||
const extraArgv = extraArgs | ||
? yargs(extraArgs.split(' ')) | ||
.parserConfiguration(extraYargsConfig.parserConfiguration) | ||
.parse() | ||
: {}; | ||
|
||
// Merge arguments | ||
const mergedArgv: { [key: string]: unknown } = { ...baseArgv }; | ||
for (const [key, value] of Object.entries(extraArgv)) { | ||
if (key !== '_' && key !== '$0') { | ||
if (!value && | ||
typeof value === 'boolean' && | ||
( | ||
typeof (baseArgv as any)[key] === 'boolean' || | ||
(typeof (baseArgv as any)[`no-${key}`] != 'boolean' && typeof (baseArgv as any)[`no-${key}`]) | ||
) | ||
) { | ||
delete mergedArgv[key]; | ||
const key = firstWhitespaceIndex === undefined ? trimmedPart : trimmedPart.slice(0, firstWhitespaceIndex).trim(); | ||
const value = firstWhitespaceIndex === undefined ? '' : trimmedPart.slice(firstWhitespaceIndex + 1).trim(); | ||
|
||
// Validate the key starts with -- followed by a non-whitespace characters | ||
if (/^--\S+/.test(key)) { | ||
if (args[key] !== undefined) { | ||
args[key].push(value); | ||
} else { | ||
mergedArgv[key] = value; | ||
args[key] = [value]; | ||
} | ||
} else { | ||
throw new Error(`Invalid argument key: '${key}'. Argument keys must start with '--' and contain no spaces.`); | ||
} | ||
}); | ||
if (argString.trim() && !args) { | ||
throw new Error(`Unable to parse args provided: '${argString}'`); | ||
} | ||
|
||
// Reconstruct command | ||
const mergedArgs = Object.entries(mergedArgv) | ||
.filter(([key]) => key !== '_' && key !== '$0') | ||
.map(([key, value]) => { | ||
if (typeof value === 'boolean') { | ||
return value ? `--${key}` : `--no-${key}`; | ||
} | ||
return `--${key} ${value}`; | ||
}) | ||
.join(' '); | ||
|
||
let fullCommand = `${commandPrefix} ${mergedArgs}`.trim() | ||
return fullCommand; | ||
} | ||
|
||
export function getTargetPasswordAccessPolicy(targetPasswordSecretArn: string): PolicyStatement { | ||
return new PolicyStatement({ | ||
effect: Effect.ALLOW, | ||
resources: [targetPasswordSecretArn], | ||
actions: [ | ||
"secretsmanager:GetSecretValue" | ||
] | ||
}) | ||
return args; | ||
} | ||
|
||
export function createOpenSearchIAMAccessPolicy(partition: string, region: string, accountId: string): PolicyStatement { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: We're not using the values (only keys) as of now (except the unit tests). I wanted to keep this since it's written, working, and tested