fix(node): add project name sanitization for Docker commands. (#31461)
This PR improves our Docker support by sanitizing project names for compatibility with Docker commands and Linux systems. closes: #31421
This commit is contained in:
parent
66eaf2fc74
commit
33bfc51ec2
@ -18,6 +18,7 @@ RUN addgroup --system docker-express-app && \\
|
||||
adduser --system -G docker-express-app docker-express-app
|
||||
|
||||
COPY dist/apps/docker-express-app docker-express-app/
|
||||
COPY apps/docker-express-app/package.json docker-express-app/
|
||||
RUN chown -R docker-express-app:docker-express-app .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
|
||||
@ -18,6 +18,7 @@ RUN addgroup --system node-nest-docker-test && \\
|
||||
adduser --system -G node-nest-docker-test node-nest-docker-test
|
||||
|
||||
COPY dist/node-nest-docker-test node-nest-docker-test/
|
||||
COPY node-nest-docker-test/package.json node-nest-docker-test/
|
||||
RUN chown -R node-nest-docker-test:node-nest-docker-test .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# Build the docker image with `npx nx docker-build <%= project %>`.
|
||||
# Tip: Modify "docker-build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with `docker run -p 3000:3000 -t <%= project %>`.
|
||||
# Run the container with `docker run -p 3000:3000 -t <%= sanitizedProjectName %>`.
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
@ -11,14 +11,15 @@ ENV PORT=3000
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system <%= project %> && \
|
||||
adduser --system -G <%= project %> <%= project %>
|
||||
RUN addgroup --system <%= sanitizedProjectName %> && \
|
||||
adduser --system -G <%= sanitizedProjectName %> <%= sanitizedProjectName %>
|
||||
|
||||
COPY <%= buildLocation %> <%= project %>/
|
||||
RUN chown -R <%= project %>:<%= project %> .
|
||||
COPY <%= buildLocation %> <%= sanitizedProjectName %>/
|
||||
COPY <%= projectPath %>/package.json <%= sanitizedProjectName %>/
|
||||
RUN chown -R <%= sanitizedProjectName %>:<%= sanitizedProjectName %> .
|
||||
|
||||
# You can remove this install step if you build with `--bundle` option.
|
||||
# The bundled output will include external dependencies.
|
||||
RUN npm --prefix <%= project %> --omit=dev -f install
|
||||
RUN npm --prefix <%= sanitizedProjectName %> --omit=dev -f install
|
||||
|
||||
CMD [ "node", "<%= project %>" ]
|
||||
CMD [ "node", "<%= sanitizedProjectName %>" ]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { readProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { applicationGenerator } from '../application/application';
|
||||
import { setupDockerGenerator } from './setup-docker';
|
||||
|
||||
describe('setupDockerGenerator', () => {
|
||||
let tree: Tree;
|
||||
@ -65,4 +66,198 @@ describe('setupDockerGenerator', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('project name sanitization', () => {
|
||||
it('should sanitize project names with special characters for Docker commands', async () => {
|
||||
const projectName = '@myorg/my-app';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
await setupDockerGenerator(tree, {
|
||||
project: projectName,
|
||||
outputPath: 'dist/myorg/my-app',
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
|
||||
expect(project.targets['docker-build']).toEqual({
|
||||
dependsOn: ['build'],
|
||||
command: `docker build -f Dockerfile . -t myorg-my-app`,
|
||||
});
|
||||
|
||||
expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(`
|
||||
"# This file is generated by Nx.
|
||||
#
|
||||
# Build the docker image with \`npx nx docker-build @myorg/my-app\`.
|
||||
# Tip: Modify "docker-build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with \`docker run -p 3000:3000 -t myorg-my-app\`.
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system myorg-my-app && \\
|
||||
adduser --system -G myorg-my-app myorg-my-app
|
||||
|
||||
COPY dist/myorg/my-app myorg-my-app/
|
||||
COPY ./package.json myorg-my-app/
|
||||
RUN chown -R myorg-my-app:myorg-my-app .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
# The bundled output will include external dependencies.
|
||||
RUN npm --prefix myorg-my-app --omit=dev -f install
|
||||
|
||||
CMD [ "node", "myorg-my-app" ]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should sanitize project names with slashes and other special characters', async () => {
|
||||
const projectName = 'my/special@app';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
await setupDockerGenerator(tree, {
|
||||
project: projectName,
|
||||
outputPath: 'dist/basic-app',
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
|
||||
expect(project.targets['docker-build']).toEqual({
|
||||
dependsOn: ['build'],
|
||||
command: `docker build -f Dockerfile . -t my-special-app`,
|
||||
});
|
||||
|
||||
expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(`
|
||||
"# This file is generated by Nx.
|
||||
#
|
||||
# Build the docker image with \`npx nx docker-build my/special@app\`.
|
||||
# Tip: Modify "docker-build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with \`docker run -p 3000:3000 -t my-special-app\`.
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system my-special-app && \\
|
||||
adduser --system -G my-special-app my-special-app
|
||||
|
||||
COPY dist/basic-app my-special-app/
|
||||
COPY ./package.json my-special-app/
|
||||
RUN chown -R my-special-app:my-special-app .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
# The bundled output will include external dependencies.
|
||||
RUN npm --prefix my-special-app --omit=dev -f install
|
||||
|
||||
CMD [ "node", "my-special-app" ]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle uppercase and multiple special characters', async () => {
|
||||
const projectName = 'My_App@123/Test';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
docker: true,
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
|
||||
expect(project.targets['docker-build']).toEqual({
|
||||
dependsOn: ['build'],
|
||||
command: `docker build -f Dockerfile . -t my_app-123-test`,
|
||||
});
|
||||
|
||||
expect(tree.read('Dockerfile', 'utf8')).toMatchInlineSnapshot(`
|
||||
"# This file is generated by Nx.
|
||||
#
|
||||
# Build the docker image with \`npx nx docker-build My_App@123/Test\`.
|
||||
# Tip: Modify "docker-build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with \`docker run -p 3000:3000 -t my_app-123-test\`.
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system my_app-123-test && \\
|
||||
adduser --system -G my_app-123-test my_app-123-test
|
||||
|
||||
COPY dist/My_App@123/Test my_app-123-test/
|
||||
COPY ./package.json my_app-123-test/
|
||||
RUN chown -R my_app-123-test:my_app-123-test .
|
||||
|
||||
# You can remove this install step if you build with \`--bundle\` option.
|
||||
# The bundled output will include external dependencies.
|
||||
RUN npm --prefix my_app-123-test --omit=dev -f install
|
||||
|
||||
CMD [ "node", "my_app-123-test" ]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should ensure docker-build target works with sanitized names in Dockerfile', async () => {
|
||||
const projectName = '@scope/my-app';
|
||||
|
||||
await applicationGenerator(tree, {
|
||||
name: projectName,
|
||||
directory: '.',
|
||||
framework: 'express',
|
||||
e2eTestRunner: 'none',
|
||||
addPlugin: true,
|
||||
});
|
||||
|
||||
await setupDockerGenerator(tree, {
|
||||
project: projectName,
|
||||
outputPath: 'dist/scope/my-app',
|
||||
});
|
||||
|
||||
const dockerfileContent = tree.read('Dockerfile', 'utf8');
|
||||
|
||||
expect(dockerfileContent).toMatch(/^FROM\s+\S+/m);
|
||||
expect(dockerfileContent).toMatch(/^WORKDIR\s+\S+/m);
|
||||
expect(dockerfileContent).toMatch(/^CMD\s+\[/m);
|
||||
|
||||
// Verify user/group names are valid (no special chars that would break Linux)
|
||||
const userGroupMatches = dockerfileContent.match(
|
||||
/addgroup --system (\S+)/
|
||||
);
|
||||
const userGroupName = userGroupMatches?.[1];
|
||||
expect(userGroupName).toMatch(/^[a-z0-9._-]+$/);
|
||||
expect(userGroupName).not.toContain('@');
|
||||
expect(userGroupName).not.toContain('/');
|
||||
|
||||
const project = readProjectConfiguration(tree, projectName);
|
||||
expect(project.targets['docker-build'].command).toContain(
|
||||
`-t ${userGroupName}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -25,6 +25,15 @@ function normalizeOptions(
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeProjectName(projectName: string): string {
|
||||
return projectName
|
||||
.toLowerCase()
|
||||
.replace(/[@\/]/g, '-')
|
||||
.replace(/[^a-z0-9._-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
function addDocker(tree: Tree, options: SetUpDockerOptions) {
|
||||
const projectConfig = readProjectConfiguration(tree, options.project);
|
||||
|
||||
@ -40,22 +49,30 @@ function addDocker(tree: Tree, options: SetUpDockerOptions) {
|
||||
`The output path for the project ${options.project} is not defined. Please provide it as an option to the generator.`
|
||||
);
|
||||
}
|
||||
|
||||
const sanitizedProjectName = sanitizeProjectName(options.project);
|
||||
|
||||
generateFiles(tree, join(__dirname, './files'), projectConfig.root, {
|
||||
tmpl: '',
|
||||
buildLocation: options.outputPath ?? outputPath,
|
||||
project: options.project,
|
||||
projectPath: projectConfig.root,
|
||||
sanitizedProjectName,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateProjectConfig(tree: Tree, options: SetUpDockerOptions) {
|
||||
let projectConfig = readProjectConfiguration(tree, options.project);
|
||||
|
||||
// Use sanitized project name for Docker image tag
|
||||
const sanitizedProjectName = sanitizeProjectName(options.project);
|
||||
|
||||
projectConfig.targets[`${options.targetName}`] = {
|
||||
dependsOn: [`${options.buildTarget}`],
|
||||
command: `docker build -f ${joinPathFragments(
|
||||
projectConfig.root,
|
||||
'Dockerfile'
|
||||
)} . -t ${options.project}`,
|
||||
)} . -t ${sanitizedProjectName}`,
|
||||
};
|
||||
|
||||
updateProjectConfiguration(tree, options.project, projectConfig);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user