From a23f07d2dec8ee1e16ada349985a73993dec12c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Fern=C3=A1ndez?= <fmartin91@gmail.com> Date: Sun, 29 Mar 2020 04:20:38 -0300 Subject: [PATCH] feat(bundler): authentication support using hostRules (#5269) --- .../__snapshots__/artifacts.spec.ts.snap | 46 +++++ lib/manager/bundler/artifacts.spec.ts | 53 +++++- lib/manager/bundler/artifacts.ts | 29 ++++ lib/manager/bundler/host-rules.spec.ts | 159 ++++++++++++++++++ lib/manager/bundler/host-rules.ts | 40 +++++ 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 lib/manager/bundler/host-rules.spec.ts create mode 100644 lib/manager/bundler/host-rules.ts diff --git a/lib/manager/bundler/__snapshots__/artifacts.spec.ts.snap b/lib/manager/bundler/__snapshots__/artifacts.spec.ts.snap index f1cae80b85..ce832abf7f 100644 --- a/lib/manager/bundler/__snapshots__/artifacts.spec.ts.snap +++ b/lib/manager/bundler/__snapshots__/artifacts.spec.ts.snap @@ -90,6 +90,52 @@ Array [ ] `; +exports[`bundler.updateArtifacts() Docker injects bundler host configuration environment variables 1`] = ` +Array [ + Object { + "file": Object { + "contents": "Updated Gemfile.lock", + "name": "Gemfile.lock", + }, + }, +] +`; + +exports[`bundler.updateArtifacts() Docker injects bundler host configuration environment variables 2`] = ` +Array [ + Object { + "cmd": "docker pull renovate/ruby:1.2.0", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker ps --filter name=renovate_ruby -aq | xargs --no-run-if-empty docker rm -f", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker run --rm --name=renovate_ruby --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -e BUNDLE_GEMS__PRIVATE__COM -w \\"/tmp/github/some/repo\\" renovate/ruby:1.2.0 bash -l -c \\"ruby --version && gem install bundler --no-document && bundle lock --update foo bar\\"", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "BUNDLE_GEMS__PRIVATE__COM": "some-user:some-password", + "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", + "PATH": "/tmp/path", + }, + "timeout": 900000, + }, + }, +] +`; + exports[`bundler.updateArtifacts() Docker invalid compatibility options 1`] = ` Array [ Object { diff --git a/lib/manager/bundler/artifacts.spec.ts b/lib/manager/bundler/artifacts.spec.ts index 0f09448895..22cb8445f6 100644 --- a/lib/manager/bundler/artifacts.spec.ts +++ b/lib/manager/bundler/artifacts.spec.ts @@ -8,6 +8,7 @@ import * as _datasource from '../../datasource/docker'; import { mocked } from '../../../test/util'; import { envMock, mockExecAll } from '../../../test/execUtil'; import * as _env from '../../util/exec/env'; +import * as _bundlerHostRules from './host-rules'; import { BinarySource } from '../../util/exec/common'; import { setUtilConfig } from '../../util'; import { resetPrefetchedImages } from '../../util/exec/docker'; @@ -17,12 +18,15 @@ const exec: jest.Mock<typeof _exec> = _exec as any; const env = mocked(_env); const platform = mocked(_platform); const datasource = mocked(_datasource); +const bundlerHostRules = mocked(_bundlerHostRules); jest.mock('fs-extra'); jest.mock('child_process'); -jest.mock('../../util/exec/env'); -jest.mock('../../platform'); -jest.mock('../../datasource/docker'); +jest.mock('../../../lib/util/exec/env'); +jest.mock('../../../lib/platform'); +jest.mock('../../../lib/datasource/docker'); +jest.mock('../../../lib/util/host-rules'); +jest.mock('./host-rules'); let config; @@ -38,6 +42,7 @@ describe('bundler.updateArtifacts()', () => { }; env.getChildProcessEnv.mockReturnValue(envMock.basic); + bundlerHostRules.findAllAuthenticatable.mockReturnValue([]); resetPrefetchedImages(); setUtilConfig(config); }); @@ -207,5 +212,47 @@ describe('bundler.updateArtifacts()', () => { ).toMatchSnapshot(); expect(execSnapshots).toMatchSnapshot(); }); + + it('injects bundler host configuration environment variables', async () => { + platform.getFile.mockResolvedValueOnce('Current Gemfile.lock'); + fs.outputFile.mockResolvedValueOnce(null as never); + platform.getFile.mockResolvedValueOnce('1.2.0'); + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [ + { version: '1.0.0' }, + { version: '1.2.0' }, + { version: '1.3.0' }, + ], + }); + bundlerHostRules.findAllAuthenticatable.mockReturnValue([ + { + hostType: 'bundler', + hostName: 'gems.private.com', + username: 'some-user', + password: 'some-password', + }, + ]); + bundlerHostRules.getDomain.mockReturnValue('gems.private.com'); + bundlerHostRules.getAuthenticationHeaderValue.mockReturnValue( + 'some-user:some-password' + ); + const execSnapshots = mockExecAll(exec); + platform.getRepoStatus.mockResolvedValueOnce({ + modified: ['Gemfile.lock'], + } as Git.StatusResult); + fs.readFile.mockResolvedValueOnce('Updated Gemfile.lock' as any); + expect( + await updateArtifacts({ + packageFileName: 'Gemfile', + updatedDeps: ['foo', 'bar'], + newPackageFileContent: 'Updated Gemfile content', + config: { + ...config, + binarySource: BinarySource.Docker, + }, + }) + ).toMatchSnapshot(); + expect(execSnapshots).toMatchSnapshot(); + }); }); }); diff --git a/lib/manager/bundler/artifacts.ts b/lib/manager/bundler/artifacts.ts index 7dd603c81f..16b5b19c7c 100644 --- a/lib/manager/bundler/artifacts.ts +++ b/lib/manager/bundler/artifacts.ts @@ -12,6 +12,14 @@ import { BUNDLER_INVALID_CREDENTIALS, BUNDLER_UNKNOWN_ERROR, } from '../../constants/error-messages'; +import { HostRule } from '../../types'; +import { + getAuthenticationHeaderValue, + findAllAuthenticatable, + getDomain, +} from './host-rules'; + +const hostConfigVariablePrefix = 'BUNDLE_'; async function getRubyConstraint( updateArtifact: UpdateArtifact @@ -41,6 +49,19 @@ async function getRubyConstraint( return rubyConstraint; } +function buildBundleHostVariable(hostRule: HostRule): Record<string, string> { + const varName = + hostConfigVariablePrefix + + getDomain(hostRule) + .split('.') + .map(term => term.toUpperCase()) + .join('__'); + + return { + [varName]: `${getAuthenticationHeaderValue(hostRule)}`, + }; +} + export async function updateArtifacts( updateArtifact: UpdateArtifact ): Promise<UpdateArtifactsResult[] | null> { @@ -85,8 +106,16 @@ export async function updateArtifacts( 'ruby --version', `gem install bundler${bundlerVersion} --no-document`, ]; + + const bundlerHostRulesVariables = findAllAuthenticatable({ + hostType: 'bundler', + }).reduce((variables, hostRule) => { + return { ...variables, ...buildBundleHostVariable(hostRule) }; + }, {} as Record<string, string>); + const execOptions: ExecOptions = { cwdFile: packageFileName, + extraEnv: bundlerHostRulesVariables, docker: { image: 'renovate/ruby', tagScheme: 'ruby', diff --git a/lib/manager/bundler/host-rules.spec.ts b/lib/manager/bundler/host-rules.spec.ts new file mode 100644 index 0000000000..4f2e20d730 --- /dev/null +++ b/lib/manager/bundler/host-rules.spec.ts @@ -0,0 +1,159 @@ +import { add, clear } from '../../util/host-rules'; +import { HostRule } from '../../types'; + +import { + findAllAuthenticatable, + getDomain, + getAuthenticationHeaderValue, +} from './host-rules'; + +describe('lib/manager/bundler/host-rules', () => { + beforeEach(() => { + clear(); + }); + describe('getDomain()', () => { + it('returns the hostName if hostName is present', () => { + expect( + getDomain({ + hostName: 'api.github.com', + }) + ).toEqual('api.github.com'); + }); + it('returns the domainName if domainName is present and hostName is not present', () => { + expect( + getDomain({ + domainName: 'github.com', + }) + ).toEqual('github.com'); + }); + it('returns the hostName if hostName and domainName are present', () => { + expect( + getDomain({ + hostName: 'api.github.com', + domainName: 'github.com', + }) + ).toEqual('api.github.com'); + }); + it('returns the baseUrl host if hostName and domainName are not present', () => { + expect( + getDomain({ + baseUrl: 'https://github.com', + }) + ).toEqual('github.com'); + }); + it('returns undefined if hostName, domainName and baseUrl are not present', () => { + expect(getDomain({})).toBeNull(); + }); + }); + describe('getAuthenticationHeaderValue()', () => { + it('returns the authentication header with the password', () => { + expect( + getAuthenticationHeaderValue({ + username: 'test', + password: 'password', + }) + ).toEqual('test:password'); + }); + it('returns the authentication header with the token', () => { + expect( + getAuthenticationHeaderValue({ + token: 'token', + }) + ).toEqual('token'); + }); + }); + describe('findAllAuthenticatable()', () => { + let hostRule: HostRule; + + beforeEach(() => { + hostRule = { + hostType: 'nuget', + hostName: 'nuget.org', + domainName: 'api.nuget.org', + username: 'root', + password: 'p4$$w0rd', + token: 'token', + }; + }); + it('returns an empty array if domainName, hostName and baseUrl are missing', () => { + delete hostRule.hostName; + delete hostRule.domainName; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([]); + }); + it('returns an empty array if username is missing and password is present', () => { + delete hostRule.domainName; + delete hostRule.username; + delete hostRule.password; + delete hostRule.token; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([]); + }); + it('returns an empty array if password and token are missing', () => { + delete hostRule.domainName; + delete hostRule.password; + delete hostRule.token; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([]); + }); + it('returns the hostRule if using hostName and password', () => { + delete hostRule.domainName; + delete hostRule.token; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([ + hostRule, + ]); + }); + it('returns the hostRule if using domainName and password', () => { + delete hostRule.hostName; + delete hostRule.token; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([ + hostRule, + ]); + }); + it('returns the hostRule if using hostName and token', () => { + delete hostRule.domainName; + delete hostRule.password; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([ + hostRule, + ]); + }); + it('returns the hostRule if using domainName and token', () => { + delete hostRule.hostName; + delete hostRule.password; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([ + hostRule, + ]); + }); + it('returns the hostRule if using baseUrl and password', () => { + hostRule.baseUrl = 'https://nuget.com'; + delete hostRule.domainName; + delete hostRule.hostName; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([ + hostRule, + ]); + }); + it('returns the hostRule if using baseUrl and token', () => { + hostRule.baseUrl = 'https://nuget.com'; + delete hostRule.hostName; + delete hostRule.domainName; + + add(hostRule); + expect(findAllAuthenticatable({ hostType: 'nuget' } as any)).toEqual([ + hostRule, + ]); + }); + }); +}); diff --git a/lib/manager/bundler/host-rules.ts b/lib/manager/bundler/host-rules.ts new file mode 100644 index 0000000000..69b5f07cb0 --- /dev/null +++ b/lib/manager/bundler/host-rules.ts @@ -0,0 +1,40 @@ +import URL from 'url'; +import { HostRule } from '../../types'; +import { findAll } from '../../util/host-rules'; + +function isAuthenticatable(rule: HostRule): boolean { + return ( + (!!rule.hostName || !!rule.domainName || !!rule.baseUrl) && + ((!!rule.username && !!rule.password) || !!rule.token) + ); +} + +export function findAllAuthenticatable({ + hostType, +}: { + hostType: string; +}): HostRule[] { + return findAll({ hostType }).filter(isAuthenticatable); +} + +export function getDomain(hostRule: HostRule): string { + if (hostRule.hostName) { + return hostRule.hostName; + } + if (hostRule.domainName) { + return hostRule.domainName; + } + if (hostRule.baseUrl) { + return URL.parse(hostRule.baseUrl).host; + } + + return null; +} + +export function getAuthenticationHeaderValue(hostRule: HostRule): string { + if (hostRule.username) { + return `${hostRule.username}:${hostRule.password}`; + } + + return hostRule.token; +} -- GitLab