Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pipenv): add support for auth #24581

Merged
merged 26 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d10618
feat: add support for pipenv auth
maxromanovsky Sep 21, 2023
c332b25
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Sep 27, 2023
219cdf4
feat: add support for pipenv auth. tests not working yet
maxromanovsky Sep 27, 2023
79241b3
feat: add support for pipenv auth. tests
maxromanovsky Sep 28, 2023
69f98a2
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Sep 28, 2023
ce2d8ec
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Sep 29, 2023
969bc9d
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Oct 12, 2023
bbbe542
feat: add support for pipenv auth. code review fixes
maxromanovsky Oct 12, 2023
cef05b0
feat: add support for pipenv auth. code review fixes
maxromanovsky Oct 12, 2023
d7f2661
feat: add support for pipenv auth. code review fixes
maxromanovsky Oct 12, 2023
9e4833c
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Oct 12, 2023
59fe924
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Oct 23, 2023
15dfda8
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Oct 24, 2023
3b81dc2
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Oct 26, 2023
64f05cf
Apply suggestions from code review
maxromanovsky Oct 26, 2023
1614caa
feat: add support for pipenv auth. code review fixes
maxromanovsky Oct 26, 2023
3402638
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Oct 31, 2023
68d4d4b
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Nov 2, 2023
e8e7a16
feat: add support for pipenv auth. code review fixes
maxromanovsky Nov 2, 2023
962fb98
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Nov 2, 2023
3a308ff
Merge branch 'main' into feat/24541-support-pipenv-auth
rarkins Nov 2, 2023
a8ef436
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Nov 13, 2023
1ad25c6
prettier-fix
rarkins Nov 13, 2023
54f8f53
Apply suggestions from code review
maxromanovsky Nov 14, 2023
347a975
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Nov 14, 2023
7438cbf
Merge branch 'main' into feat/24541-support-pipenv-auth
maxromanovsky Nov 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/usage/getting-started/private-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,19 @@ For example:
}
```

### pipenv

If a `Pipfile` contains a `source` with `USERNAME` or `PASSWORD` environment variables and there is a `hostRules` entry with a matching host plus `username` and `password` fields, then these variables would be passed to `pipenv lock`.

For example:

```ini
[[source]]
url = "https://$USERNAME:${PASSWORD}@mypypi.example.com/simple"
verify_ssl = true
name = "pypi"
```

### poetry

For every Poetry source, a `hostRules` search is done and then any found credentials are added to env like `POETRY_HTTP_BASIC_X_USERNAME` and `POETRY_HTTP_BASIC_X_PASSWORD`, where `X` represents the normalized name of the source in `pyproject.toml`.
Expand Down
12 changes: 12 additions & 0 deletions lib/modules/manager/pipenv/__fixtures__/Pipfile6
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[[source]]
url = "https://$USERNAME:${PASSWORD}@mypypi.example.com/simple"
verify_ssl = true
name = "private"

[packages]
requests = {version = "==0.21.0", index = "private"}
12 changes: 12 additions & 0 deletions lib/modules/manager/pipenv/__fixtures__/Pipfile7
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[[source]]
url = "https://$USERNAME_FOO:${PAZZWORD}@mypypi.example.com/simple"
verify_ssl = true
name = "private"

[packages]
requests = {version = "==0.21.0", index = "private"}
120 changes: 120 additions & 0 deletions lib/modules/manager/pipenv/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mockDeep } from 'jest-mock-extended';
import { join } from 'upath';
import { envMock, mockExecAll } from '../../../../test/exec-util';
import { Fixtures } from '../../../../test/fixtures';
import {
env,
fs,
Expand All @@ -13,12 +14,14 @@ import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import * as docker from '../../../util/exec/docker';
import type { StatusResult } from '../../../util/git/types';
import { find as _find } from '../../../util/host-rules';
import { getPkgReleases as _getPkgReleases } from '../../datasource';
import * as _datasource from '../../datasource';
import type { UpdateArtifactsConfig } from '../types';
import * as pipenv from '.';

const datasource = mocked(_datasource);
const find = mockedFunction(_find);

jest.mock('../../../util/exec/env');
jest.mock('../../../util/git');
Expand Down Expand Up @@ -328,4 +331,121 @@ describe('modules/manager/pipenv/artifacts', () => {
).not.toBeNull();
expect(execSnapshots).toMatchSnapshot();
});

it('passes private credential environment vars', async () => {
fs.ensureCacheDir.mockResolvedValueOnce(
'/tmp/renovate/cache/others/pipenv',
);
fs.readLocalFile.mockResolvedValueOnce('current pipfile.lock');
const execSnapshots = mockExecAll();
git.getRepoStatus.mockResolvedValue(
partial<StatusResult>({
modified: ['Pipfile.lock'],
}),
);
fs.readLocalFile.mockResolvedValueOnce('New Pipfile.lock');

find.mockReturnValueOnce({
username: 'usernameOne',
password: 'passwordTwo',
});

const pipfile = Fixtures.get('Pipfile6');

expect(
await pipenv.updateArtifacts({
packageFileName: 'Pipfile',
updatedDeps: [],
newPackageFileContent: pipfile,
config: { ...config, constraints: { python: '== 3.8.*' } },
}),
).toEqual([
{
file: {
contents: 'New Pipfile.lock',
path: 'Pipfile.lock',
type: 'addition',
},
},
]);

expect(execSnapshots).toMatchObject([
{
cmd: 'pipenv lock',
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',
PASSWORD: 'passwordTwo',
PATH: '/tmp/path',
PIPENV_CACHE_DIR: '/tmp/renovate/cache/others/pipenv',
USERNAME: 'usernameOne',
},
maxBuffer: 10485760,
timeout: 900000,
},
},
]);
});

it('does not pass private credential environment vars if variable names differ from allowed', async () => {
fs.ensureCacheDir.mockResolvedValueOnce(
'/tmp/renovate/cache/others/pipenv',
);
fs.readLocalFile.mockResolvedValueOnce('current pipfile.lock');
const execSnapshots = mockExecAll();
git.getRepoStatus.mockResolvedValue(
partial<StatusResult>({
modified: ['Pipfile.lock'],
}),
);
fs.readLocalFile.mockResolvedValueOnce('New Pipfile.lock');

const pipfile = Fixtures.get('Pipfile7');

expect(
await pipenv.updateArtifacts({
packageFileName: 'Pipfile',
updatedDeps: [],
newPackageFileContent: pipfile,
config: { ...config, constraints: { python: '== 3.8.*' } },
}),
).toEqual([
{
file: {
contents: 'New Pipfile.lock',
path: 'Pipfile.lock',
type: 'addition',
},
},
]);

expect(execSnapshots).toMatchObject([
{
cmd: 'pipenv lock',
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',
PATH: '/tmp/path',
PIPENV_CACHE_DIR: '/tmp/renovate/cache/others/pipenv',
},
maxBuffer: 10485760,
timeout: 900000,
},
},
]);
});
});
67 changes: 62 additions & 5 deletions lib/modules/manager/pipenv/artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import type { HostRule } from '../../../types';
import { exec } from '../../../util/exec';
import type { ExecOptions } from '../../../util/exec/types';
import type { ExecOptions, ExtraEnv, Opt } from '../../../util/exec/types';
import {
deleteLocalFile,
ensureCacheDir,
readLocalFile,
writeLocalFile,
} from '../../../util/fs';
import { getRepoStatus } from '../../../util/git';
import { find } from '../../../util/host-rules';
import { PypiDatasource } from '../../datasource/pypi';
import type {
UpdateArtifact,
UpdateArtifactsConfig,
UpdateArtifactsResult,
} from '../types';
import { extractPackageFile } from './extract';
import { PipfileLockSchema } from './schema';

export function getPythonConstraint(
Expand Down Expand Up @@ -78,6 +82,37 @@ export function getPipenvConstraint(
return '';
}

function getMatchingHostRule(url: string | undefined): HostRule {
maxromanovsky marked this conversation as resolved.
Show resolved Hide resolved
return find({ hostType: PypiDatasource.id, url });
}

async function findPipfileSourceUrlWithCredentials(
pipfileContent: string,
pipfileName: string,
): Promise<string | null> {
const pipfile = await extractPackageFile(pipfileContent, pipfileName);
if (!pipfile) {
logger.debug('Error parsing Pipfile');
return null;
}

const credentialTokens = [
'$USERNAME:',
// eslint-disable-next-line no-template-curly-in-string
'${USERNAME}',
'$PASSWORD@',
// eslint-disable-next-line no-template-curly-in-string
'${PASSWORD}',
];

const sourceWithCredentials = pipfile.registryUrls?.find((url) =>
credentialTokens.some((token) => url.includes(token)),
);

// Only one source is currently supported
return sourceWithCredentials ?? null;
}

export async function updateArtifacts({
packageFileName: pipfileName,
newPackageFileContent: newPipfileContent,
Expand All @@ -102,12 +137,12 @@ export async function updateArtifacts({
existingLockFileContent,
config,
);
const extraEnv: Opt<ExtraEnv> = {
PIPENV_CACHE_DIR: await ensureCacheDir('pipenv'),
PIP_CACHE_DIR: await ensureCacheDir('pip'),
};
const execOptions: ExecOptions = {
cwdFile: pipfileName,
extraEnv: {
PIPENV_CACHE_DIR: await ensureCacheDir('pipenv'),
PIP_CACHE_DIR: await ensureCacheDir('pip'),
},
docker: {},
toolConstraints: [
{
Expand All @@ -120,6 +155,28 @@ export async function updateArtifacts({
},
],
};

const sourceUrl = await findPipfileSourceUrlWithCredentials(
newPipfileContent,
pipfileName,
);
if (sourceUrl) {
logger.trace('Pipfile contains credentials');
maxromanovsky marked this conversation as resolved.
Show resolved Hide resolved
const hostRule = getMatchingHostRule(sourceUrl);
if (hostRule) {
logger.trace('Found matching hostRule for Pipfile credentials');
maxromanovsky marked this conversation as resolved.
Show resolved Hide resolved
if (hostRule.username) {
logger.trace('Adding USERNAME environment variable for pipenv');
maxromanovsky marked this conversation as resolved.
Show resolved Hide resolved
extraEnv.USERNAME = hostRule.username;
}
if (hostRule.password) {
logger.trace('Adding PASSWORD environment variable for pipenv');
maxromanovsky marked this conversation as resolved.
Show resolved Hide resolved
extraEnv.PASSWORD = hostRule.password;
}
}
}
execOptions.extraEnv = extraEnv;

logger.trace({ cmd }, 'pipenv lock command');
await exec(cmd, execOptions);
const status = await getRepoStatus();
Expand Down