From fceb8e8a49e3bf297aea5a133b1969bce7f4190d Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 7 May 2024 21:06:11 +0200 Subject: [PATCH] fix(crux-ui): compose env_file apply (#968) --- .../composer/compose-environment.tsx | 13 ++- .../components/composer/use-composer-state.ts | 109 ++++++++++++------ web/crux-ui/src/models/compose.ts | 59 +++++++++- web/crux-ui/src/pages/composer.tsx | 6 +- web/crux-ui/src/validations/compose.ts | 2 +- 5 files changed, 141 insertions(+), 48 deletions(-) diff --git a/web/crux-ui/src/components/composer/compose-environment.tsx b/web/crux-ui/src/components/composer/compose-environment.tsx index 2a116925f..7b12cdd79 100644 --- a/web/crux-ui/src/components/composer/compose-environment.tsx +++ b/web/crux-ui/src/components/composer/compose-environment.tsx @@ -1,4 +1,5 @@ import DyoWrap from '@app/elements/dyo-wrap' +import { DotEnvironment } from '@app/models' import useTranslation from 'next-translate/useTranslation' import DotEnvFileCard from './dot-env-file-card' import { @@ -21,9 +22,9 @@ const ComposeEnvironment = (props: ComposeEnvironmentProps) => { const { t } = useTranslation('compose') - const onEnvFileChange = (name: string, text: string) => dispatch(convertEnvFile(t, name, text)) - const onEnvNameChange = (from: string, to: string) => dispatch(changeEnvFileName(from, to)) - const onRemoveDotEnv = (name: string) => dispatch(removeEnvFile(name)) + const onEnvFileChange = (target: DotEnvironment, text: string) => dispatch(convertEnvFile(t, target, text)) + const onEnvNameChange = (target: DotEnvironment, to: string) => dispatch(changeEnvFileName(target, to)) + const onRemoveDotEnv = (target: DotEnvironment) => dispatch(removeEnvFile(t, target)) const defaultDotEnv = selectDefaultEnvironment(state) @@ -33,9 +34,9 @@ const ComposeEnvironment = (props: ComposeEnvironmentProps) => { onEnvFileChange(it.name, text)} - onNameChange={it !== defaultDotEnv ? name => onEnvNameChange(it.name, name) : null} - onRemove={it !== defaultDotEnv ? () => onRemoveDotEnv(it.name) : null} + onEnvChange={text => onEnvFileChange(it, text)} + onNameChange={it !== defaultDotEnv ? name => onEnvNameChange(it, name) : null} + onRemove={it !== defaultDotEnv ? () => onRemoveDotEnv(it) : null} /> ))} diff --git a/web/crux-ui/src/components/composer/use-composer-state.ts b/web/crux-ui/src/components/composer/use-composer-state.ts index ff2d85491..fc740d26c 100644 --- a/web/crux-ui/src/components/composer/use-composer-state.ts +++ b/web/crux-ui/src/components/composer/use-composer-state.ts @@ -74,10 +74,17 @@ const applyEnvironments = ( const appliedServices = services.map(entry => { const [key, service] = entry - const dotEnvName = service.env_file ?? DEFAULT_ENVIRONMENT_NAME - const dotEnv = envs.find(it => it.name === dotEnvName) - - const applied = applyDotEnvToComposeService(service, dotEnv.environment) + const envFile: string[] = !service.env_file + ? [DEFAULT_ENVIRONMENT_NAME] + : typeof service.env_file === 'string' + ? [service.env_file] + : service.env_file + const dotEnvs = envFile.map(envName => envs.find(it => it.name === envName)).filter(it => !!it) + + let applied = service + dotEnvs.forEach(it => { + applied = applyDotEnvToComposeService(applied, it.environment) + }) return [key, applied] }) @@ -90,11 +97,15 @@ type ApplyComposeToStateOptions = { envedCompose: Compose t: Translate } -const applyComposeToState = (state: ComposerState, options: ApplyComposeToStateOptions) => { +const applyComposeToState = ( + state: ComposerState, + options: ApplyComposeToStateOptions, + environment: DotEnvironment[], +) => { const { t } = options try { - const newContainers = mapComposeServices(options.envedCompose) + const newContainers = mapComposeServices(options.envedCompose, environment) return { ...state, @@ -170,23 +181,27 @@ export const convertComposeFile = services: applyEnvironments(compose?.services, state.environment), } - return applyComposeToState(state, { - compose: { - text, - yaml: compose, - error: null, + return applyComposeToState( + state, + { + compose: { + text, + yaml: compose, + error: null, + }, + envedCompose, + t, }, - envedCompose, - t, - }) + state.environment, + ) } export const convertEnvFile = - (t: Translate, name: string, text: string): ComposerAction => + (t: Translate, target: DotEnvironment, text: string): ComposerAction => state => { const { environment } = state - const index = environment.findIndex(it => it.name === name) + const index = environment.findIndex(it => it === target) if (index < 0) { return state } @@ -223,19 +238,25 @@ export const convertEnvFile = const newEnv = [...environment] newEnv[index] = dotEnv + let newState = state const { compose } = state + if (compose) { + const envedCompose = { + ...compose.yaml, + services: applyEnvironments(compose?.yaml?.services, newEnv), + } - const envedCompose = { - ...compose.yaml, - services: applyEnvironments(compose?.yaml?.services, newEnv), + newState = applyComposeToState( + state, + { + compose, + envedCompose, + t, + }, + newEnv, + ) } - const newState = applyComposeToState(state, { - compose, - envedCompose, - t, - }) - return { ...newState, environment: newEnv, @@ -243,18 +264,18 @@ export const convertEnvFile = } export const changeEnvFileName = - (from: string, to: string): ComposerAction => + (target: DotEnvironment, name: string): ComposerAction => state => { const { environment } = state - const index = environment.findIndex(it => it.name === from) + const index = environment.findIndex(it => it === target) if (index < 0) { return state } const dotEnv: DotEnvironment = { ...environment[index], - name: to, + name, } const newEnv = [...environment] @@ -285,23 +306,45 @@ export const addEnvFile = (): ComposerAction => state => { } export const removeEnvFile = - (name: string): ComposerAction => + (t: Translate, target: DotEnvironment): ComposerAction => state => { - if (name === DEFAULT_ENVIRONMENT_NAME) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const defaultDotEnv = selectDefaultEnvironment(state) + if (defaultDotEnv === target) { return state } const { environment } = state - const index = environment.findIndex(it => it.name === name) + const index = environment.findIndex(it => it === target) if (index < 0) { return state } - return { + const newState = { ...state, - environment: environment.filter(it => it.name !== name), + environment: environment.filter(it => it !== target), + } + + const compose = newState.compose?.yaml + if (!compose) { + return newState + } + + const envedCompose = { + ...compose, + services: applyEnvironments(compose.services, newState.environment), } + + return applyComposeToState( + newState, + { + compose: newState.compose, + envedCompose, + t, + }, + newState.environment, + ) } // selectors diff --git a/web/crux-ui/src/models/compose.ts b/web/crux-ui/src/models/compose.ts index 13ff325cb..cf6b0c8d9 100644 --- a/web/crux-ui/src/models/compose.ts +++ b/web/crux-ui/src/models/compose.ts @@ -12,8 +12,8 @@ import { UniqueKeyValue, VolumeType, } from './container' -import { VersionType } from './version' import { Project } from './project' +import { VersionType } from './version' export const COMPOSE_RESTART_VALUES = ['no', 'always', 'on-failure', 'unless-stopped'] as const export type ComposeRestart = (typeof COMPOSE_RESTART_VALUES)[number] @@ -52,7 +52,7 @@ export type ComposeService = { tty?: boolean working_dir?: string user?: string // we only support numbers, so '0' will work but root won't - env_file?: string + env_file?: string | string[] } export type Compose = { @@ -188,6 +188,29 @@ const mapUser = (user: string): number => { } } +const mapKeyValuesToRecord = (items: string[] | null): Record => + items?.reduce((result, it) => { + const [key, value] = it.split('=') + + result[key] = value + return result + }, {}) + +const mapRecordToKeyValues = (map: Record | null): UniqueKeyValue[] | null => { + if (!map) { + return null + } + + return Object.entries(map).map(entry => { + const [key, value] = entry + return { + id: uuid(), + key, + value, + } + }) +} + const mapKeyValues = (items: string[] | null): UniqueKeyValue[] | null => items?.map(it => { const [key, value] = it.split('=') @@ -210,6 +233,7 @@ const mapStringOrStringArray = (candidate: string | string[]): UniqueKey[] => export const mapComposeServiceToContainerConfig = ( service: ComposeService, serviceKey: string, + envs: DotEnvironment[], ): ContainerConfigData => { const ports: ContainerConfigPort[] = [] const portRanges: ContainerConfigPortRange[] = [] @@ -222,9 +246,34 @@ export const mapComposeServiceToContainerConfig = ( } }) + let environment = mapKeyValuesToRecord(service.environment) + if (service.env_file) { + const envFile = typeof service.env_file === 'string' ? [service.env_file] : service.env_file + + const dotEnvs = envs.filter(it => envFile.includes(it.name)) + if (dotEnvs.length > 0) { + if (!environment) { + environment = {} + } + + const mergedEnvs = dotEnvs.reduce( + (result, it) => ({ + ...result, + ...it.environment, + }), + {}, + ) + + environment = { + ...mergedEnvs, + ...environment, + } + } + } + return { name: service.container_name ?? serviceKey, - environment: mapKeyValues(service.environment), + environment: mapRecordToKeyValues(environment), commands: mapStringOrStringArray(service.entrypoint), args: mapStringOrStringArray(service.command), ports: ports.length > 0 ? ports : null, @@ -263,13 +312,13 @@ export const mapComposeServiceToContainerConfig = ( } } -export const mapComposeServices = (compose: Compose): ConvertedContainer[] => +export const mapComposeServices = (compose: Compose, envs: DotEnvironment[]): ConvertedContainer[] => Object.entries(compose.services).map(entry => { const [key, service] = entry return { image: service.image, - config: mapComposeServiceToContainerConfig(service, key), + config: mapComposeServiceToContainerConfig(service, key, envs), } }) diff --git a/web/crux-ui/src/pages/composer.tsx b/web/crux-ui/src/pages/composer.tsx index edb686973..0d1c135f3 100644 --- a/web/crux-ui/src/pages/composer.tsx +++ b/web/crux-ui/src/pages/composer.tsx @@ -22,7 +22,7 @@ import DyoToggle from '@app/elements/dyo-toggle' import DyoWrap from '@app/elements/dyo-wrap' import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { Project, Registry, VersionDetails, findRegistryByUrl, imageUrlOfImageName } from '@app/models' +import { DotEnvironment, Project, Registry, VersionDetails, findRegistryByUrl, imageUrlOfImageName } from '@app/models' import { appendTeamSlug } from '@app/providers/team-routes' import { ROUTE_COMPOSER, ROUTE_INDEX } from '@app/routes' import { fetcher, redirectTo, teamSlugOrFirstTeam, withContextAuthorization } from '@app/utils' @@ -72,7 +72,7 @@ const ComposerPage = () => { const onComposeFileChange = (text: string) => dispatch(convertComposeFile(t, text)) const onToggleShowDefaultDotEnv = () => dispatch(toggleShowDefaultDotEnv()) - const onEnvFileChange = (name: string, text: string) => dispatch(convertEnvFile(t, name, text)) + const onEnvFileChange = (target: DotEnvironment, text: string) => dispatch(convertEnvFile(t, target, text)) const onAddDotEnv = () => dispatch(addEnvFile()) const onActivateGenerate = () => dispatch(activateUpperSection('generate')) @@ -117,7 +117,7 @@ const ComposerPage = () => { /> {showDefaultEnv && ( - onEnvFileChange(defaultDotEnv.name, text)} /> + onEnvFileChange(defaultDotEnv, text)} /> )} ) : ( diff --git a/web/crux-ui/src/validations/compose.ts b/web/crux-ui/src/validations/compose.ts index 2cbc7bd00..2b751d561 100644 --- a/web/crux-ui/src/validations/compose.ts +++ b/web/crux-ui/src/validations/compose.ts @@ -65,7 +65,7 @@ export const composeServiceSchema = yup.object().shape({ return it }), - env_file: yup.string().optional().nullable(), + env_file: mixedStringOrStringArrayRule.optional().nullable(), }) export const composeSchema = yup