UNPKG

@liara/cli

Version:

The command line interface for Liara

270 lines (268 loc) 10.4 kB
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;