@liara/cli
Version:
The command line interface for Liara
270 lines (268 loc) • 10.4 kB
JavaScript
import os from 'node:os';
import path from 'node:path';
import { createServer } from 'node:http';
import { fileURLToPath } from 'node:url';
import fs from 'fs-extra';
import WebSocket from 'ws';
import ora from 'ora';
import inquirer from 'inquirer';
import got from 'got';
import open, { apps } from 'open';
import { Command, Flags } from '@oclif/core';
import updateNotifier from 'update-notifier';
import getPort, { portNumbers } from 'get-port';
import { HttpsProxyAgent } from 'https-proxy-agent';
import hooks from './interceptors.js';
import browserLoginHeader from './utils/browser-login-header.js';
import { DEV_MODE, REGIONS_API_URL, FALLBACK_REGION, GLOBAL_CONF_PATH, GLOBAL_CONF_VERSION, } from './constants.js';
import { NoVMsFoundError } from './errors/vm-error.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageJson = fs.readJSONSync(path.join(__dirname, '..', 'package.json'));
updateNotifier({ pkg: packageJson }).notify({ isGlobal: true });
const isWin = os.platform() === 'win32';
class default_1 extends Command {
constructor() {
super(...arguments);
this.got = got.extend();
}
async readGlobalConfig() {
const content = fs.readJSONSync(GLOBAL_CONF_PATH, { throws: false }) || {
version: GLOBAL_CONF_VERSION,
accounts: {},
};
return content;
}
async catch(error) {
if (error.response && error.response.statusCode === 401) {
throw new Error(`Authentication failed.
Please log in using the 'liara login' command.
If you are using an API token for authentication, please consider updating your API token.
`);
}
if (error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET') {
this.error(`Could not connect to ${(error.config && error.config.baseURL) || 'https://api.liara.ir'}.
Please check your network connection.`);
}
if (error.oclif && error.oclif.exit === 0)
return;
this.error(error.message);
}
async setGotConfig(config) {
const gotConfig = {
headers: {
'User-Agent': this.config.userAgent,
},
timeout: {
request: (config.image ? 25 : 10) * 1000,
},
hooks: {
beforeRequest: [
(options) => {
if (options.url) {
options.url.searchParams.set('teamID', config['team-id'] || '');
}
},
],
},
};
const proxy = process.env.http_proxy || process.env.https_proxy;
if (proxy && !isWin) {
this.log(`Using proxy server ${proxy}`);
const agent = new HttpsProxyAgent(proxy);
gotConfig.agent = { https: agent };
}
if (!config['api-token'] || !config.region) {
const { api_token, region } = config.account
? await this.getAccount(config.account)
: await this.getCurrentAccount();
config['api-token'] = config['api-token'] || api_token;
config.region = config.region || region;
}
// @ts-ignore
gotConfig.headers.Authorization = `Bearer ${config['api-token']}`;
config.region = config.region || FALLBACK_REGION;
const actualBaseURL = REGIONS_API_URL[config.region];
gotConfig.prefixUrl = DEV_MODE ? 'http://localhost:3000' : actualBaseURL;
if (DEV_MODE) {
this.log(`[dev] The actual base url is: ${actualBaseURL}`);
this.log(`[dev] but in dev mode we use http://localhost:3000`);
}
this.got = got.extend({ hooks, ...gotConfig });
}
createProxiedWebsocket(endpoint) {
const proxy = process.env.http_proxy || process.env.https_proxy;
if (proxy && !isWin) {
const agent = new HttpsProxyAgent(proxy);
return new WebSocket(endpoint, { agent });
}
return new WebSocket(endpoint);
}
async promptProject() {
this.spinner = ora();
this.spinner.start('Loading...');
try {
const { projects } = await this.got('v1/projects').json();
this.spinner.stop();
if (!projects.length) {
this.warn('Please go to https://console.liara.ir/apps and create an app, first.');
this.exit(1);
}
const { project } = (await inquirer.prompt({
name: 'project',
type: 'list',
message: 'Please select an app:',
choices: [...projects.map((project) => project.project_id)],
}));
return project;
}
catch (error) {
this.spinner.stop();
throw error;
}
}
async getCurrentAccount() {
const accounts = (await this.readGlobalConfig()).accounts;
const accName = Object.keys(accounts).find((account) => accounts[account].current);
return { ...accounts[accName || ''], accountName: accName };
}
async getAccount(accountName) {
const accounts = (await this.readGlobalConfig()).accounts;
if (!accounts[accountName]) {
this.error(`Account ${accountName} not found.
Please use 'liara account add' to add this account, first.`);
}
return accounts[accountName];
}
async browser(browser) {
this.spinner = ora();
this.spinner.start('Opening browser...');
const port = await getPort({ port: portNumbers(3001, 3100) });
const query = `cli=v1&callbackURL=localhost:${port}/callback&client=cli`;
const url = `https://console.liara.ir/login?${Buffer.from(query).toString('base64')}`;
const app = browser ? { app: { name: apps[browser] } } : {};
const cp = await open(url, app);
return new Promise(async (resolve, reject) => {
cp.on('error', async (err) => {
reject(err);
});
cp.on('exit', (code) => {
if (code === 0) {
this.spinner.succeed('Browser opened.');
this.spinner.start('Waiting for login');
}
});
const buffers = [];
const server = createServer(async (req, res) => {
if (req.method === 'OPTIONS') {
res.writeHead(204, browserLoginHeader);
res.end();
return;
}
if (req.url === '/callback' && req.method === 'POST') {
for await (const chunk of req) {
buffers.push(chunk);
}
const { data } = JSON.parse(Buffer.concat(buffers).toString() || '[]');
res.writeHead(200, browserLoginHeader);
res.end();
this.spinner.stop();
server.close();
resolve(data);
}
}).listen(port);
});
}
async promptNetwork() {
this.spinner = ora();
this.spinner.start('Loading...');
try {
const { networks } = await this.got('v1/networks').json();
this.spinner.stop();
if (networks.length === 0) {
this.warn("Please create network via 'liara network:create' command, first.");
this.exit(1);
}
const { networkName } = (await inquirer.prompt({
name: 'networkName',
type: 'list',
message: 'Please select a network:',
choices: [
...networks.map((network) => {
return {
name: network.name,
};
}),
],
}));
return networks.find((network) => network.name === networkName);
}
catch (error) {
this.spinner.stop();
throw error;
}
}
async getNetwork(name) {
const { networks } = await this.got('v1/networks').json();
const network = networks.find((network) => network.name === name);
if (!network) {
this.error(`Network ${name} not found.`);
}
return network;
}
async getVms(errorMessage, filter) {
this.spinner.start('Loading...');
try {
const { vms } = await this.got('vm').json();
if (vms.length === 0) {
throw new NoVMsFoundError("You didn't create any VMs yet.\ncreate a VM using liara VM create command.");
}
const filteredVms = filter ? vms.filter(filter) : vms;
if (filteredVms.length === 0) {
throw new NoVMsFoundError(errorMessage);
}
this.spinner.stop();
return filteredVms;
}
catch (error) {
this.spinner.stop();
if (error instanceof NoVMsFoundError) {
throw new Error(error.message);
}
if (error.response && error.response.statusCode == 401) {
throw error;
}
throw error;
}
}
async getVMOperations(vm) {
try {
const { operations } = await this.got(`vm/operation/${vm._id}`).json();
return operations;
}
catch (error) {
if (error.response && error.response.statusCode == 401) {
throw error;
}
throw new Error('There was something wrong while fetching your VMs operations.');
}
}
}
default_1.flags = {
help: Flags.help({ char: 'h' }),
dev: Flags.boolean({ description: 'run in dev mode', hidden: true }),
debug: Flags.boolean({ description: 'show debug logs' }),
'api-token': Flags.string({
description: 'your api token to use for authentication',
}),
// region: Flags.string({
// description: 'the region you want to deploy your app to',
// options: ['iran', 'germany'],
// }),
account: Flags.string({
description: 'temporarily switch to a different account',
}),
'team-id': Flags.string({
description: 'your team id',
}),
};
export default default_1;