neonctl 1.29.1 → 1.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/analytics.js +78 -0
- package/api.js +35 -0
- package/auth.js +101 -0
- package/{src/cli.ts → cli.js} +0 -1
- package/commands/auth.js +102 -0
- package/commands/auth.test.js +42 -0
- package/commands/branches.js +303 -0
- package/commands/branches.test.js +321 -0
- package/commands/connection_string.js +156 -0
- package/commands/connection_string.test.js +236 -0
- package/commands/databases.js +79 -0
- package/commands/databases.test.js +51 -0
- package/commands/help.test.js +11 -0
- package/{src/commands/index.ts → commands/index.js} +10 -11
- package/commands/ip_allow.js +135 -0
- package/commands/ip_allow.test.js +78 -0
- package/commands/operations.js +28 -0
- package/commands/operations.test.js +11 -0
- package/commands/projects.js +186 -0
- package/commands/projects.test.js +132 -0
- package/commands/roles.js +57 -0
- package/commands/roles.test.js +42 -0
- package/commands/set_context.js +22 -0
- package/commands/set_context.test.js +53 -0
- package/commands/user.js +15 -0
- package/config.js +11 -0
- package/context.js +48 -0
- package/env.js +6 -0
- package/errors.js +16 -0
- package/help.js +146 -0
- package/index.js +168 -0
- package/log.js +15 -0
- package/package.json +1 -1
- package/parameters.gen.js +322 -0
- package/pkg.js +3 -45
- package/test_utils/mock_server.js +16 -0
- package/test_utils/oauth_server.js +9 -0
- package/test_utils/test_cli_command.js +80 -0
- package/types.js +1 -0
- package/utils/enrichers.js +49 -0
- package/utils/formats.js +5 -0
- package/utils/formats.test.js +32 -0
- package/utils/middlewares.js +20 -0
- package/utils/point_in_time.js +50 -0
- package/utils/psql.js +24 -0
- package/utils/string.js +5 -0
- package/utils/ui.js +59 -0
- package/writer.js +87 -0
- package/writer.test.js +86 -0
- package/.bump +0 -1
- package/.editorconfig +0 -7
- package/.eslintrc.cjs +0 -15
- package/.github/workflows/commitlint.yml +0 -46
- package/.github/workflows/pr.yml +0 -25
- package/.github/workflows/release.yml +0 -30
- package/.husky/commit-msg +0 -4
- package/.husky/pre-commit +0 -4
- package/.nvmrc +0 -1
- package/.prettierignore +0 -3
- package/.prettierrc.json +0 -3
- package/.releaserc.json +0 -47
- package/LICENSE +0 -202
- package/commitlint.config.cjs +0 -7
- package/generateOptionsFromSpec.ts +0 -68
- package/jest/setup.js +0 -5
- package/jest.config.ts +0 -199
- package/mocks/bin/psql.cjs +0 -9
- package/mocks/main/projects/GET.js +0 -27
- package/mocks/main/projects/POST.js +0 -22
- package/mocks/main/projects/shared/GET.js +0 -16
- package/mocks/main/projects/test/DELETE.json +0 -7
- package/mocks/main/projects/test/GET.json +0 -13
- package/mocks/main/projects/test/PATCH.js +0 -18
- package/mocks/main/projects/test/branches/GET.json +0 -25
- package/mocks/main/projects/test/branches/POST.js +0 -83
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/DELETE.json +0 -7
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/GET.json +0 -9
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/PATCH.js +0 -14
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/GET.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/POST.js +0 -13
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/test_db/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/GET.json +0 -26
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/POST.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/POST.js +0 -14
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/reveal_password/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/set_as_primary/POST.json +0 -9
- package/mocks/main/projects/test/branches/br-numbered-branch-123456/GET.json +0 -10
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/DELETE.json +0 -7
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/GET.json +0 -10
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/PATCH.js +0 -14
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/GET.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/POST.js +0 -13
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/test_db/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/GET.json +0 -26
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/POST.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/restore/POST.js +0 -16
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/POST.js +0 -14
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/DELETE.json +0 -6
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/reveal_password/GET.json +0 -3
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/set_as_primary/POST.json +0 -9
- package/mocks/main/projects/test/endpoints/GET.json +0 -9
- package/mocks/main/projects/test/endpoints/POST.js +0 -32
- package/mocks/main/projects/test/endpoints/test_endpoint_id/DELETE.json +0 -7
- package/mocks/main/projects/test/endpoints/test_endpoint_id/GET.json +0 -9
- package/mocks/main/projects/test/endpoints/test_endpoint_id/PATCH.js +0 -17
- package/mocks/main/projects/test/operations/GET.json +0 -22
- package/mocks/main/users/me/GET.json +0 -5
- package/mocks/restore/projects/test/branches/GET.json +0 -21
- package/mocks/restore/projects/test/branches/br-another-branch-123456/GET.json +0 -6
- package/mocks/restore/projects/test/branches/br-another-branch-123456/restore/POST.js +0 -13
- package/mocks/restore/projects/test/branches/br-any-branch-123456/GET.json +0 -6
- package/mocks/restore/projects/test/branches/br-parent-tots-123456/GET.json +0 -7
- package/mocks/restore/projects/test/branches/br-parent-tots-123456/restore/POST.js +0 -14
- package/mocks/restore/projects/test/branches/br-self-tolsn-123456/GET.json +0 -6
- package/mocks/restore/projects/test/branches/br-self-tolsn-123456/restore/POST.js +0 -15
- package/mocks/single_project/projects/GET.json +0 -10
- package/mocks/single_project/projects/test-project-123456/GET.json +0 -14
- package/mocks/single_project/projects/test-project-123456/branches/GET.json +0 -11
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/databases/GET.json +0 -3
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/endpoints/GET.json +0 -10
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/GET.json +0 -3
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/test_role/reveal_password/GET.json +0 -3
- package/rollup.config.js +0 -20
- package/snapshots/commands/branches.test.snap +0 -221
- package/snapshots/commands/connection_string.test.snap +0 -70
- package/snapshots/commands/databases.test.snap +0 -20
- package/snapshots/commands/ip_allow.test.snap +0 -55
- package/snapshots/commands/operations.test.snap +0 -17
- package/snapshots/commands/projects.test.snap +0 -141
- package/snapshots/commands/roles.test.snap +0 -19
- package/snapshots/commands/set_context.test.snap +0 -30
- package/snapshots/writer.test.snap +0 -60
- package/snapshotsResolver.cjs +0 -32
- package/src/analytics.ts +0 -95
- package/src/api.ts +0 -44
- package/src/auth.ts +0 -137
- package/src/commands/auth.test.ts +0 -62
- package/src/commands/auth.ts +0 -148
- package/src/commands/branches.test.ts +0 -354
- package/src/commands/branches.ts +0 -451
- package/src/commands/connection_string.test.ts +0 -250
- package/src/commands/connection_string.ts +0 -210
- package/src/commands/databases.test.ts +0 -55
- package/src/commands/databases.ts +0 -129
- package/src/commands/help.test.ts +0 -13
- package/src/commands/ip_allow.test.ts +0 -86
- package/src/commands/ip_allow.ts +0 -202
- package/src/commands/operations.test.ts +0 -13
- package/src/commands/operations.ts +0 -41
- package/src/commands/projects.test.ts +0 -147
- package/src/commands/projects.ts +0 -275
- package/src/commands/roles.test.ts +0 -46
- package/src/commands/roles.ts +0 -100
- package/src/commands/set_context.test.ts +0 -64
- package/src/commands/set_context.ts +0 -27
- package/src/commands/user.ts +0 -21
- package/src/config.ts +0 -22
- package/src/context.ts +0 -61
- package/src/env.ts +0 -7
- package/src/errors.ts +0 -24
- package/src/help.ts +0 -185
- package/src/index.ts +0 -180
- package/src/log.ts +0 -16
- package/src/parameters.gen.ts +0 -332
- package/src/pkg.ts +0 -9
- package/src/test_utils/mock_server.ts +0 -27
- package/src/test_utils/oauth_server.ts +0 -10
- package/src/test_utils/test_cli_command.ts +0 -117
- package/src/types.ts +0 -25
- package/src/utils/enrichers.ts +0 -73
- package/src/utils/formats.test.ts +0 -41
- package/src/utils/formats.ts +0 -11
- package/src/utils/middlewares.ts +0 -23
- package/src/utils/point_in_time.ts +0 -86
- package/src/utils/psql.ts +0 -29
- package/src/utils/string.ts +0 -8
- package/src/utils/ui.ts +0 -64
- package/src/writer.test.ts +0 -98
- package/src/writer.ts +0 -131
- package/tsconfig.json +0 -17
- /package/{src/callback.html → callback.html} +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { retryOnLock } from '../api.js';
|
|
2
|
+
import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
|
|
3
|
+
import { writer } from '../writer.js';
|
|
4
|
+
const DATABASE_FIELDS = ['name', 'owner_name', 'created_at'];
|
|
5
|
+
export const command = 'databases';
|
|
6
|
+
export const describe = 'Manage databases';
|
|
7
|
+
export const aliases = ['database', 'db'];
|
|
8
|
+
export const builder = (argv) => argv
|
|
9
|
+
.usage('$0 databases <sub-command> [options]')
|
|
10
|
+
.options({
|
|
11
|
+
'project-id': {
|
|
12
|
+
describe: 'Project ID',
|
|
13
|
+
type: 'string',
|
|
14
|
+
},
|
|
15
|
+
branch: {
|
|
16
|
+
describe: 'Branch ID or name',
|
|
17
|
+
type: 'string',
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
.middleware(fillSingleProject)
|
|
21
|
+
.command('list', 'List databases', (yargs) => yargs, async (args) => await list(args))
|
|
22
|
+
.command('create', 'Create a database', (yargs) => yargs.options({
|
|
23
|
+
name: {
|
|
24
|
+
describe: 'Database name',
|
|
25
|
+
type: 'string',
|
|
26
|
+
demandOption: true,
|
|
27
|
+
},
|
|
28
|
+
'owner-name': {
|
|
29
|
+
describe: 'Owner name',
|
|
30
|
+
type: 'string',
|
|
31
|
+
},
|
|
32
|
+
}), async (args) => await create(args))
|
|
33
|
+
.command('delete <database>', 'Delete a database', (yargs) => yargs, async (args) => await deleteDb(args));
|
|
34
|
+
export const handler = (args) => {
|
|
35
|
+
return args;
|
|
36
|
+
};
|
|
37
|
+
export const list = async (props) => {
|
|
38
|
+
const branchId = await branchIdFromProps(props);
|
|
39
|
+
const { data } = await props.apiClient.listProjectBranchDatabases(props.projectId, branchId);
|
|
40
|
+
writer(props).end(data.databases, {
|
|
41
|
+
fields: DATABASE_FIELDS,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
export const create = async (props) => {
|
|
45
|
+
const branchId = await branchIdFromProps(props);
|
|
46
|
+
const owner = props.ownerName ??
|
|
47
|
+
(await props.apiClient
|
|
48
|
+
.listProjectBranchRoles(props.projectId, branchId)
|
|
49
|
+
.then(({ data }) => {
|
|
50
|
+
if (data.roles.length === 0) {
|
|
51
|
+
throw new Error(`No roles found in branch ${branchId}`);
|
|
52
|
+
}
|
|
53
|
+
if (data.roles.length > 1) {
|
|
54
|
+
throw new Error(`More than one role found in branch ${branchId}. Please specify the owner name. Roles: ${data.roles
|
|
55
|
+
.map((r) => r.name)
|
|
56
|
+
.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
return data.roles[0].name;
|
|
59
|
+
}));
|
|
60
|
+
if (!owner) {
|
|
61
|
+
throw new Error('No owner found');
|
|
62
|
+
}
|
|
63
|
+
const { data } = await retryOnLock(() => props.apiClient.createProjectBranchDatabase(props.projectId, branchId, {
|
|
64
|
+
database: {
|
|
65
|
+
name: props.name,
|
|
66
|
+
owner_name: owner,
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
writer(props).end(data.database, {
|
|
70
|
+
fields: DATABASE_FIELDS,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
export const deleteDb = async (props) => {
|
|
74
|
+
const branchId = await branchIdFromProps(props);
|
|
75
|
+
const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.projectId, branchId, props.database));
|
|
76
|
+
writer(props).end(data.database, {
|
|
77
|
+
fields: DATABASE_FIELDS,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe } from '@jest/globals';
|
|
2
|
+
import { testCliCommand } from '../test_utils/test_cli_command.js';
|
|
3
|
+
describe('databases', () => {
|
|
4
|
+
testCliCommand({
|
|
5
|
+
name: 'list',
|
|
6
|
+
args: [
|
|
7
|
+
'databases',
|
|
8
|
+
'list',
|
|
9
|
+
'--project-id',
|
|
10
|
+
'test',
|
|
11
|
+
'--branch',
|
|
12
|
+
'test_branch',
|
|
13
|
+
],
|
|
14
|
+
expected: {
|
|
15
|
+
snapshot: true,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
testCliCommand({
|
|
19
|
+
name: 'create',
|
|
20
|
+
args: [
|
|
21
|
+
'databases',
|
|
22
|
+
'create',
|
|
23
|
+
'--project-id',
|
|
24
|
+
'test',
|
|
25
|
+
'--branch',
|
|
26
|
+
'test_branch',
|
|
27
|
+
'--name',
|
|
28
|
+
'test_db',
|
|
29
|
+
'--owner-name',
|
|
30
|
+
'test_owner',
|
|
31
|
+
],
|
|
32
|
+
expected: {
|
|
33
|
+
snapshot: true,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
testCliCommand({
|
|
37
|
+
name: 'delete',
|
|
38
|
+
args: [
|
|
39
|
+
'databases',
|
|
40
|
+
'delete',
|
|
41
|
+
'test_db',
|
|
42
|
+
'--project-id',
|
|
43
|
+
'test',
|
|
44
|
+
'--branch',
|
|
45
|
+
'test_branch',
|
|
46
|
+
],
|
|
47
|
+
expected: {
|
|
48
|
+
snapshot: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect } from '@jest/globals';
|
|
2
|
+
import { testCliCommand } from '../test_utils/test_cli_command.js';
|
|
3
|
+
describe('help', () => {
|
|
4
|
+
testCliCommand({
|
|
5
|
+
name: 'without args',
|
|
6
|
+
args: [],
|
|
7
|
+
expected: {
|
|
8
|
+
stderr: expect.stringContaining(`neonctl <command> [options]`),
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -8,16 +8,15 @@ import * as roles from './roles.js';
|
|
|
8
8
|
import * as operations from './operations.js';
|
|
9
9
|
import * as cs from './connection_string.js';
|
|
10
10
|
import * as setContext from './set_context.js';
|
|
11
|
-
|
|
12
11
|
export default [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
auth,
|
|
13
|
+
users,
|
|
14
|
+
projects,
|
|
15
|
+
ipAllow,
|
|
16
|
+
branches,
|
|
17
|
+
databases,
|
|
18
|
+
roles,
|
|
19
|
+
operations,
|
|
20
|
+
cs,
|
|
21
|
+
setContext,
|
|
23
22
|
];
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { writer } from '../writer.js';
|
|
2
|
+
import { fillSingleProject } from '../utils/enrichers.js';
|
|
3
|
+
import { projectUpdateRequest } from '../parameters.gen.js';
|
|
4
|
+
import { log } from '../log.js';
|
|
5
|
+
const IP_ALLOW_FIELDS = [
|
|
6
|
+
'id',
|
|
7
|
+
'name',
|
|
8
|
+
'IP_addresses',
|
|
9
|
+
'primary_branch_only',
|
|
10
|
+
];
|
|
11
|
+
export const command = 'ip-allow';
|
|
12
|
+
export const describe = 'Manage IP Allow';
|
|
13
|
+
export const builder = (argv) => {
|
|
14
|
+
return argv
|
|
15
|
+
.usage('$0 ip-allow <sub-command> [options]')
|
|
16
|
+
.options({
|
|
17
|
+
'project-id': {
|
|
18
|
+
describe: 'Project ID',
|
|
19
|
+
type: 'string',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
.middleware(fillSingleProject)
|
|
23
|
+
.command('list', 'List the IP allowlist', (yargs) => yargs, async (args) => {
|
|
24
|
+
await list(args);
|
|
25
|
+
})
|
|
26
|
+
.command('add [ips...]', 'Add IP addresses to the IP allowlist', (yargs) => yargs
|
|
27
|
+
.usage('$0 ip-allow add [ips...]')
|
|
28
|
+
.positional('ips', {
|
|
29
|
+
describe: 'The list of IP addresses to add',
|
|
30
|
+
type: 'string',
|
|
31
|
+
default: [],
|
|
32
|
+
array: true,
|
|
33
|
+
})
|
|
34
|
+
.options({
|
|
35
|
+
'primary-only': {
|
|
36
|
+
describe: projectUpdateRequest['project.settings.allowed_ips.primary_branch_only'].description,
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
},
|
|
39
|
+
}), async (args) => {
|
|
40
|
+
await add(args);
|
|
41
|
+
})
|
|
42
|
+
.command('remove [ips...]', 'Remove IP addresses from the IP allowlist', (yargs) => yargs.usage('$0 ip-allow remove [ips...]').positional('ips', {
|
|
43
|
+
describe: 'The list of IP addresses to remove',
|
|
44
|
+
type: 'string',
|
|
45
|
+
default: [],
|
|
46
|
+
array: true,
|
|
47
|
+
}), async (args) => {
|
|
48
|
+
await remove(args);
|
|
49
|
+
})
|
|
50
|
+
.command('reset [ips...]', 'Reset the IP allowlist', (yargs) => yargs.usage('$0 ip-allow reset [ips...]').positional('ips', {
|
|
51
|
+
describe: 'The list of IP addresses to reset',
|
|
52
|
+
type: 'string',
|
|
53
|
+
default: [],
|
|
54
|
+
array: true,
|
|
55
|
+
}), async (args) => {
|
|
56
|
+
await reset(args);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
export const handler = (args) => {
|
|
60
|
+
return args;
|
|
61
|
+
};
|
|
62
|
+
const list = async (props) => {
|
|
63
|
+
const { data } = await props.apiClient.getProject(props.projectId);
|
|
64
|
+
writer(props).end(parse(data.project), {
|
|
65
|
+
fields: IP_ALLOW_FIELDS,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
const add = async (props) => {
|
|
69
|
+
if (props.ips.length <= 0) {
|
|
70
|
+
throw new Error(`Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
|
|
71
|
+
Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`);
|
|
72
|
+
}
|
|
73
|
+
const project = {};
|
|
74
|
+
const { data } = await props.apiClient.getProject(props.projectId);
|
|
75
|
+
const existingAllowedIps = data.project.settings?.allowed_ips;
|
|
76
|
+
project.settings = {
|
|
77
|
+
allowed_ips: {
|
|
78
|
+
ips: [...new Set(props.ips.concat(existingAllowedIps?.ips ?? []))],
|
|
79
|
+
primary_branch_only: props.primaryOnly ?? existingAllowedIps?.primary_branch_only ?? false,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const { data: response } = await props.apiClient.updateProject(props.projectId, {
|
|
83
|
+
project,
|
|
84
|
+
});
|
|
85
|
+
writer(props).end(parse(response.project), {
|
|
86
|
+
fields: IP_ALLOW_FIELDS,
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const remove = async (props) => {
|
|
90
|
+
if (props.ips.length <= 0) {
|
|
91
|
+
throw new Error(`Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id>`);
|
|
92
|
+
}
|
|
93
|
+
const project = {};
|
|
94
|
+
const { data } = await props.apiClient.getProject(props.projectId);
|
|
95
|
+
const existingAllowedIps = data.project.settings?.allowed_ips;
|
|
96
|
+
project.settings = {
|
|
97
|
+
allowed_ips: {
|
|
98
|
+
ips: existingAllowedIps?.ips?.filter((ip) => !props.ips.includes(ip)) ?? [],
|
|
99
|
+
primary_branch_only: existingAllowedIps?.primary_branch_only ?? false,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const { data: response } = await props.apiClient.updateProject(props.projectId, {
|
|
103
|
+
project,
|
|
104
|
+
});
|
|
105
|
+
writer(props).end(parse(response.project), {
|
|
106
|
+
fields: IP_ALLOW_FIELDS,
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const reset = async (props) => {
|
|
110
|
+
const project = {};
|
|
111
|
+
project.settings = {
|
|
112
|
+
allowed_ips: {
|
|
113
|
+
ips: props.ips,
|
|
114
|
+
primary_branch_only: false,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const { data } = await props.apiClient.updateProject(props.projectId, {
|
|
118
|
+
project,
|
|
119
|
+
});
|
|
120
|
+
writer(props).end(parse(data.project), {
|
|
121
|
+
fields: IP_ALLOW_FIELDS,
|
|
122
|
+
});
|
|
123
|
+
if (props.ips.length <= 0) {
|
|
124
|
+
log.info(`The IP allowlist has been reset. All databases on project "${data.project.name}" are now exposed to the internet`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const parse = (project) => {
|
|
128
|
+
const ips = project.settings?.allowed_ips?.ips ?? [];
|
|
129
|
+
return {
|
|
130
|
+
id: project.id,
|
|
131
|
+
name: project.name,
|
|
132
|
+
IP_addresses: ips,
|
|
133
|
+
primary_branch_only: project.settings?.allowed_ips?.primary_branch_only ?? false,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe } from '@jest/globals';
|
|
2
|
+
import { testCliCommand } from '../test_utils/test_cli_command.js';
|
|
3
|
+
describe('ip-allow', () => {
|
|
4
|
+
testCliCommand({
|
|
5
|
+
name: 'list IP allow',
|
|
6
|
+
args: ['ip-allow', 'list', '--project-id', 'test'],
|
|
7
|
+
expected: {
|
|
8
|
+
snapshot: true,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
testCliCommand({
|
|
12
|
+
name: 'list IP Allow with single-project',
|
|
13
|
+
args: ['ip-allow', 'list'],
|
|
14
|
+
mockDir: 'single_project',
|
|
15
|
+
expected: {
|
|
16
|
+
snapshot: true,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
testCliCommand({
|
|
20
|
+
name: 'Add IP allow - Error',
|
|
21
|
+
args: ['ip-allow', 'add', '--projectId', 'test'],
|
|
22
|
+
expected: {
|
|
23
|
+
code: 1,
|
|
24
|
+
stderr: `ERROR: Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
|
|
25
|
+
Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
testCliCommand({
|
|
29
|
+
name: 'Add IP allow',
|
|
30
|
+
args: [
|
|
31
|
+
'ip-allow',
|
|
32
|
+
'add',
|
|
33
|
+
'127.0.0.1',
|
|
34
|
+
'192.168.10.1-192.168.10.15',
|
|
35
|
+
'--primary-only',
|
|
36
|
+
'--project-id',
|
|
37
|
+
'test',
|
|
38
|
+
],
|
|
39
|
+
expected: {
|
|
40
|
+
snapshot: true,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
testCliCommand({
|
|
44
|
+
name: 'Remove IP allow - Error',
|
|
45
|
+
args: ['ip-allow', 'remove', '--project-id', 'test'],
|
|
46
|
+
expected: {
|
|
47
|
+
code: 1,
|
|
48
|
+
stderr: `ERROR: Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id>`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
testCliCommand({
|
|
52
|
+
name: 'Remove IP allow',
|
|
53
|
+
args: ['ip-allow', 'remove', '192.168.1.1', '--project-id', 'test'],
|
|
54
|
+
expected: {
|
|
55
|
+
snapshot: true,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
testCliCommand({
|
|
59
|
+
name: 'Reset IP allow',
|
|
60
|
+
args: ['ip-allow', 'reset', '--project-id', 'test'],
|
|
61
|
+
expected: {
|
|
62
|
+
snapshot: true,
|
|
63
|
+
stdout: `id: test
|
|
64
|
+
name: test_project
|
|
65
|
+
IP_addresses: []
|
|
66
|
+
primary_branch_only: false
|
|
67
|
+
`,
|
|
68
|
+
stderr: `INFO: The IP allowlist has been reset. All databases on project "test_project" are now exposed to the internet`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
testCliCommand({
|
|
72
|
+
name: 'Reset IP allow to new list',
|
|
73
|
+
args: ['ip-allow', 'reset', '192.168.2.2', '--project-id', 'test'],
|
|
74
|
+
expected: {
|
|
75
|
+
snapshot: true,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fillSingleProject } from '../utils/enrichers.js';
|
|
2
|
+
import { writer } from '../writer.js';
|
|
3
|
+
const OPERATIONS_FIELDS = ['id', 'action', 'status', 'created_at'];
|
|
4
|
+
export const command = 'operations';
|
|
5
|
+
export const describe = 'Manage operations';
|
|
6
|
+
export const aliases = ['operation'];
|
|
7
|
+
export const builder = (argv) => argv
|
|
8
|
+
.usage('$0 operations <sub-command> [options]')
|
|
9
|
+
.options({
|
|
10
|
+
'project-id': {
|
|
11
|
+
describe: 'Project ID',
|
|
12
|
+
type: 'string',
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
.middleware(fillSingleProject)
|
|
16
|
+
.command('list', 'List operations', (yargs) => yargs, async (args) => await list(args));
|
|
17
|
+
export const handler = (args) => {
|
|
18
|
+
return args;
|
|
19
|
+
};
|
|
20
|
+
export const list = async (props) => {
|
|
21
|
+
const { data } = await props.apiClient.listProjectOperations({
|
|
22
|
+
projectId: props.projectId,
|
|
23
|
+
limit: props.limit,
|
|
24
|
+
});
|
|
25
|
+
writer(props).end(data.operations, {
|
|
26
|
+
fields: OPERATIONS_FIELDS,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe } from '@jest/globals';
|
|
2
|
+
import { testCliCommand } from '../test_utils/test_cli_command.js';
|
|
3
|
+
describe('operations', () => {
|
|
4
|
+
testCliCommand({
|
|
5
|
+
name: 'list',
|
|
6
|
+
args: ['operations', 'list', '--project-id', 'test'],
|
|
7
|
+
expected: {
|
|
8
|
+
snapshot: true,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { log } from '../log.js';
|
|
2
|
+
import { projectCreateRequest, projectUpdateRequest, } from '../parameters.gen.js';
|
|
3
|
+
import { writer } from '../writer.js';
|
|
4
|
+
import { psql } from '../utils/psql.js';
|
|
5
|
+
import { updateContextFile } from '../context.js';
|
|
6
|
+
const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
|
|
7
|
+
const REGIONS = [
|
|
8
|
+
'aws-us-west-2',
|
|
9
|
+
'aws-ap-southeast-1',
|
|
10
|
+
'aws-eu-central-1',
|
|
11
|
+
'aws-us-east-2',
|
|
12
|
+
'aws-us-east-1',
|
|
13
|
+
];
|
|
14
|
+
const PROJECTS_LIST_LIMIT = 100;
|
|
15
|
+
export const command = 'projects';
|
|
16
|
+
export const describe = 'Manage projects';
|
|
17
|
+
export const aliases = ['project'];
|
|
18
|
+
export const builder = (argv) => {
|
|
19
|
+
return argv
|
|
20
|
+
.usage('$0 projects <sub-command> [options]')
|
|
21
|
+
.command('list', 'List projects', (yargs) => yargs, async (args) => {
|
|
22
|
+
await list(args);
|
|
23
|
+
})
|
|
24
|
+
.command('create', 'Create a project', (yargs) => yargs.options({
|
|
25
|
+
name: {
|
|
26
|
+
describe: projectCreateRequest['project.name'].description,
|
|
27
|
+
type: 'string',
|
|
28
|
+
},
|
|
29
|
+
'region-id': {
|
|
30
|
+
describe: `The region ID. Possible values: ${REGIONS.join(', ')}`,
|
|
31
|
+
type: 'string',
|
|
32
|
+
},
|
|
33
|
+
psql: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
describe: 'Connect to a new project via psql',
|
|
36
|
+
default: false,
|
|
37
|
+
},
|
|
38
|
+
database: {
|
|
39
|
+
describe: projectCreateRequest['project.branch.database_name'].description,
|
|
40
|
+
type: 'string',
|
|
41
|
+
},
|
|
42
|
+
role: {
|
|
43
|
+
describe: projectCreateRequest['project.branch.role_name'].description,
|
|
44
|
+
type: 'string',
|
|
45
|
+
},
|
|
46
|
+
'set-context': {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
describe: 'Set the current context to the new project',
|
|
49
|
+
default: false,
|
|
50
|
+
},
|
|
51
|
+
}), async (args) => {
|
|
52
|
+
await create(args);
|
|
53
|
+
})
|
|
54
|
+
.command('update <id>', 'Update a project', (yargs) => yargs.options({
|
|
55
|
+
name: {
|
|
56
|
+
describe: projectCreateRequest['project.name'].description,
|
|
57
|
+
type: 'string',
|
|
58
|
+
},
|
|
59
|
+
'ip-allow': {
|
|
60
|
+
describe: projectUpdateRequest['project.settings.allowed_ips.ips']
|
|
61
|
+
.description,
|
|
62
|
+
type: 'string',
|
|
63
|
+
array: true,
|
|
64
|
+
deprecated: "Deprecated. Use 'ip-allow' command",
|
|
65
|
+
},
|
|
66
|
+
'ip-primary-only': {
|
|
67
|
+
describe: projectUpdateRequest['project.settings.allowed_ips.primary_branch_only'].description,
|
|
68
|
+
type: 'boolean',
|
|
69
|
+
deprecated: "Deprecated. Use 'ip-allow' command",
|
|
70
|
+
},
|
|
71
|
+
}), async (args) => {
|
|
72
|
+
await update(args);
|
|
73
|
+
})
|
|
74
|
+
.command('delete <id>', 'Delete a project', (yargs) => yargs, async (args) => {
|
|
75
|
+
await deleteProject(args);
|
|
76
|
+
})
|
|
77
|
+
.command('get <id>', 'Get a project', (yargs) => yargs, async (args) => {
|
|
78
|
+
await get(args);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
export const handler = (args) => {
|
|
82
|
+
return args;
|
|
83
|
+
};
|
|
84
|
+
const list = async (props) => {
|
|
85
|
+
const getList = async (fn) => {
|
|
86
|
+
const result = [];
|
|
87
|
+
let cursor;
|
|
88
|
+
let end = false;
|
|
89
|
+
while (!end) {
|
|
90
|
+
const { data } = await fn({
|
|
91
|
+
limit: PROJECTS_LIST_LIMIT,
|
|
92
|
+
cursor,
|
|
93
|
+
});
|
|
94
|
+
result.push(...data.projects);
|
|
95
|
+
cursor = data.pagination?.cursor;
|
|
96
|
+
log.debug('Got %d projects, with cursor: %s', data.projects.length, cursor);
|
|
97
|
+
if (data.projects.length < PROJECTS_LIST_LIMIT) {
|
|
98
|
+
end = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
const [ownedProjects, sharedProjects] = await Promise.all([
|
|
104
|
+
getList(props.apiClient.listProjects),
|
|
105
|
+
getList(props.apiClient.listSharedProjects),
|
|
106
|
+
]);
|
|
107
|
+
const out = writer(props);
|
|
108
|
+
out.write(ownedProjects, {
|
|
109
|
+
fields: PROJECT_FIELDS,
|
|
110
|
+
title: 'Projects',
|
|
111
|
+
});
|
|
112
|
+
out.write(sharedProjects, {
|
|
113
|
+
fields: PROJECT_FIELDS,
|
|
114
|
+
title: 'Shared with me',
|
|
115
|
+
});
|
|
116
|
+
out.end();
|
|
117
|
+
};
|
|
118
|
+
const create = async (props) => {
|
|
119
|
+
const project = {};
|
|
120
|
+
if (props.name) {
|
|
121
|
+
project.name = props.name;
|
|
122
|
+
}
|
|
123
|
+
if (props.regionId) {
|
|
124
|
+
project.region_id = props.regionId;
|
|
125
|
+
}
|
|
126
|
+
project.branch = {};
|
|
127
|
+
if (props.database) {
|
|
128
|
+
project.branch.database_name = props.database;
|
|
129
|
+
}
|
|
130
|
+
if (props.role) {
|
|
131
|
+
project.branch.role_name = props.role;
|
|
132
|
+
}
|
|
133
|
+
const { data } = await props.apiClient.createProject({
|
|
134
|
+
project,
|
|
135
|
+
});
|
|
136
|
+
if (props.setContext) {
|
|
137
|
+
updateContextFile(props.contextFile, {
|
|
138
|
+
projectId: data.project.id,
|
|
139
|
+
branchId: data.branch.id,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const out = writer(props);
|
|
143
|
+
out.write(data.project, { fields: PROJECT_FIELDS, title: 'Project' });
|
|
144
|
+
out.write(data.connection_uris, {
|
|
145
|
+
fields: ['connection_uri'],
|
|
146
|
+
title: 'Connection URIs',
|
|
147
|
+
});
|
|
148
|
+
out.end();
|
|
149
|
+
if (props.psql) {
|
|
150
|
+
const connection_uri = data.connection_uris[0].connection_uri;
|
|
151
|
+
const psqlArgs = props['--'];
|
|
152
|
+
await psql(connection_uri, psqlArgs);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const deleteProject = async (props) => {
|
|
156
|
+
const { data } = await props.apiClient.deleteProject(props.id);
|
|
157
|
+
writer(props).end(data.project, {
|
|
158
|
+
fields: PROJECT_FIELDS,
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
const update = async (props) => {
|
|
162
|
+
const project = {};
|
|
163
|
+
if (props.name) {
|
|
164
|
+
project.name = props.name;
|
|
165
|
+
}
|
|
166
|
+
if (props.ipAllow || props.ipPrimaryOnly != undefined) {
|
|
167
|
+
const { data } = await props.apiClient.getProject(props.id);
|
|
168
|
+
const existingAllowedIps = data.project.settings?.allowed_ips;
|
|
169
|
+
project.settings = {
|
|
170
|
+
allowed_ips: {
|
|
171
|
+
ips: props.ipAllow ?? existingAllowedIps?.ips ?? [],
|
|
172
|
+
primary_branch_only: props.ipPrimaryOnly ??
|
|
173
|
+
existingAllowedIps?.primary_branch_only ??
|
|
174
|
+
false,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const { data } = await props.apiClient.updateProject(props.id, {
|
|
179
|
+
project,
|
|
180
|
+
});
|
|
181
|
+
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
182
|
+
};
|
|
183
|
+
const get = async (props) => {
|
|
184
|
+
const { data } = await props.apiClient.getProject(props.id);
|
|
185
|
+
writer(props).end(data.project, { fields: PROJECT_FIELDS });
|
|
186
|
+
};
|