Skip to content

Commit

Permalink
feat(poetry): support GCloud credentials for Google Artifact Registry…
Browse files Browse the repository at this point in the history
… when locking (#32586)

Co-authored-by: Rhys Arkins <[email protected]>
  • Loading branch information
maxbrunet and rarkins authored Jan 8, 2025
1 parent 39fb207 commit 147b620
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 8 deletions.
4 changes: 4 additions & 0 deletions lib/modules/manager/poetry/__fixtures__/pyproject.10.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ url = "last.url"
[[tool.poetry.source]]
name = "five"

[[tool.poetry.source]]
name = "invalid-url"
url = "invalid-url"

[build-system]
requires = ["poetry_core>=1.0", "wheel"]
build-backend = "poetry.masonry.api"
105 changes: 104 additions & 1 deletion lib/modules/manager/poetry/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { codeBlock } from 'common-tags';
import { GoogleAuth as _googleAuth } from 'google-auth-library';
import { mockDeep } from 'jest-mock-extended';
import { join } from 'upath';
import { envMock, mockExecAll } from '../../../../test/exec-util';
Expand All @@ -15,16 +16,26 @@ import { updateArtifacts } from '.';

const pyproject1toml = Fixtures.get('pyproject.1.toml');
const pyproject10toml = Fixtures.get('pyproject.10.toml');
const pyproject13toml = `[[tool.poetry.source]]
name = "some-gar-repo"
url = "https://someregion-python.pkg.dev/some-project/some-repo/simple/"
[build-system]
requires = ["poetry_core>=1.0", "wheel"]
build-backend = "poetry.masonry.api"
`;

jest.mock('../../../util/exec/env');
jest.mock('../../../util/fs');
jest.mock('../../datasource', () => mockDeep());
jest.mock('../../../util/host-rules', () => mockDeep());
jest.mock('google-auth-library');

process.env.CONTAINERBASE = 'true';

const datasource = mocked(_datasource);
const hostRules = mocked(_hostRules);
const googleAuth = mocked(_googleAuth);

const adminConfig: RepoGlobalConfig = {
localDir: join('/tmp/github/some/repo'),
Expand Down Expand Up @@ -198,7 +209,99 @@ describe('modules/manager/poetry/artifacts', () => {
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(5);
expect(hostRules.find.mock.calls).toHaveLength(7);
expect(execSnapshots).toMatchObject([
{
cmd: 'poetry update --lock --no-interaction dep1',
options: {
env: {
POETRY_HTTP_BASIC_ONE_PASSWORD: 'passwordOne',
POETRY_HTTP_BASIC_ONE_USERNAME: 'usernameOne',
POETRY_HTTP_BASIC_TWO_USERNAME: 'usernameTwo',
POETRY_HTTP_BASIC_FOUR_OH_FOUR_PASSWORD: 'passwordFour',
},
},
},
]);
});

it('passes Google Artifact Registry credentials environment vars', async () => {
// poetry.lock
fs.getSiblingFileName.mockReturnValueOnce('poetry.lock');
fs.readLocalFile.mockResolvedValueOnce(null);
// pyproject.lock
fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock');
fs.readLocalFile.mockResolvedValueOnce('[metadata]\n');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('New poetry.lock');
googleAuth.mockImplementationOnce(
jest.fn().mockImplementationOnce(() => ({
getAccessToken: jest.fn().mockResolvedValue('some-token'),
})),
);
const updatedDeps = [{ depName: 'dep1' }];
expect(
await updateArtifacts({
packageFileName: 'pyproject.toml',
updatedDeps,
newPackageFileContent: pyproject13toml,
config,
}),
).toEqual([
{
file: {
type: 'addition',
path: 'pyproject.lock',
contents: 'New poetry.lock',
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(3);
expect(execSnapshots).toMatchObject([
{
cmd: 'poetry update --lock --no-interaction dep1',
options: {
env: {
POETRY_HTTP_BASIC_SOME_GAR_REPO_USERNAME: 'oauth2accesstoken',
POETRY_HTTP_BASIC_SOME_GAR_REPO_PASSWORD: 'some-token',
},
},
},
]);
});

it('continues if Google auth is not configured', async () => {
// poetry.lock
fs.getSiblingFileName.mockReturnValueOnce('poetry.lock');
fs.readLocalFile.mockResolvedValueOnce(null);
// pyproject.lock
fs.getSiblingFileName.mockReturnValueOnce('pyproject.lock');
fs.readLocalFile.mockResolvedValueOnce('[metadata]\n');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('New poetry.lock');
googleAuth.mockImplementation(
jest.fn().mockImplementation(() => ({
getAccessToken: jest.fn().mockResolvedValue(undefined),
})),
);
const updatedDeps = [{ depName: 'dep1' }];
expect(
await updateArtifacts({
packageFileName: 'pyproject.toml',
updatedDeps,
newPackageFileContent: pyproject13toml,
config,
}),
).toEqual([
{
file: {
type: 'addition',
path: 'pyproject.lock',
contents: 'New poetry.lock',
},
},
]);
expect(hostRules.find.mock.calls).toHaveLength(3);
expect(execSnapshots).toMatchObject([
{ cmd: 'poetry update --lock --no-interaction dep1' },
]);
Expand Down
41 changes: 34 additions & 7 deletions lib/modules/manager/poetry/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { find } from '../../../util/host-rules';
import { regEx } from '../../../util/regex';
import { Result } from '../../../util/result';
import { parse as parseToml } from '../../../util/toml';
import { parseUrl } from '../../../util/url';
import { PypiDatasource } from '../../datasource/pypi';
import { getGoogleAuthTokenRaw } from '../../datasource/util';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import { Lockfile, PoetrySchemaToml } from './schema';
import type { PoetryFile, PoetrySource } from './types';
Expand Down Expand Up @@ -101,7 +103,7 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] {
return [];
}
if (!pyprojectFile.tool?.poetry) {
logger.debug(`{$fileName} contains no poetry section`);
logger.debug(`${fileName} contains no poetry section`);
return [];
}

Expand All @@ -115,20 +117,42 @@ function getPoetrySources(content: string, fileName: string): PoetrySource[] {
return sourceArray;
}

function getMatchingHostRule(url: string | undefined): HostRule {
async function getMatchingHostRule(url: string | undefined): Promise<HostRule> {
const scopedMatch = find({ hostType: PypiDatasource.id, url });
return is.nonEmptyObject(scopedMatch) ? scopedMatch : find({ url });
const hostRule = is.nonEmptyObject(scopedMatch) ? scopedMatch : find({ url });
if (hostRule) {
return hostRule;
}

const parsedUrl = parseUrl(url);
if (!parsedUrl) {
logger.once.debug(`Failed to parse URL ${url}`);
return {};
}

if (parsedUrl.hostname.endsWith('.pkg.dev')) {
const accessToken = await getGoogleAuthTokenRaw();
if (accessToken) {
return {
username: 'oauth2accesstoken',
password: accessToken,
};
}
logger.once.debug(`Could not get Google access token (url=${url})`);
}

return {};
}

function getSourceCredentialVars(
async function getSourceCredentialVars(
pyprojectContent: string,
packageFileName: string,
): NodeJS.ProcessEnv {
): Promise<NodeJS.ProcessEnv> {
const poetrySources = getPoetrySources(pyprojectContent, packageFileName);
const envVars: NodeJS.ProcessEnv = {};

for (const source of poetrySources) {
const matchingHostRule = getMatchingHostRule(source.url);
const matchingHostRule = await getMatchingHostRule(source.url);
const formattedSourceName = source.name
.replace(regEx(/(\.|-)+/g), '_')
.toUpperCase();
Expand Down Expand Up @@ -192,7 +216,10 @@ export async function updateArtifacts({
config.constraints?.poetry ??
getPoetryRequirement(newPackageFileContent, existingLockFileContent);
const extraEnv = {
...getSourceCredentialVars(newPackageFileContent, packageFileName),
...(await getSourceCredentialVars(
newPackageFileContent,
packageFileName,
)),
...getGitEnvironmentVariables(['poetry']),
PIP_CACHE_DIR: await ensureCacheDir('pip'),
};
Expand Down

0 comments on commit 147b620

Please sign in to comment.