@reshapr/reshapr-cli 0.0.1
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/README.md +44 -0
- package/dist/cli.js +96 -0
- package/dist/commands/api-token.js +123 -0
- package/dist/commands/attach.js +72 -0
- package/dist/commands/config.js +387 -0
- package/dist/commands/expo.js +222 -0
- package/dist/commands/gateway-group.js +108 -0
- package/dist/commands/import.js +230 -0
- package/dist/commands/index.js +27 -0
- package/dist/commands/info.js +47 -0
- package/dist/commands/login.js +164 -0
- package/dist/commands/logout.js +25 -0
- package/dist/commands/quotas.js +60 -0
- package/dist/commands/secret.js +280 -0
- package/dist/commands/service.js +145 -0
- package/dist/constants.js +17 -0
- package/dist/utils/age.js +49 -0
- package/dist/utils/config.js +80 -0
- package/dist/utils/context.js +36 -0
- package/dist/utils/editor.js +70 -0
- package/dist/utils/format.js +21 -0
- package/dist/utils/logger.js +53 -0
- package/dist/version.js +1 -0
- package/package.json +43 -0
- package/src/cli.ts +104 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright The Reshapr Authors.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { program } from "commander";
|
|
17
|
+
import { Logger } from "../utils/logger.js";
|
|
18
|
+
import { ConfigUtil } from "../utils/config.js";
|
|
19
|
+
import { Context } from "../utils/context.js";
|
|
20
|
+
import { CLI_LABEL } from '../constants.js';
|
|
21
|
+
export const gatewayGroupCommand = program.command('gateway-group')
|
|
22
|
+
.description(`Manage gateway groups in ${CLI_LABEL}`);
|
|
23
|
+
/* List all gateway groups */
|
|
24
|
+
gatewayGroupCommand.command('list')
|
|
25
|
+
.description('List all gateway groups')
|
|
26
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/gatewayGroups`, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
headers: {
|
|
31
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
Logger.error('Fetching gateway groups failed: ' + response.statusText);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
if (data.length === 0) {
|
|
40
|
+
Logger.info('No gateway groups found.');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
Context.put('gatewayGroups', data);
|
|
44
|
+
const longestName = longestGroupName(data) + 1; // +1 for padding
|
|
45
|
+
const longestGroupOrganization = longestGroupOrganizationName(data) + 1; // +1 for padding
|
|
46
|
+
Logger.log(`${'ID'.padEnd(13, ' ')} ${'ORG'.padEnd(longestGroupOrganization, ' ')} ${'NAME'.padEnd(longestName, ' ')} ${'LABELS'.padEnd(60, ' ')}`);
|
|
47
|
+
data.forEach((group) => {
|
|
48
|
+
Logger.log(`${group.id.padEnd(13, ' ')} ${group.organizationId.padEnd(longestGroupOrganization, ' ')} ${group.name.padEnd(longestName, ' ')} ${JSON.stringify(group.labels).padEnd(60, ' ')}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
function longestGroupName(groups) {
|
|
53
|
+
return groups.reduce((max, group) => {
|
|
54
|
+
return Math.max(max, group.name.length);
|
|
55
|
+
}, 0);
|
|
56
|
+
}
|
|
57
|
+
function longestGroupOrganizationName(groups) {
|
|
58
|
+
return groups.reduce((max, group) => {
|
|
59
|
+
return Math.max(max, group.organizationId ? group.organizationId.length : 0);
|
|
60
|
+
}, 0);
|
|
61
|
+
}
|
|
62
|
+
/** Create a new gateway group */
|
|
63
|
+
gatewayGroupCommand.command('create <name>')
|
|
64
|
+
.description('Create a new gateway group')
|
|
65
|
+
.option('-l, --labels <labels>', 'JSON map of key-values labels for the gateway group')
|
|
66
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
67
|
+
.action(async (name, options) => {
|
|
68
|
+
let labels = options.labels ? JSON.parse(options.labels) : {};
|
|
69
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/gatewayGroups`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`,
|
|
73
|
+
'Content-Type': 'application/json'
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
name: name,
|
|
77
|
+
labels: labels
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
if (response.status === 429) {
|
|
82
|
+
Logger.error('Gateway group creation quota exceeded. Check your quotas.');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
Logger.error('Creating gateway group failed: ' + response.statusText);
|
|
86
|
+
}
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
Logger.success(`Gateway group '${data.name}' created successfully with ID: ${data.id}`);
|
|
91
|
+
Context.put('gatewayGroup', data);
|
|
92
|
+
});
|
|
93
|
+
/** Delete a gateway group by ID */
|
|
94
|
+
gatewayGroupCommand.command('delete <id>')
|
|
95
|
+
.description('Delete a gateway group by ID')
|
|
96
|
+
.action(async (id) => {
|
|
97
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/gatewayGroups/${id}`, {
|
|
98
|
+
method: 'DELETE',
|
|
99
|
+
headers: {
|
|
100
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
Logger.error('Deleting gateway group failed: ' + response.statusText);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
Logger.success(`Gateway group with ID '${id}' deleted successfully.`);
|
|
108
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright The Reshapr Authors.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import yoctoSpinner from 'yocto-spinner';
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
import { Logger } from "../utils/logger.js";
|
|
20
|
+
import { ConfigUtil } from "../utils/config.js";
|
|
21
|
+
import { formatEndpoint } from "../utils/format.js";
|
|
22
|
+
import { Context } from "../utils/context.js";
|
|
23
|
+
import { CLI_LABEL } from '../constants.js';
|
|
24
|
+
const DEFAULT_GATEWAY_GROUP_ID = '1'; // Default Gateway Group ID, can be changed later
|
|
25
|
+
export const importCommand = new Command('import')
|
|
26
|
+
.description(`Import an artifact into ${CLI_LABEL}`)
|
|
27
|
+
.option('-f, --file <file>', 'Path to the artifact file to import')
|
|
28
|
+
.option('-u, --url <url>', 'URL of the artifact to import')
|
|
29
|
+
.option('-s, --secret <artifactSecret>', 'Use a secret to authenticate the artifact to import')
|
|
30
|
+
.option('--sn, --serviceName <name>', 'Set the service name (mandatory for GraphQL schema imports)')
|
|
31
|
+
.option('--sv, --serviceVersion <version>', 'Set the service version (mandatory for GraphQL schema imports)')
|
|
32
|
+
.option('--io, --includedOperations [<operation1>, <operation2>]', 'Include these operations when importing service artifact (JSON array). Takes precedence over excludedOperations.')
|
|
33
|
+
.option('--eo, --excludedOperations [<operation1>, <operation2>]', 'Exclude these operations when importing service artifact (JSON array). Only considered if no includedOperations.')
|
|
34
|
+
.option('--be, --backendEndpoint <backendEndpointURL>', 'Directly expose the artifact on a Gateway using a backend endpoint')
|
|
35
|
+
.option('--bs, --backendSecret <backendSecretId>', 'ID of a secret to authenticate exposed MCP with backend endpoint')
|
|
36
|
+
.option('--apiKey', 'Generate an API key for the configuration plan to secure the MCP endpoint')
|
|
37
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
if (!options.file && !options.url) {
|
|
40
|
+
Logger.error('You must provide either a file path or a URL to import.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
let body;
|
|
44
|
+
if (options.file) {
|
|
45
|
+
if (!fs.existsSync(options.file)) {
|
|
46
|
+
Logger.error(`File not found: ${options.file}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
// We should encode in multipart/form-data
|
|
50
|
+
body = new FormData();
|
|
51
|
+
body.append('file', new Blob([fs.readFileSync(options.file)]), options.file.split('/').pop());
|
|
52
|
+
body.append('mainArtifact', 'true');
|
|
53
|
+
}
|
|
54
|
+
else if (options.url) {
|
|
55
|
+
// We should encode in application/x-www-form-urlencoded
|
|
56
|
+
body = new URLSearchParams();
|
|
57
|
+
body.append('url', options.url);
|
|
58
|
+
body.append('mainArtifact', 'true');
|
|
59
|
+
if (options.secret) {
|
|
60
|
+
body.append('secretName', options.secret);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (options.serviceName) {
|
|
64
|
+
if (!options.serviceVersion || options.serviceName.trim() === '') {
|
|
65
|
+
Logger.error('Service version cannot be empty when service name is provided.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
body.append('serviceName', options.serviceName);
|
|
69
|
+
}
|
|
70
|
+
if (options.serviceVersion) {
|
|
71
|
+
if (!options.serviceName || options.serviceName.trim() === '') {
|
|
72
|
+
Logger.error('Service name cannot be empty when service version is provided.');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
body.append('serviceVersion', options.serviceVersion);
|
|
76
|
+
}
|
|
77
|
+
if (options.includedOperations) {
|
|
78
|
+
let operations = getArrayOfStrings(options.includedOperations, 'includedOperations');
|
|
79
|
+
for (const op of operations) {
|
|
80
|
+
body.append('includedOperations', op);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!options.includedOperations && options.excludedOperations) {
|
|
84
|
+
let operations = getArrayOfStrings(options.excludedOperations, 'excludedOperations');
|
|
85
|
+
for (const op of operations) {
|
|
86
|
+
body.append('excludedOperations', op);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const spinner = yoctoSpinner({ text: 'Importing artifact...' }).start();
|
|
90
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/artifacts`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`,
|
|
94
|
+
},
|
|
95
|
+
body: body
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
Logger.error('Import failed: ' + response.statusText);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json().catch(err => {
|
|
102
|
+
Logger.error('Failed to parse response: ' + err.message);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
spinner.stop();
|
|
106
|
+
Context.put('service', data);
|
|
107
|
+
Logger.success('Import successful!');
|
|
108
|
+
Logger.info(`Discovered Service ${data.name} with ID: ${data.id}`);
|
|
109
|
+
if (options.backendEndpoint) {
|
|
110
|
+
await exposeService(options, data);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
async function exposeService(options, service) {
|
|
114
|
+
if (options.backendEndpoint) {
|
|
115
|
+
const backendEndpoint = options.backendEndpoint;
|
|
116
|
+
// First create a Configuration Plan.
|
|
117
|
+
const planResponse = await fetch(`${ConfigUtil.config.server}/api/v1/configurationPlans`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`,
|
|
121
|
+
'Content-Type': 'application/json'
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
serviceId: service.id,
|
|
125
|
+
name: `default-plan for ${service.name}`,
|
|
126
|
+
description: `Configuration plan for ${service.name} on ${backendEndpoint}`,
|
|
127
|
+
backendEndpoint: options.backendEndpoint,
|
|
128
|
+
backendSecretId: options.backendSecret || undefined,
|
|
129
|
+
apiKey: (options.apiKey ? 'generate-me' : undefined),
|
|
130
|
+
initialAccessToken: (options.internalOAuth2 ? 'generate-me' : undefined)
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
if (!planResponse.ok) {
|
|
134
|
+
Logger.error('Failed to create a Config Plan for service: ' + planResponse.statusText);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const planData = await planResponse.json();
|
|
138
|
+
Context.put('configurationPlan', planData);
|
|
139
|
+
if (options.apiKey) {
|
|
140
|
+
Logger.warn(`The API Key to access future expositions is: ${planData.apiKey}`);
|
|
141
|
+
Logger.warn('Make sure to store it securely, as it will not be shown again.');
|
|
142
|
+
}
|
|
143
|
+
// Then expose the config plan on the default Gateway Group.
|
|
144
|
+
const exposeResponse = await fetch(`${ConfigUtil.config.server}/api/v1/expositions`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {
|
|
147
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`,
|
|
148
|
+
'Content-Type': 'application/json'
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
gatewayGroupId: DEFAULT_GATEWAY_GROUP_ID,
|
|
152
|
+
configurationPlanId: planData.id
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
if (!exposeResponse.ok) {
|
|
156
|
+
Logger.error('Failed to expose configuration: ' + exposeResponse.statusText);
|
|
157
|
+
if (exposeResponse.status === 429) {
|
|
158
|
+
Logger.error('Exposition creation quota exceeded. Check your quotas.');
|
|
159
|
+
}
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
const exposeData = await exposeResponse.json().catch(err => {
|
|
163
|
+
Logger.error('Failed to parse exposition response: ' + err.message);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
166
|
+
Logger.success('Exposition done!');
|
|
167
|
+
Context.put('exposition', exposeData);
|
|
168
|
+
await getActiveExposition(exposeData);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function getActiveExposition(exposition) {
|
|
172
|
+
const activeResponse = await fetch(`${ConfigUtil.config.server}/api/v1/expositions/active/${exposition.id}`, {
|
|
173
|
+
method: 'GET',
|
|
174
|
+
headers: {
|
|
175
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
if (activeResponse.status === 404) {
|
|
179
|
+
Logger.warn(`No active exposition found for the Exposition ${exposition.id}. Maybe there's no running Gateway at the moment?`);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
if (!activeResponse.ok) {
|
|
183
|
+
Logger.error('Failed to retrieve active exposition: ' + activeResponse.statusText);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const data = await activeResponse.json().catch(err => {
|
|
187
|
+
Logger.error('Failed to parse active exposition response: ' + err.message);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|
|
190
|
+
Logger.success('Exposition is now active!');
|
|
191
|
+
Logger.log(`Exposition ID : ${data.id}`);
|
|
192
|
+
Logger.log(`Organization : ${data.organizationId}`);
|
|
193
|
+
Logger.log(`Created on : ${data.createdOn}`);
|
|
194
|
+
Logger.log(`Service ID : ${data.service.id}`);
|
|
195
|
+
Logger.log(`Service Name : ${data.service.name}`);
|
|
196
|
+
Logger.log(`Service Version: ${data.service.version}`);
|
|
197
|
+
Logger.log(`Service Type : ${data.service.type} -> ${data.configurationPlan.backendEndpoint}`);
|
|
198
|
+
let allFqdns = uniqueFQDNs(data.gateways);
|
|
199
|
+
Context.put('endpoints', uniqueFQDNs(data.gateways).map(fqdn => formatEndpoint(fqdn, exposition.organizationId, exposition.service.name, exposition.service.version)));
|
|
200
|
+
Logger.log(`Endpoints : ${allFqdns.map((fqdn) => formatEndpoint(fqdn, data.organizationId, data.service.name, data.service.version))
|
|
201
|
+
.join(', ')}`);
|
|
202
|
+
}
|
|
203
|
+
function uniqueFQDNs(gateways) {
|
|
204
|
+
let allFqdns = [];
|
|
205
|
+
gateways.forEach(gateway => {
|
|
206
|
+
gateway.fqdns.filter(fqdn => !allFqdns.includes(fqdn)).forEach(fqdn => allFqdns.push(fqdn));
|
|
207
|
+
});
|
|
208
|
+
return allFqdns;
|
|
209
|
+
}
|
|
210
|
+
function getArrayOfStrings(input, name) {
|
|
211
|
+
if (Array.isArray(input)) {
|
|
212
|
+
return input;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(input);
|
|
217
|
+
if (Array.isArray(parsed)) {
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
throw new Error('Not an array');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
Logger.error(`Input must be a JSON array of strings for ${name}.`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright The Reshapr Authors.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
export { loginCommand } from './login.js';
|
|
17
|
+
export { infoCommand } from './info.js';
|
|
18
|
+
export { logoutCommand } from './logout.js';
|
|
19
|
+
export { importCommand } from './import.js';
|
|
20
|
+
export { attachCommand } from './attach.js';
|
|
21
|
+
export { serviceCommand } from './service.js';
|
|
22
|
+
export { secretCommand } from './secret.js';
|
|
23
|
+
export { expoCommand } from './expo.js';
|
|
24
|
+
export { configCommand } from './config.js';
|
|
25
|
+
export { gatewayGroupCommand } from './gateway-group.js';
|
|
26
|
+
export { tokenCommand } from './api-token.js';
|
|
27
|
+
export { quotasCommand } from './quotas.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright The Reshapr Authors.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import { Logger } from "../utils/logger.js";
|
|
18
|
+
import { ConfigUtil } from "../utils/config.js";
|
|
19
|
+
import { CLI_LABEL } from '../constants.js';
|
|
20
|
+
export const infoCommand = new Command('info')
|
|
21
|
+
.description(`Display information about current context and the ${CLI_LABEL} Server`)
|
|
22
|
+
.action(async () => {
|
|
23
|
+
Logger.info('User Information');
|
|
24
|
+
console.log(` User : ${ConfigUtil.config.username}`);
|
|
25
|
+
console.log(` Organization: ${ConfigUtil.config.org}`);
|
|
26
|
+
console.log(` Server : ${ConfigUtil.config.server}`);
|
|
27
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/config`, {
|
|
28
|
+
method: 'GET'
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
Logger.error('Failed to fetch server information: ' + response.statusText);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
response.json().then(async (data) => {
|
|
35
|
+
Logger.info('Server Information');
|
|
36
|
+
console.log(` Version : ${data.version}`);
|
|
37
|
+
console.log(` Build time : ${data.buildTimestamp}`);
|
|
38
|
+
console.log(` Mode : ${data.mode}`);
|
|
39
|
+
console.log(` Internal IDP: ${data.internalIDPUrl}`);
|
|
40
|
+
if (data.authenticationConfig && data.authenticationConfig.enabled) {
|
|
41
|
+
console.log(` OAuth2 IDP : ${data.authenticationConfig.url}/${data.authenticationConfig.realm}`);
|
|
42
|
+
}
|
|
43
|
+
}).catch(err => {
|
|
44
|
+
Logger.error('Failed to parse server information: ' + err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright The Reshapr Authors.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import http from 'http';
|
|
17
|
+
import url from 'url';
|
|
18
|
+
import open from 'open';
|
|
19
|
+
import getPort, { portNumbers } from 'get-port';
|
|
20
|
+
import { Command } from "commander";
|
|
21
|
+
import inquirer from 'inquirer';
|
|
22
|
+
import { Logger } from "../utils/logger.js";
|
|
23
|
+
import { ConfigUtil } from '../utils/config.js';
|
|
24
|
+
import { CLI_LABEL } from '../constants.js';
|
|
25
|
+
export const loginCommand = new Command('login')
|
|
26
|
+
.description(`Login to ${CLI_LABEL}`)
|
|
27
|
+
.option('-u, --username <username>', `Your ${CLI_LABEL} username`)
|
|
28
|
+
.option('-p, --password <password>', `Your ${CLI_LABEL} password`)
|
|
29
|
+
.option('-t, --token <token>', `Your ${CLI_LABEL} authentication token`)
|
|
30
|
+
.option('-o, --org <org>', `Your ${CLI_LABEL} organization name`)
|
|
31
|
+
.option('-s, --server <server>', `Your ${CLI_LABEL} Control Plane URL`, 'https://app.resphar.io')
|
|
32
|
+
.option('-k, --insecure', 'Skip SSL certificate validation')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
// First validate server URL and fetch server configuration.
|
|
35
|
+
const configResponse = await fetch(`${options.server}/api/config`, {
|
|
36
|
+
method: 'GET'
|
|
37
|
+
}).catch(err => {
|
|
38
|
+
Logger.error('Failed to connect to the server. Check URL.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
const configData = await configResponse.json().catch(err => {
|
|
42
|
+
Logger.error('Failed to parse server configuration: ' + err.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
if (configData.mode === 'on-premises') {
|
|
46
|
+
await handleOnPremisesLogin(options);
|
|
47
|
+
//await handleSaaSLogin(options);
|
|
48
|
+
}
|
|
49
|
+
else if (configData.mode === 'saas') {
|
|
50
|
+
await handleSaaSLogin(options);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
async function handleOnPremisesLogin(options) {
|
|
54
|
+
// Handle on-premises login logic here if needed.
|
|
55
|
+
if (!options.username) {
|
|
56
|
+
const username = await inquirer.prompt({
|
|
57
|
+
type: 'input',
|
|
58
|
+
name: 'username',
|
|
59
|
+
message: `Enter your ${CLI_LABEL} username:`,
|
|
60
|
+
validate: (input) => {
|
|
61
|
+
if (!input) {
|
|
62
|
+
return 'Username is required';
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
options.username = username.username;
|
|
68
|
+
}
|
|
69
|
+
if (!options.password) {
|
|
70
|
+
const password = await inquirer.prompt({
|
|
71
|
+
type: 'password',
|
|
72
|
+
name: 'password',
|
|
73
|
+
message: `Enter your ${CLI_LABEL} password:`,
|
|
74
|
+
validate: (input) => {
|
|
75
|
+
if (!input) {
|
|
76
|
+
return 'Password is required';
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
options.password = password.password;
|
|
82
|
+
}
|
|
83
|
+
// Here you call a login function to authenticate the user.
|
|
84
|
+
Logger.info(`Logging in to ${CLI_LABEL} at ${options.server}...`);
|
|
85
|
+
const response = await fetch(`${options.server}/auth/login/reshapr`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json'
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
username: options.username,
|
|
92
|
+
password: options.password
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
Logger.error('Login failed: ' + response.statusText);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
response.text().then(data => {
|
|
100
|
+
Logger.success('Login successful!');
|
|
101
|
+
Logger.info(`Welcome, ${options.username}!`);
|
|
102
|
+
// Here you would typically save the authentication token or session.
|
|
103
|
+
let config = {
|
|
104
|
+
username: options.username,
|
|
105
|
+
server: options.server,
|
|
106
|
+
insecure: options.insecure,
|
|
107
|
+
token: data // Assuming the response contains a token.
|
|
108
|
+
};
|
|
109
|
+
ConfigUtil.writeConfig(config);
|
|
110
|
+
}).catch(err => {
|
|
111
|
+
Logger.error('Error parsing response: ' + err.message);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function handleSaaSLogin(options) {
|
|
116
|
+
// Prepare a token for reception.
|
|
117
|
+
let token = null;
|
|
118
|
+
// Start starting a lightweight web server to handle OAuth2 login.
|
|
119
|
+
const server = http.createServer((req, res) => {
|
|
120
|
+
// Handle Authentication callback here. Parse the URL.
|
|
121
|
+
const parsedUrl = url.parse(req.url || '', true);
|
|
122
|
+
// Get query parts of the URL.
|
|
123
|
+
const query = parsedUrl.query;
|
|
124
|
+
if (query.token && query.token.length > 0) {
|
|
125
|
+
token = query.token;
|
|
126
|
+
Logger.success('Login successful!');
|
|
127
|
+
const tokenPayload = token.split('.')[1];
|
|
128
|
+
const decodedPayload = Buffer.from(tokenPayload, 'base64').toString('utf8');
|
|
129
|
+
const username = JSON.parse(decodedPayload).sub || 'unknown';
|
|
130
|
+
Logger.info(`Welcome, ${username}!`);
|
|
131
|
+
// Here you would typically save the authentication token or session.
|
|
132
|
+
let config = {
|
|
133
|
+
username: username,
|
|
134
|
+
server: options.server,
|
|
135
|
+
insecure: options.insecure,
|
|
136
|
+
token: token
|
|
137
|
+
};
|
|
138
|
+
ConfigUtil.writeConfig(config);
|
|
139
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
140
|
+
res.end('Login successful! You can close this window now.');
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
Logger.error('Login failed: No token received.');
|
|
144
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
145
|
+
res.end(`Login failed: No token received. Please login on ${options.server} before trying again.`);
|
|
146
|
+
}
|
|
147
|
+
process.exit(0);
|
|
148
|
+
});
|
|
149
|
+
server.on('error', (err) => {
|
|
150
|
+
Logger.error('Failed to start server for OAuth2 login: ' + err.message);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
153
|
+
const localPort = await getPort({ port: portNumbers(5556, 5599) });
|
|
154
|
+
server.listen(localPort, () => {
|
|
155
|
+
Logger.info(`Listening for OAuth2 callback on http://localhost:${localPort}`);
|
|
156
|
+
});
|
|
157
|
+
// Opens the URL in the default browser.
|
|
158
|
+
await open(`${options.server}/auth/login/saas?redirect_uri=http://localhost:${localPort}`, {
|
|
159
|
+
wait: true
|
|
160
|
+
});
|
|
161
|
+
server.close(() => {
|
|
162
|
+
Logger.info('Server closed after handling OAuth2 callback.');
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright The Reshapr Authors.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import { Logger } from "../utils/logger.js";
|
|
18
|
+
import { ConfigUtil } from "../utils/config.js";
|
|
19
|
+
import { CLI_LABEL } from '../constants.js';
|
|
20
|
+
export const logoutCommand = new Command('logout')
|
|
21
|
+
.description(`Logout from ${CLI_LABEL}`)
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
ConfigUtil.deleteConfig();
|
|
24
|
+
Logger.success('You have been logged out successfully.');
|
|
25
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright The Reshapr Authors.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import { Logger } from "../utils/logger.js";
|
|
18
|
+
import { ConfigUtil } from "../utils/config.js";
|
|
19
|
+
import { Context } from "../utils/context.js";
|
|
20
|
+
import { CLI_LABEL } from '../constants.js';
|
|
21
|
+
export const quotasCommand = new Command('quotas')
|
|
22
|
+
.description(`List and check your ${CLI_LABEL} quotas`)
|
|
23
|
+
.option('-o, --output <format>', 'Output format (json, yaml)')
|
|
24
|
+
.action(async () => {
|
|
25
|
+
const response = await fetch(`${ConfigUtil.config.server}/api/v1/quotas`, {
|
|
26
|
+
method: 'GET',
|
|
27
|
+
headers: {
|
|
28
|
+
'Authorization': `Bearer ${ConfigUtil.config.token}`
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
Logger.error('Fetching quotas failed: ' + response.statusText);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const data = await response.json().catch(err => {
|
|
36
|
+
Logger.error('Error parsing quotas response: ' + err.message);
|
|
37
|
+
});
|
|
38
|
+
if (data.length === 0) {
|
|
39
|
+
Logger.info('No quotas found.');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
Context.put('quotas', data);
|
|
43
|
+
const longestMetric = longestQuotaMetric(data) + 1; // +1 for padding
|
|
44
|
+
const longestOrganization = longestQuotaOrganization(data) + 1;
|
|
45
|
+
Logger.log(`${'ORG'.padEnd(longestOrganization, ' ')} ${'METRIC'.padEnd(longestMetric, ' ')} ${'ENABLED'.padEnd(8, ' ')} ${'LIMIT'.padEnd(6, ' ')} ${'REMAINING'.padEnd(10, ' ')}`);
|
|
46
|
+
data.forEach((quota) => {
|
|
47
|
+
Logger.log(`${quota.organizationId.padEnd(longestOrganization, ' ')} ${quota.metric.padEnd(longestMetric, ' ')} ${(quota.enabled ? 'Y' : 'N').padEnd(8, ' ')} ${quota.limit.toString().padEnd(6, ' ')} ${quota.remaining.toString().padEnd(10, ' ')}`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const longestQuotaMetric = (quotas) => {
|
|
52
|
+
return quotas.reduce((max, quota) => {
|
|
53
|
+
return Math.max(max, quota.metric.length);
|
|
54
|
+
}, 0);
|
|
55
|
+
};
|
|
56
|
+
const longestQuotaOrganization = (quotas) => {
|
|
57
|
+
return quotas.reduce((max, quota) => {
|
|
58
|
+
return Math.max(max, quota.organizationId.length);
|
|
59
|
+
}, 0);
|
|
60
|
+
};
|