diff --git a/.changeset/techdocs-metal-turkeys-sleep.md b/.changeset/techdocs-metal-turkeys-sleep.md new file mode 100644 index 0000000000..79cf11ce53 --- /dev/null +++ b/.changeset/techdocs-metal-turkeys-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Pass user and group ID when invoking docker container. When TechDocs invokes Docker, docker could be run as a `root` user which results in generation of files by applications run by non-root user (e.g. TechDocs) will not have access to modify. This PR passes in current user and group ID to docker so that the file permissions of the generated files and folders are correct. diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index c4bf102610..fbca6d1ed2 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -30,6 +30,7 @@ import { patchMkdocsYmlPreBuild, runDockerContainer, storeEtagMetadata, + UserOptions, } from './helpers'; const mockEntity = { @@ -114,7 +115,7 @@ describe('helpers', () => { imageName, args, expect.any(Stream), - { + expect.objectContaining({ Volumes: { '/content': {}, '/result': {}, @@ -123,7 +124,7 @@ describe('helpers', () => { HostConfig: { Binds: [`${docsDir}:/content`, `${outputDir}:/result`], }, - }, + }), ); }); @@ -139,6 +140,30 @@ describe('helpers', () => { expect(mockDocker.ping).toHaveBeenCalled(); }); + it('should pass through the user and group id from the host machine and set the home dir', async () => { + await runDockerContainer({ + imageName, + args, + docsDir, + outputDir, + dockerClient: mockDocker, + }); + + const userOptions: UserOptions = {}; + if (process.getuid && process.getgid) { + userOptions.User = `${process.getuid()}:${process.getgid()}`; + } + + expect(mockDocker.run).toHaveBeenCalledWith( + imageName, + args, + expect.any(Stream), + expect.objectContaining({ + ...userOptions, + }), + ); + }); + describe('where docker is unavailable', () => { const dockerError = 'a docker error'; diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 32da5d19e8..a88ed13231 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -51,6 +51,12 @@ export type RunCommandOptions = { logStream?: Writable; }; +export type UserOptions = { + User?: string; +}; + +// To be replaced by a runDockerContainer from backend-common +// shared between Scaffolder and TechDocs and any other plugin. export async function runDockerContainer({ imageName, args, @@ -78,6 +84,17 @@ export async function runDockerContainer({ }); }); + const userOptions: UserOptions = {}; + // @ts-ignore + if (process.getuid && process.getgid) { + // Files that are created inside the Docker container will be owned by + // root on the host system on non Mac systems, because of reasons. Mainly the fact that + // volume sharing is done using NFS on Mac and actual mounts in Linux world. + // So we set the user in the container as the same user and group id as the host. + // On Windows we don't have process.getuid nor process.getgid + userOptions.User = `${process.getuid()}:${process.getgid()}`; + } + const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run( imageName, args, @@ -91,6 +108,7 @@ export async function runDockerContainer({ HostConfig: { Binds: [`${docsDir}:/content`, `${outputDir}:/result`], }, + ...userOptions, ...createOptions, }, );