From 63b6cf66ac7209d20c968a4d34fb030b9a1a2d5f Mon Sep 17 00:00:00 2001
From: Nils Plaschke <Chumper@users.noreply.github.com>
Date: Thu, 8 Apr 2021 12:10:25 +0200
Subject: [PATCH] feat: introduce dockerChildPrefix option (#8613)

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 docs/usage/self-hosted-configuration.md |  8 ++++++
 lib/config/admin.ts                     |  1 +
 lib/config/definitions.ts               |  8 ++++++
 lib/config/types.ts                     |  1 +
 lib/util/exec/docker/index.ts           | 34 +++++++++++++++++--------
 lib/util/exec/exec.spec.ts              | 30 ++++++++++++++++++++++
 lib/util/exec/index.ts                  | 21 ++++++++-------
 7 files changed, 83 insertions(+), 20 deletions(-)

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 138be9db3c..ee1de0277f 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -132,6 +132,14 @@ Set to `false` to prevent usage of `--ignore-platform-reqs` in the Composer pack
 
 This configuration will be applied after all other environment variables so that it can be used to override defaults.
 
+## dockerChildPrefix
+
+Adds a custom prefix to the default Renovate sidecar Docker containers name and label.
+
+If this is set to `myprefix_` the final image name for `renovate/node` would be named `myprefix_node` instead of currently used `renovate_node` and be labeled `myprefix_child` instead of `renovate_child`.
+
+Note that dangling containers will not be removed until Renovate is run with the same prefix again.
+
 ## dockerImagePrefix
 
 By default Renovate pulls the sidecar Docker containers from `docker.io/renovate`.
diff --git a/lib/config/admin.ts b/lib/config/admin.ts
index f6af54b53f..6d6fd61fc6 100644
--- a/lib/config/admin.ts
+++ b/lib/config/admin.ts
@@ -7,6 +7,7 @@ export const repoAdminOptions = [
   'allowPostUpgradeCommandTemplating',
   'allowedPostUpgradeCommands',
   'customEnvVariables',
+  'dockerChildPrefix',
   'dockerImagePrefix',
   'dockerUser',
   'dryRun',
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index ecf25ed672..9cffe44733 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -251,6 +251,14 @@ const options: RenovateOptions[] = [
     type: 'boolean',
     default: false,
   },
+  {
+    name: 'dockerChildPrefix',
+    description:
+      'Change this value in order to add a prefix to the Renovate Docker sidecar image names and labels.',
+    type: 'string',
+    admin: true,
+    default: 'renovate_',
+  },
   {
     name: 'dockerImagePrefix',
     description:
diff --git a/lib/config/types.ts b/lib/config/types.ts
index f98003293e..5a1e066b81 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -87,6 +87,7 @@ export interface RepoAdminConfig {
   allowPostUpgradeCommandTemplating?: boolean;
   allowedPostUpgradeCommands?: string[];
   customEnvVariables?: Record<string, string>;
+  dockerChildPrefix?: string;
   dockerImagePrefix?: string;
   dockerUser?: string;
   dryRun?: boolean;
diff --git a/lib/util/exec/docker/index.ts b/lib/util/exec/docker/index.ts
index b8707f7e72..bf0a39c2fa 100644
--- a/lib/util/exec/docker/index.ts
+++ b/lib/util/exec/docker/index.ts
@@ -115,12 +115,19 @@ async function getDockerTag(
   return 'latest';
 }
 
-function getContainerName(image: string): string {
-  return `renovate_${image}`.replace(/\//g, '_');
+function getContainerName(image: string, prefix?: string): string {
+  return `${prefix || 'renovate_'}${image}`.replace(/\//g, '_');
 }
 
-export async function removeDockerContainer(image: string): Promise<void> {
-  const containerName = getContainerName(image);
+function getContainerLabel(prefix: string): string {
+  return `${prefix || 'renovate_'}child`;
+}
+
+export async function removeDockerContainer(
+  image: string,
+  prefix: string
+): Promise<void> {
+  const containerName = getContainerName(image, prefix);
   let cmd = `docker ps --filter name=${containerName} -aq`;
   try {
     const res = await rawExec(cmd, {
@@ -146,11 +153,15 @@ export async function removeDockerContainer(image: string): Promise<void> {
 }
 
 // istanbul ignore next
-export async function removeDanglingContainers(): Promise<void> {
+export async function removeDanglingContainers(prefix: string): Promise<void> {
   try {
-    const res = await rawExec(`docker ps --filter label=renovate_child -aq`, {
-      encoding: 'utf-8',
-    });
+    const containerLabel = getContainerLabel(prefix);
+    const res = await rawExec(
+      `docker ps --filter label=${containerLabel} -aq`,
+      {
+        encoding: 'utf-8',
+      }
+    );
     if (res?.stdout?.trim().length) {
       const containerIds = res.stdout
         .trim()
@@ -187,11 +198,12 @@ export async function generateDockerCommand(
   const preCommands = options.preCommands || [];
   const postCommands = options.postCommands || [];
   const { localDir, cacheDir } = config;
-  const { dockerUser, dockerImagePrefix } = getAdminConfig();
+  const { dockerUser, dockerChildPrefix, dockerImagePrefix } = getAdminConfig();
   const result = ['docker run --rm'];
-  const containerName = getContainerName(image);
+  const containerName = getContainerName(image, dockerChildPrefix);
+  const containerLabel = getContainerLabel(dockerChildPrefix);
   result.push(`--name=${containerName}`);
-  result.push(`--label=renovate_child`);
+  result.push(`--label=${containerLabel}`);
   if (dockerUser) {
     result.push(`--user=${dockerUser}`);
   }
diff --git a/lib/util/exec/exec.spec.ts b/lib/util/exec/exec.spec.ts
index 3fb345ca0d..f5a67fbda5 100644
--- a/lib/util/exec/exec.spec.ts
+++ b/lib/util/exec/exec.spec.ts
@@ -448,6 +448,36 @@ describe(getName(__filename), () => {
       },
     ],
 
+    [
+      'Docker child prefix',
+      {
+        execConfig: {
+          ...execConfig,
+          binarySource: BinarySource.Docker,
+        },
+        processEnv,
+        inCmd,
+        inOpts: { docker },
+        outCmd: [
+          dockerPullCmd,
+          `docker ps --filter name=myprefix_${image} -aq`,
+          `docker run --rm --name=myprefix_${image} --label=myprefix_child ${defaultVolumes} -w "${cwd}" ${fullImage} bash -l -c "${inCmd}"`,
+        ],
+        outOpts: [
+          dockerPullOpts,
+          dockerRemoveOpts,
+          {
+            cwd,
+            encoding,
+            env: envMock.basic,
+            timeout: 900000,
+            maxBuffer: 10485760,
+          },
+        ],
+        adminConfig: { dockerChildPrefix: 'myprefix_' },
+      },
+    ],
+
     [
       'Docker extra commands',
       {
diff --git a/lib/util/exec/index.ts b/lib/util/exec/index.ts
index f4cfa79596..c2205e02c4 100644
--- a/lib/util/exec/index.ts
+++ b/lib/util/exec/index.ts
@@ -34,7 +34,7 @@ export async function setExecConfig(
     execConfig[key] = value || null;
   }
   if (execConfig.binarySource === 'docker') {
-    await removeDanglingContainers();
+    await removeDanglingContainers(getAdminConfig().dockerChildPrefix);
   }
 }
 
@@ -95,7 +95,8 @@ export async function exec(
   opts: ExecOptions = {}
 ): Promise<ExecResult> {
   const { env, docker, cwdFile } = opts;
-  const extraEnv = { ...opts.extraEnv, ...getAdminConfig().customEnvVariables };
+  const { dockerChildPrefix, customEnvVariables } = getAdminConfig();
+  const extraEnv = { ...opts.extraEnv, ...customEnvVariables };
   let cwd;
   // istanbul ignore if
   if (cwdFile) {
@@ -142,7 +143,7 @@ export async function exec(
   for (const rawExecCommand of commands) {
     const startTime = Date.now();
     if (useDocker) {
-      await removeDockerContainer(docker.image);
+      await removeDockerContainer(docker.image, dockerChildPrefix);
     }
     logger.debug({ command: rawExecCommand }, 'Executing command');
     logger.trace({ commandOptions: rawExecOptions }, 'Command options');
@@ -151,12 +152,14 @@ export async function exec(
     } catch (err) {
       logger.trace({ err }, 'rawExec err');
       if (useDocker) {
-        await removeDockerContainer(docker.image).catch((removeErr: Error) => {
-          const message: string = err.message;
-          throw new Error(
-            `Error: "${removeErr.message}" - Original Error: "${message}"`
-          );
-        });
+        await removeDockerContainer(docker.image, dockerChildPrefix).catch(
+          (removeErr: Error) => {
+            const message: string = err.message;
+            throw new Error(
+              `Error: "${removeErr.message}" - Original Error: "${message}"`
+            );
+          }
+        );
       }
       if (err.signal === `SIGTERM`) {
         logger.debug(
-- 
GitLab