Skip to content

Commit

Permalink
fix(web): missing composer image config patches (#969)
Browse files Browse the repository at this point in the history
  • Loading branch information
m8vago authored May 8, 2024
1 parent d55f4f5 commit a71886e
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 108 deletions.
21 changes: 21 additions & 0 deletions web/crux-ui/src/components/composer/generate-version-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import {
ConvertedContainer,
CreateProject,
CreateVersion,
PatchVersionImage,
Project,
ProjectDetails,
Registry,
VERSION_TYPE_VALUES,
VersionDetails,
VersionImage,
findRegistryByUrl,
imageUrlOfImageName as imageUrlOfImageTag,
} from '@app/models'
Expand Down Expand Up @@ -112,11 +114,30 @@ const GenerateVersionCard = (props: GenerateVersionCardProps) => {
routes.project.versions(project.id).api.images(version.id),
addImagesBody,
)

if (!createImagesRes.ok) {
await handleApiError(createImagesRes)
return
}

const images = (await createImagesRes.json()) as VersionImage[]

const configPatches = images.map((image, index) => {
const container = containers[index]

const patchImageBody: PatchVersionImage = {
config: container.config,
}

return sendForm(
'PATCH',
routes.project.versions(project.id).api.imageDetails(version.id, image.id),
patchImageBody,
)
})

await Promise.all(configPatches)

await onVersionGenerated(project, version)
},
})
Expand Down
100 changes: 61 additions & 39 deletions web/crux-ui/src/components/composer/use-composer-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DotEnvironment,
applyDotEnvToComposeService,
mapComposeServices,
mapKeyValuesToRecord as mapKeyValuesToObject,
} from '@app/models'
import { composeSchema, getValidationError } from '@app/validations'
import { load } from 'js-yaml'
Expand Down Expand Up @@ -66,6 +67,33 @@ export const toggleShowDefaultDotEnv = (): ComposerAction => state => ({
showDefaultDotEnv: !state.showDefaultDotEnv,
})

const mergeDotEnvsWithServiceEnv = (envs: DotEnvironment[], serviceEnv: string[] | null): string[] | null => {
if (envs.length < 1) {
return serviceEnv
}

let merged = envs.reduce(
(result, it) => ({
...result,
...it.environment,
}),
{},
)

if (serviceEnv) {
const serviceEnvObj = mapKeyValuesToObject(serviceEnv)
merged = {
...merged,
...serviceEnvObj,
}
}

return Object.entries(merged).map(entry => {
const [key, value] = entry
return `${key}=${value}`
})
}

const applyEnvironments = (
composeServices: Record<string, ComposeService>,
envs: DotEnvironment[],
Expand All @@ -75,13 +103,23 @@ const applyEnvironments = (
const [key, service] = entry

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 dotEnvs = envs.filter(it => envFile.includes(it.name))

// add explicit envs to environment
let applied: ComposeService = {
...service,
environment: mergeDotEnvsWithServiceEnv(dotEnvs, service.environment),
}

const defaultDotEnv = envs.find(it => it.name === DEFAULT_ENVIRONMENT_NAME)
if (dotEnvs.length < 1 && defaultDotEnv) {
dotEnvs = [defaultDotEnv]
}

let applied = service
dotEnvs.forEach(it => {
applied = applyDotEnvToComposeService(applied, it.environment)
})
Expand All @@ -97,15 +135,11 @@ type ApplyComposeToStateOptions = {
envedCompose: Compose
t: Translate
}
const applyComposeToState = (
state: ComposerState,
options: ApplyComposeToStateOptions,
environment: DotEnvironment[],
) => {
const applyComposeToState = (state: ComposerState, options: ApplyComposeToStateOptions) => {
const { t } = options

try {
const newContainers = mapComposeServices(options.envedCompose, environment)
const newContainers = mapComposeServices(options.envedCompose)

return {
...state,
Expand Down Expand Up @@ -181,19 +215,15 @@ export const convertComposeFile =
services: applyEnvironments(compose?.services, state.environment),
}

return applyComposeToState(
state,
{
compose: {
text,
yaml: compose,
error: null,
},
envedCompose,
t,
return applyComposeToState(state, {
compose: {
text,
yaml: compose,
error: null,
},
state.environment,
)
envedCompose,
t,
})
}

export const convertEnvFile =
Expand Down Expand Up @@ -246,15 +276,11 @@ export const convertEnvFile =
services: applyEnvironments(compose?.yaml?.services, newEnv),
}

newState = applyComposeToState(
state,
{
compose,
envedCompose,
t,
},
newEnv,
)
newState = applyComposeToState(state, {
compose,
envedCompose,
t,
})
}

return {
Expand Down Expand Up @@ -336,15 +362,11 @@ export const removeEnvFile =
services: applyEnvironments(compose.services, newState.environment),
}

return applyComposeToState(
newState,
{
compose: newState.compose,
envedCompose,
t,
},
newState.environment,
)
return applyComposeToState(newState, {
compose: newState.compose,
envedCompose,
t,
})
}

// selectors
Expand Down
91 changes: 33 additions & 58 deletions web/crux-ui/src/models/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,29 +188,14 @@ const mapUser = (user: string): number => {
}
}

const mapKeyValuesToRecord = (items: string[] | null): Record<string, string> =>
export 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 @@ -233,7 +218,6 @@ const mapStringOrStringArray = (candidate: string | string[]): UniqueKey[] =>
export const mapComposeServiceToContainerConfig = (
service: ComposeService,
serviceKey: string,
envs: DotEnvironment[],
): ContainerConfigData => {
const ports: ContainerConfigPort[] = []
const portRanges: ContainerConfigPortRange[] = []
Expand All @@ -246,34 +230,9 @@ 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: mapRecordToKeyValues(environment),
environment: mapKeyValues(service.environment),
commands: mapStringOrStringArray(service.entrypoint),
args: mapStringOrStringArray(service.command),
ports: ports.length > 0 ? ports : null,
Expand Down Expand Up @@ -312,35 +271,33 @@ export const mapComposeServiceToContainerConfig = (
}
}

export const mapComposeServices = (compose: Compose, envs: DotEnvironment[]): ConvertedContainer[] =>
export const mapComposeServices = (compose: Compose): ConvertedContainer[] =>
Object.entries(compose.services).map(entry => {
const [key, service] = entry

return {
image: service.image,
config: mapComposeServiceToContainerConfig(service, key, envs),
config: mapComposeServiceToContainerConfig(service, key),
}
})

class DotEnvApplicator {
export class DotEnvApplicator {
constructor(private readonly dotEnv: Record<string, string>) {}

applyToString(candidate: string): string {
if (!candidate) {
return candidate
}
let original = candidate ?? undefined
let applied = this.applyToStringOnce(candidate) ?? undefined

candidate = candidate.replace(/\${[^}]*}/g, subStr => {
const envName = subStr.substring(2, subStr.length - 1)
return this.applyEnvToFoundEnv(envName)
})
let iterations = 0
while (original !== applied && iterations < 32) {
original = applied
applied = this.applyToStringOnce(original) ?? undefined
candidate = applied

candidate = candidate.replace(/\$[^{ ]*\s/g, subStr => {
const envName = subStr.substring(1).trim()
return this.applyEnvToFoundEnv(envName)
})
iterations++
}

return candidate
return applied
}

applyToStringArray(candidate: string[]): string[] {
Expand Down Expand Up @@ -412,6 +369,24 @@ class DotEnvApplicator {

return this.dotEnv[key] ?? defaultValue ?? `\${${key}}`
}

private applyToStringOnce(candidate: string): string {
if (!candidate) {
return candidate
}

candidate = candidate.replace(/\${[^}]*}/g, subStr => {
const envName = subStr.substring(2, subStr.length - 1)
return this.applyEnvToFoundEnv(envName)
})

candidate = candidate.replace(/\$[^{][a-zA-Z0-9_]*/g, subStr => {
const envName = subStr.substring(1).trim()
return this.applyEnvToFoundEnv(envName)
})

return candidate
}
}

export const applyDotEnvToComposeService = (
Expand Down
8 changes: 6 additions & 2 deletions web/crux-ui/src/pages/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const ComposerPage = () => {
[registries],
)

const missingRegistries = !state.containers.every(it => hasRegistry(it.image))

const submit = useSubmit()

const onComposeFileChange = (text: string) => dispatch(convertComposeFile(t, text))
Expand Down Expand Up @@ -94,14 +96,16 @@ const ComposerPage = () => {
<Layout title={t('common:composer')}>
<PageHeading pageLink={pageLink}>
{state.upperSection === 'compose' ? (
<DyoButton onClick={onActivateGenerate}>{t('generate')}</DyoButton>
<DyoButton onClick={onActivateGenerate} disabled={missingRegistries}>
{t('generate')}
</DyoButton>
) : (
<>
<DyoButton className="px-4" secondary onClick={onDiscardVersion}>
{t('common:discard')}
</DyoButton>

<DyoButton className="px-4 ml-4" onClick={onGenerateVersion}>
<DyoButton className="px-4 ml-4" onClick={onGenerateVersion} disabled={missingRegistries}>
{t('generate')}
</DyoButton>
</>
Expand Down
Loading

0 comments on commit a71886e

Please sign in to comment.