neonctl 1.2.0 → 1.3.0
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/api.js +1 -1
- package/commands/auth.js +4 -1
- package/commands/branches.js +91 -0
- package/commands/index.js +2 -1
- package/commands/projects.js +15 -7
- package/index.js +2 -0
- package/package.json +7 -4
- package/parameters.gen.js +68 -32
- package/writer.js +46 -17
package/api.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { createApiClient } from '@neondatabase/api-client';
|
|
2
|
-
export const getApiClient = ({ apiKey, apiHost }) => createApiClient({ apiKey, baseURL: apiHost });
|
|
2
|
+
export const getApiClient = ({ apiKey, apiHost }) => createApiClient({ apiKey, baseURL: apiHost, timeout: 10000 });
|
|
3
3
|
export const isApiError = (err) => err instanceof Error && 'response' in err;
|
package/commands/auth.js
CHANGED
|
@@ -52,7 +52,10 @@ export const ensureAuth = async (props) => {
|
|
|
52
52
|
const refreshedTokenSet = await refreshToken({
|
|
53
53
|
oauthHost: props.oauthHost,
|
|
54
54
|
clientId: props.clientId,
|
|
55
|
-
}, tokenSet)
|
|
55
|
+
}, tokenSet).catch((e) => {
|
|
56
|
+
log.error('failed to refresh token\n%s', e?.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
56
59
|
props.apiKey = refreshedTokenSet.access_token || 'UNKNOWN';
|
|
57
60
|
props.apiClient = getApiClient({
|
|
58
61
|
apiKey: props.apiKey,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import { writeOut } from '../writer.js';
|
|
4
|
+
import { branchCreateRequest, branchCreateRequestEndpointOptions, branchUpdateRequest, } from '../parameters.gen.js';
|
|
5
|
+
export const command = 'branches';
|
|
6
|
+
export const describe = 'Manage branches';
|
|
7
|
+
export const builder = (argv) => argv
|
|
8
|
+
.demandCommand(1, '')
|
|
9
|
+
.fail(async (_msg, _err, argv) => {
|
|
10
|
+
const y = yargs(hideBin(process.argv));
|
|
11
|
+
if (y.argv._.length === 1) {
|
|
12
|
+
argv.showHelp();
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
.usage('usage: $0 branches <cmd> [args]')
|
|
17
|
+
.options({
|
|
18
|
+
'project.id': {
|
|
19
|
+
describe: 'Project ID',
|
|
20
|
+
type: 'string',
|
|
21
|
+
demandOption: true,
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
.command('list', 'List branches', (yargs) => yargs, async (args) => await list(args))
|
|
25
|
+
.command('create', 'Create a branch', (yargs) => yargs.options({
|
|
26
|
+
...branchCreateRequest,
|
|
27
|
+
...Object.fromEntries(Object.entries(branchCreateRequestEndpointOptions).map(([key, value]) => [`endpoint.${key}`, value])),
|
|
28
|
+
}), async (args) => await create(args))
|
|
29
|
+
.command('update', 'Update a branch', (yargs) => yargs.options(branchUpdateRequest).option('branch.id', {
|
|
30
|
+
describe: 'Branch ID',
|
|
31
|
+
type: 'string',
|
|
32
|
+
demandOption: true,
|
|
33
|
+
}), async (args) => await update(args))
|
|
34
|
+
.command('delete', 'Delete a branch', (yargs) => yargs.option('branch.id', {
|
|
35
|
+
describe: 'Branch ID',
|
|
36
|
+
type: 'string',
|
|
37
|
+
demandOption: true,
|
|
38
|
+
}), async (args) => await deleteBranch(args));
|
|
39
|
+
export const handler = (args) => {
|
|
40
|
+
return args;
|
|
41
|
+
};
|
|
42
|
+
const list = async (props) => {
|
|
43
|
+
const { data } = await props.apiClient.listProjectBranches(props.project.id);
|
|
44
|
+
writeOut(props)(data.branches, {
|
|
45
|
+
fields: ['id', 'name', 'created_at'],
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const create = async (props) => {
|
|
49
|
+
const { data } = await props.apiClient.createProjectBranch(props.project.id, {
|
|
50
|
+
branch: props.branch,
|
|
51
|
+
endpoints: props.endpoint ? [props.endpoint] : undefined,
|
|
52
|
+
});
|
|
53
|
+
writeOut(props)({
|
|
54
|
+
branch: {
|
|
55
|
+
data: data.branch,
|
|
56
|
+
config: { fields: ['id', 'name', 'created_at'] },
|
|
57
|
+
},
|
|
58
|
+
...(data.endpoints?.length > 0
|
|
59
|
+
? {
|
|
60
|
+
endpoints: {
|
|
61
|
+
data: data.endpoints,
|
|
62
|
+
config: { fields: ['id', 'created_at'] },
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
: {}),
|
|
66
|
+
...(data.connection_uris
|
|
67
|
+
? {
|
|
68
|
+
connection_uri: {
|
|
69
|
+
data: data.connection_uris[0],
|
|
70
|
+
config: {
|
|
71
|
+
fields: ['connection_uri'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
: {}),
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const update = async (props) => {
|
|
79
|
+
const { data } = await props.apiClient.updateProjectBranch(props.project.id, props.branch.id, {
|
|
80
|
+
branch: props.branch,
|
|
81
|
+
});
|
|
82
|
+
writeOut(props)(data.branch, {
|
|
83
|
+
fields: ['id', 'name', 'created_at'],
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
const deleteBranch = async (props) => {
|
|
87
|
+
const { data } = await props.apiClient.deleteProjectBranch(props.project.id, props.branch.id);
|
|
88
|
+
writeOut(props)(data.branch, {
|
|
89
|
+
fields: ['id', 'name', 'created_at'],
|
|
90
|
+
});
|
|
91
|
+
};
|
package/commands/index.js
CHANGED
package/commands/projects.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import yargs from 'yargs';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
1
3
|
import { projectCreateRequest } from '../parameters.gen.js';
|
|
2
4
|
import { writeOut } from '../writer.js';
|
|
3
5
|
const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
|
|
4
6
|
export const command = 'projects [command]';
|
|
5
7
|
export const describe = 'Manage projects';
|
|
6
|
-
export const builder = (
|
|
7
|
-
return
|
|
8
|
+
export const builder = (argv) => {
|
|
9
|
+
return argv
|
|
8
10
|
.demandCommand(1, '')
|
|
9
|
-
.fail((
|
|
10
|
-
yargs.
|
|
11
|
-
|
|
11
|
+
.fail((_msg, _err, yyargs) => {
|
|
12
|
+
const y = yargs(hideBin(process.argv));
|
|
13
|
+
if (y.argv._.length === 1) {
|
|
14
|
+
yyargs.showHelp();
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
12
17
|
})
|
|
13
18
|
.usage('usage: $0 projects <cmd> [args]')
|
|
14
19
|
.command('list', 'List projects', (yargs) => yargs, async (args) => {
|
|
@@ -46,11 +51,14 @@ const list = async (props) => {
|
|
|
46
51
|
};
|
|
47
52
|
const create = async (props) => {
|
|
48
53
|
if (props.project == null) {
|
|
54
|
+
props.project = {};
|
|
49
55
|
const inquirer = await import('inquirer');
|
|
50
56
|
const answers = await inquirer.default.prompt([
|
|
51
|
-
{ name: 'name', message: 'Project name', type: 'input' },
|
|
57
|
+
{ name: 'name', message: 'Project name (optional)', type: 'input' },
|
|
52
58
|
]);
|
|
53
|
-
|
|
59
|
+
if (answers.name) {
|
|
60
|
+
props.project = answers;
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
writeOut(props)((await props.apiClient.createProject({
|
|
56
64
|
project: props.project,
|
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import yargs from 'yargs';
|
|
|
2
2
|
import { hideBin } from 'yargs/helpers';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
|
+
import 'axios-debug-log';
|
|
5
6
|
import { ensureAuth } from './commands/auth.js';
|
|
6
7
|
import { defaultDir, ensureConfigDir } from './config.js';
|
|
7
8
|
import { log } from './log.js';
|
|
@@ -15,6 +16,7 @@ const builder = yargs(hideBin(process.argv))
|
|
|
15
16
|
.usage('usage: $0 <cmd> [args]')
|
|
16
17
|
.help()
|
|
17
18
|
.option('output', {
|
|
19
|
+
alias: 'o',
|
|
18
20
|
describe: 'Set output format',
|
|
19
21
|
type: 'string',
|
|
20
22
|
choices: ['json', 'yaml', 'table'],
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git@github.com:neondatabase/neonctl.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.3.0",
|
|
9
9
|
"description": "CLI tool for NeonDB Cloud management",
|
|
10
10
|
"main": "index.js",
|
|
11
11
|
"author": "NeonDB",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"neonctl": "cli.js"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
+
"@apidevtools/swagger-parser": "^10.1.0",
|
|
22
|
+
"@semantic-release/exec": "^6.0.3",
|
|
21
23
|
"@semantic-release/git": "^10.0.1",
|
|
22
24
|
"@types/cli-table": "^0.3.0",
|
|
23
25
|
"@types/inquirer": "^9.0.3",
|
|
@@ -30,13 +32,14 @@
|
|
|
30
32
|
"lint-staged": "^13.0.3",
|
|
31
33
|
"prettier": "^2.7.1",
|
|
32
34
|
"semantic-release": "^21.0.2",
|
|
33
|
-
"ts-morph": "^18.0.0",
|
|
34
35
|
"ts-node": "^10.9.1",
|
|
35
36
|
"typescript": "^4.7.4"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@neondatabase/api-client": "^
|
|
39
|
+
"@neondatabase/api-client": "^1.1.0",
|
|
39
40
|
"axios": "^1.4.0",
|
|
41
|
+
"axios-debug-log": "^1.0.0",
|
|
42
|
+
"chalk": "^5.2.0",
|
|
40
43
|
"cli-table": "^0.3.11",
|
|
41
44
|
"inquirer": "^9.2.6",
|
|
42
45
|
"open": "^8.4.0",
|
|
@@ -53,7 +56,7 @@
|
|
|
53
56
|
"lint": "tsc --noEmit && eslint src --ext .ts",
|
|
54
57
|
"build": "npm run generateParams && npm run clean && tsc && cp src/*.html dist/ && cp package.json ./dist",
|
|
55
58
|
"clean": "rm -rf dist",
|
|
56
|
-
"generateParams": "ts-node --esm
|
|
59
|
+
"generateParams": "ts-node --esm generateOptionsFromSpec.ts",
|
|
57
60
|
"start": "node src/index.js"
|
|
58
61
|
},
|
|
59
62
|
"lint-staged": {
|
package/parameters.gen.js
CHANGED
|
@@ -1,64 +1,100 @@
|
|
|
1
1
|
// FILE IS GENERATED, DO NOT EDIT
|
|
2
2
|
export const projectCreateRequest = {
|
|
3
3
|
'project.settings.quota.active_time_seconds': {
|
|
4
|
-
type:
|
|
5
|
-
description: "The total amount of wall-clock time allowed to be spent by project's compute endpoints
|
|
4
|
+
type: "number",
|
|
5
|
+
description: "The total amount of wall-clock time allowed to be spent by project's compute endpoints.\n",
|
|
6
6
|
},
|
|
7
7
|
'project.settings.quota.compute_time_seconds': {
|
|
8
|
-
type:
|
|
9
|
-
description: "The total amount of CPU seconds allowed to be spent by project's compute endpoints
|
|
8
|
+
type: "number",
|
|
9
|
+
description: "The total amount of CPU seconds allowed to be spent by project's compute endpoints.\n",
|
|
10
10
|
},
|
|
11
11
|
'project.settings.quota.written_data_bytes': {
|
|
12
|
-
type:
|
|
13
|
-
description: "Total amount of data written to all project's branches
|
|
12
|
+
type: "number",
|
|
13
|
+
description: "Total amount of data written to all project's branches.\n",
|
|
14
14
|
},
|
|
15
15
|
'project.settings.quota.data_transfer_bytes': {
|
|
16
|
-
type:
|
|
17
|
-
description: "Total amount of data transferred from all project's branches using proxy
|
|
16
|
+
type: "number",
|
|
17
|
+
description: "Total amount of data transferred from all project's branches using proxy.\n",
|
|
18
18
|
},
|
|
19
19
|
'project.settings.quota.logical_size_bytes': {
|
|
20
|
-
type:
|
|
21
|
-
description: "Limit on the logical size of every project's branch
|
|
20
|
+
type: "number",
|
|
21
|
+
description: "Limit on the logical size of every project's branch.\n",
|
|
22
22
|
},
|
|
23
23
|
'project.name': {
|
|
24
|
-
type:
|
|
24
|
+
type: "string",
|
|
25
25
|
description: "The project name",
|
|
26
26
|
},
|
|
27
27
|
'project.branch.name': {
|
|
28
|
-
type:
|
|
29
|
-
description: "The branch name. If not specified, the default branch name will be used
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "The branch name. If not specified, the default branch name will be used.\n",
|
|
30
30
|
},
|
|
31
31
|
'project.branch.role_name': {
|
|
32
|
-
type:
|
|
33
|
-
description: "The role name. If not specified, the default role name will be used
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "The role name. If not specified, the default role name will be used.\n",
|
|
34
34
|
},
|
|
35
35
|
'project.branch.database_name': {
|
|
36
|
-
type:
|
|
37
|
-
description: "The database name. If not specified, the default database name will be used
|
|
38
|
-
},
|
|
39
|
-
'project.autoscaling_limit_min_cu': {
|
|
40
|
-
type: 'number',
|
|
41
|
-
description: "The minimum number of CPU units",
|
|
42
|
-
},
|
|
43
|
-
'project.autoscaling_limit_max_cu': {
|
|
44
|
-
type: 'number',
|
|
45
|
-
description: "The maximum number of CPU units",
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "The database name. If not specified, the default database name will be used.\n",
|
|
46
38
|
},
|
|
47
39
|
'project.provisioner': {
|
|
48
|
-
type:
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "The Neon compute provisioner.\n",
|
|
49
42
|
choices: ["k8s-pod", "k8s-neonvm", "docker"],
|
|
50
|
-
description: "The Neon compute provisioner.",
|
|
51
43
|
},
|
|
52
44
|
'project.region_id': {
|
|
53
|
-
type:
|
|
54
|
-
description: "The region identifier. See [the documentation](https://neon.tech/docs/introduction/regions) for the list of supported regions
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "The region identifier. See [the documentation](https://neon.tech/docs/introduction/regions) for the list of supported regions.\n",
|
|
55
47
|
},
|
|
56
48
|
'project.pg_version': {
|
|
57
|
-
type:
|
|
49
|
+
type: "number",
|
|
58
50
|
description: "The major PostgreSQL version number. Currently supported version are `14` and `15`.",
|
|
59
51
|
},
|
|
60
52
|
'project.store_passwords': {
|
|
61
|
-
type:
|
|
62
|
-
description: "Whether or not passwords are stored for roles in the Neon project. Storing passwords facilitates access to Neon features that require authorization
|
|
53
|
+
type: "boolean",
|
|
54
|
+
description: "Whether or not passwords are stored for roles in the Neon project. Storing passwords facilitates access to Neon features that require authorization.\n",
|
|
55
|
+
},
|
|
56
|
+
'project.history_retention_seconds': {
|
|
57
|
+
type: "number",
|
|
58
|
+
description: "The number of seconds to retain PITR backup history for this project. Defaults to 7 days\n",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
export const branchCreateRequest = {
|
|
62
|
+
'branch.parent_id': {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The `branch_id` of the parent branch\n",
|
|
65
|
+
},
|
|
66
|
+
'branch.name': {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "The branch name\n",
|
|
69
|
+
},
|
|
70
|
+
'branch.parent_lsn': {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "A Log Sequence Number (LSN) on the parent branch. The branch will be created with data from this LSN.\n",
|
|
73
|
+
},
|
|
74
|
+
'branch.parent_timestamp': {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "A timestamp identifying a point in time on the parent branch. The branch will be created with data starting from this point in time.\n",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export const branchCreateRequestEndpointOptions = {
|
|
80
|
+
'type': {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "The compute endpoint type. Either `read_write` or `read_only`.\nThe `read_only` compute endpoint type is not yet supported.\n",
|
|
83
|
+
choices: ["read_only", "read_write"],
|
|
84
|
+
},
|
|
85
|
+
'provisioner': {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "The Neon compute provisioner.\n",
|
|
88
|
+
choices: ["k8s-pod", "k8s-neonvm", "docker"],
|
|
89
|
+
},
|
|
90
|
+
'suspend_timeout_seconds': {
|
|
91
|
+
type: "number",
|
|
92
|
+
description: "Duration of inactivity in seconds after which endpoint will be\nautomatically suspended. Value `0` means use global default,\n`-1` means never suspend. Maximum value is 1 week in seconds.\n",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
export const branchUpdateRequest = {
|
|
96
|
+
'branch.name': {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: undefined,
|
|
63
99
|
},
|
|
64
100
|
};
|
package/writer.js
CHANGED
|
@@ -1,33 +1,62 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import Table from 'cli-table';
|
|
3
|
+
import chalk from 'chalk';
|
|
3
4
|
// Allow PIPE to finish reading before the end of the output.
|
|
4
5
|
process.stdout.on('error', function (err) {
|
|
5
6
|
if (err.code == 'EPIPE') {
|
|
6
7
|
process.exit(0);
|
|
7
8
|
}
|
|
8
9
|
});
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
*
|
|
12
|
+
* Parses the output format, takes data and writes the output to stdout.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // to output single data
|
|
16
|
+
* const { data } = await props.apiClient.listProjectBranches(props.project.id);
|
|
17
|
+
* writeOut(props)(data, { fields: ['id', 'name', 'created_at'] })
|
|
18
|
+
* // to output multiple data
|
|
19
|
+
* writeOut(props)({
|
|
20
|
+
* branches: {
|
|
21
|
+
* data,
|
|
22
|
+
* config: { fields: ['id', 'name', 'created_at'] }
|
|
23
|
+
* },
|
|
24
|
+
* endpoints: {
|
|
25
|
+
* data,
|
|
26
|
+
* config: { fields: ['id', 'created_at'] }
|
|
27
|
+
* }
|
|
28
|
+
* })
|
|
29
|
+
*/
|
|
30
|
+
export const writeOut = (props) => (arg1, arg2) => {
|
|
10
31
|
if (props.output == 'yaml') {
|
|
11
|
-
process.stdout.write(YAML.stringify(
|
|
32
|
+
process.stdout.write(YAML.stringify(arg2 === undefined
|
|
33
|
+
? Object.fromEntries(Object.entries(arg1).map(([k, v]) => [k, v.data]))
|
|
34
|
+
: arg1, null, 2));
|
|
12
35
|
return;
|
|
13
36
|
}
|
|
14
37
|
if (props.output == 'json') {
|
|
15
|
-
process.stdout.write(JSON.stringify(
|
|
38
|
+
process.stdout.write(JSON.stringify(arg2 === undefined
|
|
39
|
+
? Object.fromEntries(Object.entries(arg1).map(([k, v]) => [k, v.data]))
|
|
40
|
+
: arg1, null, 2));
|
|
16
41
|
return;
|
|
17
42
|
}
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.map((
|
|
26
|
-
|
|
43
|
+
const options = arg2 === undefined ? arg1 : { '': { data: arg1, config: arg2 } };
|
|
44
|
+
Object.entries(options).forEach(([key, { data, config }]) => {
|
|
45
|
+
const arrayData = Array.isArray(data) ? data : [data];
|
|
46
|
+
const table = new Table({
|
|
47
|
+
style: {
|
|
48
|
+
head: ['green'],
|
|
49
|
+
},
|
|
50
|
+
head: config.fields.map((field) => field
|
|
51
|
+
.split('_')
|
|
52
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
53
|
+
.join(' ')),
|
|
54
|
+
});
|
|
55
|
+
arrayData.forEach((item) => {
|
|
56
|
+
table.push(config.fields.map((field) => item[field] ?? ''));
|
|
57
|
+
});
|
|
58
|
+
process.stdout.write(chalk.bold(key) + '\n');
|
|
59
|
+
process.stdout.write(table.toString());
|
|
60
|
+
process.stdout.write('\n');
|
|
27
61
|
});
|
|
28
|
-
arrayData.forEach((item) => {
|
|
29
|
-
table.push(config.fields.map((field) => item[field] ?? ''));
|
|
30
|
-
});
|
|
31
|
-
process.stdout.write(table.toString());
|
|
32
|
-
process.stdout.write('\n');
|
|
33
62
|
};
|