Skip to content
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

fix(crux-ui): compose env_file apply #968

Merged
merged 1 commit into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions web/crux-ui/src/components/composer/compose-environment.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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)

Expand All @@ -33,9 +34,9 @@ const ComposeEnvironment = (props: ComposeEnvironmentProps) => {
<DotEnvFileCard
key={`dot-env-${index}`}
dotEnv={it}
onEnvChange={text => 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}
/>
))}
</DyoWrap>
Expand Down
109 changes: 76 additions & 33 deletions web/crux-ui/src/components/composer/use-composer-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
})
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -223,38 +238,44 @@ 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,
}
}

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]
Expand Down Expand Up @@ -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
Expand Down
59 changes: 54 additions & 5 deletions web/crux-ui/src/models/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -188,6 +188,29 @@ const mapUser = (user: string): number => {
}
}

const mapKeyValuesToRecord = (items: string[] | null): Record<string, string> =>
items?.reduce((result, it) => {
const [key, value] = it.split('=')

result[key] = value
return result
}, {})

const mapRecordToKeyValues = (map: Record<string, string> | 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('=')
Expand All @@ -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[] = []
Expand All @@ -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,
Expand Down Expand Up @@ -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),
}
})

Expand Down
6 changes: 3 additions & 3 deletions web/crux-ui/src/pages/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -117,7 +117,7 @@ const ComposerPage = () => {
/>

{showDefaultEnv && (
<DotEnvFileCard dotEnv={defaultDotEnv} onEnvChange={text => onEnvFileChange(defaultDotEnv.name, text)} />
<DotEnvFileCard dotEnv={defaultDotEnv} onEnvChange={text => onEnvFileChange(defaultDotEnv, text)} />
)}
</div>
) : (
Expand Down
2 changes: 1 addition & 1 deletion web/crux-ui/src/validations/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading