From d4a22c7f3c874a7d451022e4680b66abca196796 Mon Sep 17 00:00:00 2001 From: Niels Basjes Date: Wed, 16 Jun 2021 22:22:32 +0200 Subject: [PATCH] feat(docker): Support for Bearer token to access the Docker registry (#10400) --- docs/usage/docker.md | 47 ++++++++++++++++++++++++++++ lib/datasource/docker/common.spec.ts | 45 ++++++++++++++++++++++++++ lib/datasource/docker/common.ts | 10 +++++- 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/usage/docker.md b/docs/usage/docker.md index 38a6cec386e3c0..5146194127eac7 100644 --- a/docs/usage/docker.md +++ b/docs/usage/docker.md @@ -206,6 +206,53 @@ module.exports = { }; ``` +#### Google Container Registry + +Assume you are running GitLab CI in the Google Cloud, and you are storing your Docker images in the Google Container Registry (GCR). + +Access to the GCR uses Bearer token based authentication. +This token can be obtained by running `gcloud auth print-access-token`, which requires the Google Cloud SDK to be installed. + +The token expires after 60 minutes so you cannot store it in a variable for subsequent builds (like you can with `RENOVATE_TOKEN`). + +When running Renovate in this context the Google access token must be retrieved and injected into the `hostRules` configuration just before Renovate is started. + +_This documentation gives **a few hints** on **a possible way** to achieve this end result._ + +The basic approach is that you create a custom image and then run Renovate as one of the stages of your project. +To make this run independent of any user you should use a [`Project Access Token`](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html) (with Scopes: `api`, `read_api` and `write_repository`) for the project and use this as the `RENOVATE_TOKEN` variable for Gitlab CI. +See also the [renovate-runner repository on GitLab](https://gitlab.com/renovate-bot/renovate-runner) where `.gitlab-ci.yml` configuration examples can be found. + +To get access to the token a custom Renovate Docker image is needed that includes the Google Cloud SDK. +The Dockerfile to create such an image can look like this: + +```Dockerfile +FROM renovate/renovate:25.40.1 +# Include the "Docker tip" which you can find here https://cloud.google.com/sdk/docs/install +# under "Installation" for "Debian/Ubuntu" +RUN ... +``` + +For Renovate to access the Google Container Registry (GCR) it needs the current Google Access Token. +The configuration fragment to do that looks something like this: + +```js +hostRules: [ + { + matchHost: 'eu.gcr.io', + token: 'MyReallySecretTokenThatExpiresAfter60Minutes', + }, +]; +``` + +One way to provide the short-lived Google Access Token to Renovate is by generating these settings into a `config.js` file from within the `.gitlab-ci.yml` right before starting Renovate: + +```yaml +script: + - 'echo "module.exports = { hostRules: [ { matchHost: ''eu.gcr.io'', token: ''"$(gcloud auth print-access-token)"'' } ] };" > config.js' + - renovate $RENOVATE_EXTRA_FLAGS +``` + #### ChartMuseum Maybe you're running your own ChartMuseum server to host your private Helm Charts. diff --git a/lib/datasource/docker/common.spec.ts b/lib/datasource/docker/common.spec.ts index 7bc9b260cae97b..6c652b798a2846 100644 --- a/lib/datasource/docker/common.spec.ts +++ b/lib/datasource/docker/common.spec.ts @@ -1,3 +1,4 @@ +import * as httpMock from '../../../test/http-mock'; import { getName, mocked } from '../../../test/util'; import * as _hostRules from '../../util/host-rules'; import * as dockerCommon from './common'; @@ -70,4 +71,48 @@ describe(getName(), () => { `); }); }); + describe('getAuthHeaders', () => { + beforeEach(() => { + httpMock + .scope('https://my.local.registry') + .get('/v2/') + .reply(401, '', { 'www-authenticate': 'Authenticate you must' }); + hostRules.hosts.mockReturnValue([]); + }); + + it('returns "authType token" if both provided', async () => { + hostRules.find.mockReturnValue({ + authType: 'some-authType', + token: 'some-token', + }); + + const headers = await dockerCommon.getAuthHeaders( + 'https://my.local.registry', + 'https://my.local.registry/prefix' + ); + + expect(headers).toMatchInlineSnapshot(` + Object { + "authorization": "some-authType some-token", + } + `); + }); + + it('returns "Bearer token" if only token provided', async () => { + hostRules.find.mockReturnValue({ + token: 'some-token', + }); + + const headers = await dockerCommon.getAuthHeaders( + 'https://my.local.registry', + 'https://my.local.registry/prefix' + ); + + expect(headers).toMatchInlineSnapshot(` + Object { + "authorization": "Bearer some-token", + } + `); + }); + }); }); diff --git a/lib/datasource/docker/common.ts b/lib/datasource/docker/common.ts index 14d9d1468e95e7..b90cf21ea0892a 100644 --- a/lib/datasource/docker/common.ts +++ b/lib/datasource/docker/common.ts @@ -81,12 +81,20 @@ export async function getAuthHeaders( 'base64' ); opts.headers = { authorization: `Basic ${auth}` }; + } else if (opts.token) { + const authType = opts.authType ?? 'Bearer'; + logger.trace( + `Using ${authType} token for Docker registry ${registryHost}` + ); + opts.headers = { authorization: `${authType} ${opts.token}` }; + return opts.headers; } delete opts.username; delete opts.password; + delete opts.token; if (authenticateHeader.scheme.toUpperCase() === 'BASIC') { - logger.debug(`Using Basic auth for docker registry ${dockerRepository}`); + logger.trace(`Using Basic auth for docker registry ${registryHost}`); await http.get(apiCheckUrl, opts); return opts.headers; }