From 16337594973b9375b179239de4a031db1140a35d Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Wed, 23 Nov 2022 10:35:38 +0100
Subject: [PATCH] feat(manager/nuget): support install mode (#19049)

---
 docs/usage/self-hosted-configuration.md       |   1 +
 .../__snapshots__/artifacts.spec.ts.snap      | 221 -----------------
 lib/modules/manager/nuget/artifacts.spec.ts   | 232 ++++++++++++++++--
 lib/modules/manager/nuget/artifacts.ts        |   5 +-
 lib/util/exec/containerbase.ts                |   5 +
 5 files changed, 224 insertions(+), 240 deletions(-)
 delete mode 100644 lib/modules/manager/nuget/__snapshots__/artifacts.spec.ts.snap

diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md
index 43acd75998..df9bf7ba4c 100644
--- a/docs/usage/self-hosted-configuration.md
+++ b/docs/usage/self-hosted-configuration.md
@@ -159,6 +159,7 @@ Supported tools for dynamic install are:
 - `bundler`
 - `cargo`
 - `composer`
+- `dotnet`
 - `flux`
 - `gradle-wrapper`
 - `jb`
diff --git a/lib/modules/manager/nuget/__snapshots__/artifacts.spec.ts.snap b/lib/modules/manager/nuget/__snapshots__/artifacts.spec.ts.snap
deleted file mode 100644
index 9a15a57672..0000000000
--- a/lib/modules/manager/nuget/__snapshots__/artifacts.spec.ts.snap
+++ /dev/null
@@ -1,221 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`modules/manager/nuget/artifacts aborts if lock file is unchanged 1`] = `
-[
-  {
-    "cmd": "dotnet restore 'path/with space/project.csproj' --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-]
-`;
-
-exports[`modules/manager/nuget/artifacts authenticates at registries 1`] = `
-[
-  {
-    "cmd": "dotnet nuget add source https://my-registry.example.org/ --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config --name myRegistry --username some-username --password some-password --store-password-in-clear-text",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-  {
-    "cmd": "dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-]
-`;
-
-exports[`modules/manager/nuget/artifacts performs lock file maintenance 1`] = `
-[
-  {
-    "cmd": "dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-]
-`;
-
-exports[`modules/manager/nuget/artifacts strips protocol version from feed url 1`] = `
-[
-  {
-    "cmd": "dotnet nuget add source https://my-registry.example.org/ --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config --name myRegistry",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-  {
-    "cmd": "dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-]
-`;
-
-exports[`modules/manager/nuget/artifacts supports docker mode 1`] = `
-[
-  {
-    "cmd": "docker pull renovate/dotnet",
-    "options": {
-      "encoding": "utf-8",
-    },
-  },
-  {
-    "cmd": "docker ps --filter name=renovate_dotnet -aq",
-    "options": {
-      "encoding": "utf-8",
-    },
-  },
-  {
-    "cmd": "docker run --rm --name=renovate_dotnet --label=renovate_child -v "/tmp/github/some/repo":"/tmp/github/some/repo" -v "/tmp/renovate/cache":"/tmp/renovate/cache" -e NUGET_PACKAGES -e BUILDPACK_CACHE_DIR -e CONTAINERBASE_CACHE_DIR -w "/tmp/github/some/repo" renovate/dotnet bash -l -c "dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config"",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "BUILDPACK_CACHE_DIR": "/tmp/renovate/cache/containerbase",
-        "CONTAINERBASE_CACHE_DIR": "/tmp/renovate/cache/containerbase",
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-]
-`;
-
-exports[`modules/manager/nuget/artifacts supports global mode 1`] = `
-[
-  {
-    "cmd": "dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-]
-`;
-
-exports[`modules/manager/nuget/artifacts updates lock file 1`] = `
-[
-  {
-    "cmd": "dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config",
-    "options": {
-      "cwd": "/tmp/github/some/repo",
-      "encoding": "utf-8",
-      "env": {
-        "HOME": "/home/user",
-        "HTTPS_PROXY": "https://example.com",
-        "HTTP_PROXY": "http://example.com",
-        "LANG": "en_US.UTF-8",
-        "LC_ALL": "en_US",
-        "NO_PROXY": "localhost",
-        "NUGET_PACKAGES": "/tmp/renovate/cache/__renovate-private-cache/nuget/packages",
-        "PATH": "/tmp/path",
-      },
-      "maxBuffer": 10485760,
-      "timeout": 900000,
-    },
-  },
-]
-`;
diff --git a/lib/modules/manager/nuget/artifacts.spec.ts b/lib/modules/manager/nuget/artifacts.spec.ts
index b4f844ef4c..639b3572e0 100644
--- a/lib/modules/manager/nuget/artifacts.spec.ts
+++ b/lib/modules/manager/nuget/artifacts.spec.ts
@@ -18,10 +18,10 @@ jest.mock('./util');
 const { getConfiguredRegistries, getDefaultRegistries } = mocked(util);
 const hostRules = mocked(_hostRules);
 
-// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
-const realFs = jest.requireActual(
-  '../../../util/fs'
-) as typeof import('../../../util/fs');
+const realFs =
+  jest.requireActual<typeof import('../../../util/fs')>('../../../util/fs');
+
+process.env.CONTAINERBASE = 'true';
 
 const adminConfig: RepoGlobalConfig = {
   // `join` fixes Windows CI
@@ -83,7 +83,18 @@ describe('modules/manager/nuget/artifacts', () => {
         config,
       })
     ).toBeNull();
-    expect(execSnapshots).toMatchSnapshot();
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: "dotnet restore 'path/with space/project.csproj' --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config",
+        options: {
+          cwd: '/tmp/github/some/repo',
+          env: {
+            NUGET_PACKAGES:
+              '/tmp/renovate/cache/__renovate-private-cache/nuget/packages',
+          },
+        },
+      },
+    ]);
   });
 
   it('updates lock file', async () => {
@@ -103,8 +114,27 @@ describe('modules/manager/nuget/artifacts', () => {
         newPackageFileContent: '{}',
         config,
       })
-    ).not.toBeNull();
-    expect(execSnapshots).toMatchSnapshot();
+    ).toEqual([
+      {
+        file: {
+          contents: 'New packages.lock.json',
+          path: 'packages.lock.json',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config',
+        options: {
+          cwd: '/tmp/github/some/repo',
+          env: {
+            NUGET_PACKAGES:
+              '/tmp/renovate/cache/__renovate-private-cache/nuget/packages',
+          },
+        },
+      },
+    ]);
   });
 
   it('does not update lock file when non-proj file is changed', async () => {
@@ -169,8 +199,27 @@ describe('modules/manager/nuget/artifacts', () => {
           isLockFileMaintenance: true,
         },
       })
-    ).not.toBeNull();
-    expect(execSnapshots).toMatchSnapshot();
+    ).toEqual([
+      {
+        file: {
+          contents: 'New packages.lock.json',
+          path: 'packages.lock.json',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config',
+        options: {
+          cwd: '/tmp/github/some/repo',
+          env: {
+            NUGET_PACKAGES:
+              '/tmp/renovate/cache/__renovate-private-cache/nuget/packages',
+          },
+        },
+      },
+    ]);
   });
 
   it('supports docker mode', async () => {
@@ -189,10 +238,104 @@ describe('modules/manager/nuget/artifacts', () => {
         packageFileName: 'project.csproj',
         updatedDeps: [{ depName: 'dep' }],
         newPackageFileContent: '{}',
-        config,
+        config: { ...config, constraints: { dotnet: '7.0.100' } },
       })
-    ).not.toBeNull();
-    expect(execSnapshots).toMatchSnapshot();
+    ).toEqual([
+      {
+        file: {
+          contents: 'New packages.lock.json',
+          path: 'packages.lock.json',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'docker pull renovate/sidecar',
+      },
+      {
+        cmd: 'docker ps --filter name=renovate_sidecar -aq',
+      },
+      {
+        cmd:
+          'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
+          '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
+          '-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' +
+          '-e NUGET_PACKAGES ' +
+          '-e BUILDPACK_CACHE_DIR ' +
+          '-e CONTAINERBASE_CACHE_DIR ' +
+          '-w "/tmp/github/some/repo" ' +
+          'renovate/sidecar ' +
+          'bash -l -c "' +
+          'install-tool dotnet 7.0.100' +
+          ' && ' +
+          'dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config' +
+          '"',
+        options: {
+          env: {
+            BUILDPACK_CACHE_DIR: '/tmp/renovate/cache/containerbase',
+            CONTAINERBASE_CACHE_DIR: '/tmp/renovate/cache/containerbase',
+            NUGET_PACKAGES:
+              '/tmp/renovate/cache/__renovate-private-cache/nuget/packages',
+          },
+        },
+      },
+    ]);
+  });
+
+  it('supports install mode', async () => {
+    GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
+    const execSnapshots = mockExecAll();
+    fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
+    fs.getFileContentMap
+      .mockResolvedValueOnce({
+        'packages.lock.json': 'Current packages.lock.json',
+      })
+      .mockResolvedValueOnce({
+        'packages.lock.json': 'New packages.lock.json',
+      });
+    expect(
+      await nuget.updateArtifacts({
+        packageFileName: 'project.csproj',
+        updatedDeps: [{ depName: 'dep' }],
+        newPackageFileContent: '{}',
+        config: { ...config, constraints: { dotnet: '7.0.100' } },
+      })
+    ).toEqual([
+      {
+        file: {
+          contents: 'New packages.lock.json',
+          path: 'packages.lock.json',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'install-tool dotnet 7.0.100',
+        options: {
+          cwd: '/tmp/github/some/repo',
+          env: {
+            BUILDPACK_CACHE_DIR: '/tmp/renovate/cache/containerbase',
+            CONTAINERBASE_CACHE_DIR: '/tmp/renovate/cache/containerbase',
+            NUGET_PACKAGES:
+              '/tmp/renovate/cache/__renovate-private-cache/nuget/packages',
+          },
+        },
+      },
+      {
+        cmd: 'dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config',
+        options: {
+          cwd: '/tmp/github/some/repo',
+          env: {
+            BUILDPACK_CACHE_DIR: '/tmp/renovate/cache/containerbase',
+            CONTAINERBASE_CACHE_DIR: '/tmp/renovate/cache/containerbase',
+            NUGET_PACKAGES:
+              '/tmp/renovate/cache/__renovate-private-cache/nuget/packages',
+          },
+        },
+      },
+    ]);
   });
 
   it('supports global mode', async () => {
@@ -213,11 +356,31 @@ describe('modules/manager/nuget/artifacts', () => {
         newPackageFileContent: '{}',
         config,
       })
-    ).not.toBeNull();
-    expect(execSnapshots).toMatchSnapshot();
+    ).toEqual([
+      {
+        file: {
+          contents: 'New packages.lock.json',
+          path: 'packages.lock.json',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config',
+        options: {
+          cwd: '/tmp/github/some/repo',
+          env: {
+            NUGET_PACKAGES:
+              '/tmp/renovate/cache/__renovate-private-cache/nuget/packages',
+          },
+        },
+      },
+    ]);
   });
 
   it('catches errors', async () => {
+    const execSnapshots = mockExecAll();
     fs.getSiblingFileName.mockReturnValueOnce('packages.lock.json');
     fs.getFileContentMap.mockResolvedValueOnce({
       'packages.lock.json': 'Current packages.lock.json',
@@ -240,6 +403,7 @@ describe('modules/manager/nuget/artifacts', () => {
         },
       },
     ]);
+    expect(execSnapshots).toBeEmptyArray();
   });
 
   it('authenticates at registries', async () => {
@@ -277,8 +441,25 @@ describe('modules/manager/nuget/artifacts', () => {
         newPackageFileContent: '{}',
         config,
       })
-    ).not.toBeNull();
-    expect(execSnapshots).toMatchSnapshot();
+    ).toEqual([
+      {
+        file: {
+          contents: 'New packages.lock.json',
+          path: 'packages.lock.json',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd:
+          'dotnet nuget add source https://my-registry.example.org/ --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config ' +
+          '--name myRegistry --username some-username --password some-password --store-password-in-clear-text',
+      },
+      {
+        cmd: 'dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config',
+      },
+    ]);
   });
 
   it('strips protocol version from feed url', async () => {
@@ -305,7 +486,22 @@ describe('modules/manager/nuget/artifacts', () => {
         newPackageFileContent: '{}',
         config,
       })
-    ).not.toBeNull();
-    expect(execSnapshots).toMatchSnapshot();
+    ).toEqual([
+      {
+        file: {
+          contents: 'New packages.lock.json',
+          path: 'packages.lock.json',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'dotnet nuget add source https://my-registry.example.org/ --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config --name myRegistry',
+      },
+      {
+        cmd: 'dotnet restore project.csproj --force-evaluate --configfile /tmp/renovate/cache/__renovate-private-cache/nuget/nuget.config',
+      },
+    ]);
   });
 });
diff --git a/lib/modules/manager/nuget/artifacts.ts b/lib/modules/manager/nuget/artifacts.ts
index a88dfdb95b..af5f31c718 100644
--- a/lib/modules/manager/nuget/artifacts.ts
+++ b/lib/modules/manager/nuget/artifacts.ts
@@ -69,9 +69,12 @@ async function runDotnetRestore(
 
   const execOptions: ExecOptions = {
     docker: {
-      image: 'dotnet',
+      image: 'sidecar',
     },
     extraEnv: { NUGET_PACKAGES: join(nugetCacheDir, 'packages') },
+    toolConstraints: [
+      { toolName: 'dotnet', constraint: config.constraints?.dotnet },
+    ],
   };
 
   const nugetConfigFile = join(nugetCacheDir, `nuget.config`);
diff --git a/lib/util/exec/containerbase.ts b/lib/util/exec/containerbase.ts
index 4c9ef8d3e2..8c37a6f591 100644
--- a/lib/util/exec/containerbase.ts
+++ b/lib/util/exec/containerbase.ts
@@ -35,6 +35,11 @@ const allToolConfig: Record<string, ToolConfig> = {
     depName: 'corepack',
     versioning: npmVersioningId,
   },
+  dotnet: {
+    datasource: 'dotnet',
+    depName: 'dotnet-sdk',
+    versioning: semverVersioningId,
+  },
   erlang: {
     datasource: 'github-releases',
     depName: 'containerbase/erlang-prebuild',
-- 
GitLab