diff --git a/src/__tests__/node/buildCliConfig.test.ts b/src/__tests__/node/buildCliConfig.test.ts new file mode 100644 index 000000000..416401c0d --- /dev/null +++ b/src/__tests__/node/buildCliConfig.test.ts @@ -0,0 +1,205 @@ +import { describe, expect, it } from 'vitest'; +import { buildCliConfig, buildEnvInput } from '../../common/nodeConfig.js'; + +describe('Building cli config and env variable cli input', () => { + it('Successfully creates a single env variable cli input', async () => { + const configValuesMap = { plexClaimCode: 'claim123' }; + const configTranslationMap = { + plexClaimCode: { + displayName: 'Plex claim code', + cliConfigPrefix: 'PLEX_CLAIM=', + uiControl: { + type: 'text', + }, + isEnvVariable: true, + addNodeFlow: 'required', + infoDescription: + 'Sign in at https://www.plex.tv/claim/, get a code, and paste it here', + defaultValue: '', + documentation: 'https://www.plex.tv/claim/', + }, + }; + const cliConfig1 = buildCliConfig({ + configValuesMap, + configTranslationMap, + excludeConfigKeys: [], + isBuildingEnvInput: true, + }); + + expect(cliConfig1).toEqual('-e PLEX_CLAIM=claim123'); + }); + + it('Successfully creates multiple env variable cli input', async () => { + const configValuesMap = { plexClaimCode: 'claim123', httpPortEnv: '8080' }; + const configTranslationMap = { + plexClaimCode: { + displayName: 'Plex claim code', + cliConfigPrefix: 'PLEX_CLAIM=', + uiControl: { + type: 'text', + }, + isEnvVariable: true, + addNodeFlow: 'required', + infoDescription: + 'Sign in at https://www.plex.tv/claim/, get a code, and paste it here', + defaultValue: '', + documentation: 'https://www.plex.tv/claim/', + }, + httpPortEnv: { + displayName: 'HTTP PORT', + cliConfigPrefix: 'HTTP_PORT=', + uiControl: { + type: 'text', + }, + isEnvVariable: true, + }, + }; + const cliConfig1 = buildCliConfig({ + configValuesMap, + configTranslationMap, + excludeConfigKeys: [], + isBuildingEnvInput: true, + }); + + expect(cliConfig1).toEqual('-e PLEX_CLAIM=claim123 -e HTTP_PORT=8080'); + }); + + it('Successfully creates a single cli config input', async () => { + const configValuesMap = { httpPort: '8080' }; + const configTranslationMap = { + httpPort: { + displayName: 'HTTP PORT', + cliConfigPrefix: '--http-port=', + uiControl: { + type: 'text', + }, + }, + }; + const cliConfig1 = buildCliConfig({ + configValuesMap, + configTranslationMap, + excludeConfigKeys: [], + }); + + expect(cliConfig1).toEqual('--http-port=8080'); + }); + + it('Successfully creates multiple cli config input', async () => { + const configValuesMap = { + httpPort: '8080', + network: 'Mainnet', + plexClaimCode: 'claimIgnoreMe', + }; + const configTranslationMap = { + httpPort: { + displayName: 'HTTP PORT', + cliConfigPrefix: '--http-port=', + uiControl: { + type: 'text', + }, + }, + network: { + displayName: 'Network', + defaultValue: 'Mainnet', + hideFromUserAfterStart: true, + uiControl: { + type: 'select/single', + controlTranslations: [ + { + value: 'Mainnet', + config: '--mainnet', + }, + { + value: 'Holesky', + config: '--holesky', + }, + { + value: 'Sepolia', + config: '--sepolia', + }, + ], + }, + }, + plexClaimCode: { + displayName: 'Plex claim code', + cliConfigPrefix: 'PLEX_CLAIM=', + uiControl: { + type: 'text', + }, + isEnvVariable: true, + addNodeFlow: 'required', + infoDescription: + 'Sign in at https://www.plex.tv/claim/, get a code, and paste it here', + defaultValue: '', + documentation: 'https://www.plex.tv/claim/', + }, + }; + const cliConfig1 = buildCliConfig({ + configValuesMap, + configTranslationMap, + excludeConfigKeys: [], + }); + + expect(cliConfig1).toEqual('--http-port=8080 --mainnet'); + }); + + it('Successfully ignores a single cli config input when building env input', async () => { + const configValuesMap = { httpPort: '8080' }; + const configTranslationMap = { + httpPort: { + displayName: 'HTTP PORT', + cliConfigPrefix: '--http-port=', + uiControl: { + type: 'text', + }, + }, + }; + const cliConfig1 = buildCliConfig({ + configValuesMap, + configTranslationMap, + excludeConfigKeys: [], + isBuildingEnvInput: true, + }); + + expect(cliConfig1).toEqual(''); + }); + + it('Successfully ignores a single cli config input and adds 1 env var when building env input', async () => { + const configValuesMap = { httpPort: '8080', myFavEnvVar: 'muchwow' }; + const configTranslationMap = { + httpPort: { + displayName: 'HTTP PORT', + cliConfigPrefix: '--http-port=', + uiControl: { + type: 'text', + }, + }, + myFavEnvVar: { + displayName: 'Plex claim code', + cliConfigPrefix: 'MYFAVENVVAR=', + uiControl: { + type: 'text', + }, + isEnvVariable: true, + }, + }; + const cliConfig1 = buildCliConfig({ + configValuesMap, + configTranslationMap, + excludeConfigKeys: [], + isBuildingEnvInput: true, + }); + + expect(cliConfig1).toEqual('-e MYFAVENVVAR=muchwow'); + }); + + it('[buildEnvInput] Successfully builds env input for 2 user supplied env vars', async () => { + const configValuesMap = { + envInput: 'ETHPORTPORT=123456789,BASEPORT=987', + myFavEnvVar: 'muchwow', + }; + const cliConfig1 = buildEnvInput(configValuesMap.envInput); + + expect(cliConfig1).toEqual('-e ETHPORTPORT=123456789 -e BASEPORT=987'); + }); +}); diff --git a/src/common/node-spec-tool/injectDefaultControllerConfig.ts b/src/common/node-spec-tool/injectDefaultControllerConfig.ts index 1993ffd12..1d0bd7d9c 100644 --- a/src/common/node-spec-tool/injectDefaultControllerConfig.ts +++ b/src/common/node-spec-tool/injectDefaultControllerConfig.ts @@ -23,6 +23,18 @@ export const injectDefaultControllerConfig = (nodeSpec: NodeSpecification) => { nodeSpec.configTranslation = {}; } + if (!nodeSpec.configTranslation.envInput) { + nodeSpec.configTranslation.envInput = { + displayName: `${nodeSpec.displayName} ENV input`, + uiControl: { + type: 'text', + }, + defaultValue: '', + addNodeFlow: 'advanced', + infoDescription: 'Additional ENV input, comma separated', + }; + } + if (!nodeSpec.configTranslation.cliInput) { nodeSpec.configTranslation.cliInput = { displayName: `${nodeSpec.displayName} CLI input`, diff --git a/src/common/nodeConfig.ts b/src/common/nodeConfig.ts index efcdf1cc5..d7079b0ab 100644 --- a/src/common/nodeConfig.ts +++ b/src/common/nodeConfig.ts @@ -53,6 +53,7 @@ export type ConfigTranslation = { addNodeFlow?: ConfigTranslationAddNodeFlow; initCommandConfig?: boolean; hideFromUserAfterStart?: boolean; + isEnvVariable?: boolean; }; export type ConfigKey = string; @@ -89,23 +90,31 @@ export const buildCliConfig = ({ configValuesMap, configTranslationMap, excludeConfigKeys, + isBuildingEnvInput, }: { configValuesMap: ConfigValuesMap; configTranslationMap?: ConfigTranslationMap; excludeConfigKeys?: string[]; + isBuildingEnvInput?: boolean; }): string => { if (!configTranslationMap) return ''; const cliConfigArray = Object.keys(configValuesMap).reduce( (cliString, configKey) => { - if (excludeConfigKeys?.includes(configKey)) { - return cliString; - } const configValue = configValuesMap[configKey]; const configTranslation: ConfigTranslation = configTranslationMap[configKey]; + // Only include env vars if building env input, ignore env vars if building cli input + if ( + excludeConfigKeys?.includes(configKey) || + isBuildingEnvInput !== configTranslation?.isEnvVariable + ) { + return cliString; + } + if (configTranslation && configValue) { let currCliString = ''; + if (configTranslation.cliConfigPrefix) { if (typeof configTranslation.cliConfigPrefix === 'string') { currCliString = configTranslation.cliConfigPrefix; @@ -161,12 +170,17 @@ export const buildCliConfig = ({ currCliString += ` ${configTranslation.cliConfigPrefix[i]}${configValue}`; } } + // each env var needs to be prefixed with -e for podman/docker + if (isBuildingEnvInput) { + currCliString = `-e ${currCliString}`; + } console.log( 'cliString, currCliString: ', JSON.stringify(cliString), JSON.stringify(currCliString), ); + // join the current config with the previous and a space between if (cliString) { return `${cliString} ${currCliString}`; @@ -179,3 +193,11 @@ export const buildCliConfig = ({ ); return cliConfigArray; }; + +export const buildEnvInput = (input: string) => { + if (!input) { + return ''; + } + const pairs = input.split(','); + return pairs.map((pair) => `-e ${pair.trim()}`).join(' '); +}; diff --git a/src/main/podman/podman.ts b/src/main/podman/podman.ts index eb4ed13cf..471d6f7a1 100644 --- a/src/main/podman/podman.ts +++ b/src/main/podman/podman.ts @@ -20,6 +20,7 @@ import { type ConfigValuesMap, buildCliConfig, } from '../../common/nodeConfig'; +import { buildEnvInput } from '../../common/nodeConfig.js'; import { CHANNELS, send } from '../messenger.js'; import { restartNodes } from '../nodePackageManager'; import { isLinux } from '../platform'; @@ -546,6 +547,7 @@ export const createRunCommand = (node: Node): string => { const excludeConfigKeys = [ 'dataDir', 'serviceVersion', + 'envInput', ...initCommandConfigKeys, ]; if ( @@ -562,6 +564,16 @@ export const createRunCommand = (node: Node): string => { }); nodeInput += ` ${cliConfigInput}`; + const directUserEnvConfigInput = buildEnvInput( + node.config.configValuesMap.envInput, + ); + const envConfigInput = buildCliConfig({ + configValuesMap: node.config.configValuesMap, + configTranslationMap: node.spec.configTranslation, + excludeConfigKeys, + isBuildingEnvInput: true, + }); + const imageTag = getImageTag(node); // if imageTage is empty, use then imageTag is already included in the imageName (backwards compatability) const imageNameWithTag = @@ -569,7 +581,7 @@ export const createRunCommand = (node: Node): string => { const containerName = getContainerName(node); // -q quiets podman logs (pulling new image logs) so we can parse the containerId - const podmanCommand = `run -q -d --name ${containerName} ${finalPodmanInput} ${imageNameWithTag} ${nodeInput}`; + const podmanCommand = `run -q -d ${envConfigInput} ${directUserEnvConfigInput} --name ${containerName} ${finalPodmanInput} ${imageNameWithTag} ${nodeInput}`; logger.info(`podman run command ${podmanCommand}`); return podmanCommand; };