diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index 85eac7c1c..e1f6758a9 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -451,7 +451,7 @@ export default class Dev extends BaseCommand { }); if (overlay_port) { - new OverlayServer().listen(overlay_port); + new OverlayServer(this.app, this.config, default_project_name, DockerComposeUtils.getLocalServiceNames(compose_file)).listen(overlay_port); } try { diff --git a/src/common/docker-compose/index.ts b/src/common/docker-compose/index.ts index 65daf2bb8..00f870802 100644 --- a/src/common/docker-compose/index.ts +++ b/src/common/docker-compose/index.ts @@ -28,6 +28,16 @@ type GenerateOptions = { getImage?: (ref: string) => string; }; +type ServiceValue = { + name: string; + display_name: string; +}; + +export type ServiceKey = { + name: string; + value: ServiceValue; +}; + export class DockerComposeUtils { // used to namespace docker-compose projects so multiple deployments can happen to local public static DEFAULT_PROJECT = 'architect'; @@ -432,7 +442,7 @@ export class DockerComposeUtils { service_to.labels.push( `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.lastModified=true`, `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.regex=`, - `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.replacement=`, + `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.replacement=`, `traefik.http.routers.${traefik_service}.middlewares=${traefik_service}-rewritebody@docker`, ); } @@ -632,18 +642,23 @@ export class DockerComposeUtils { return answers.environment; } - public static async getLocalServiceForEnvironment(compose_file: string, service_name?: string): Promise<{ display_name: string, name: string }> { - // docker-compose -f and -p don't work in tandem - const compose = yaml.load(fs.readFileSync(compose_file).toString()) as DockerComposeTemplate; + public static getLocalServiceNames(compose_file: string): ServiceKey[] { + // docker-compose -f and -p don't work in tandem + const compose = yaml.load(fs.readFileSync(compose_file).toString()) as DockerComposeTemplate; - const services: { name: string, value: { display_name: string, name: string } }[] = []; + const services: ServiceKey[] = []; for (const [service_name, service] of Object.entries(compose.services)) { const display_name = service.labels?.find((label) => label.startsWith('architect.ref='))?.split('=')[1]; if (!display_name) continue; services.push({ name: display_name, value: { name: service_name, display_name } }); } - const answers: { service: { display_name: string, name: string } } = await inquirer.prompt([ + return services; + } + + public static async getLocalServiceForEnvironment(compose_file: string, service_name?: string): Promise { + const services = DockerComposeUtils.getLocalServiceNames(compose_file); + const answers: { service: ServiceValue } = await inquirer.prompt([ { when: !service_name, type: 'autocomplete', @@ -655,7 +670,7 @@ export class DockerComposeUtils { }, ]); - let selected_service; + let selected_service: ServiceValue | undefined; if (service_name) { selected_service = services.find((service) => service.name === service_name)?.value; if (!selected_service) { diff --git a/src/common/overlay/overlay-server.ts b/src/common/overlay/overlay-server.ts index ebbf1ca08..95f20835e 100644 --- a/src/common/overlay/overlay-server.ts +++ b/src/common/overlay/overlay-server.ts @@ -1,28 +1,142 @@ import fs from 'fs-extra'; import http from 'http'; import path from 'path'; +import DevRestart from '../../commands/dev/restart'; +import { Config } from '@oclif/core'; +import AppService from '../../app-config/service'; +import Logs from '../../commands/logs'; +import { ServiceKey } from '../docker-compose'; export class OverlayServer { + private app: AppService; + private config: Config; + private environment: string; + private services: ServiceKey[]; + + constructor(app: AppService, config: Config, environment: string, services: ServiceKey[]) { + this.app = app; + this.config = config; + this.environment = environment; + this.services = services; + } + listen(port: number): void { - const server = http.createServer(function (req, res) { + const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - try { - // eslint-disable-next-line unicorn/prefer-module - const file_path = path.join(__dirname, '../../static/overlay.js'); - const file = fs.readFileSync(file_path); - res.writeHead(200, { 'Content-Type': 'text/javascript' }); - res.end(file); - } catch (err) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end('Server Error'); + if (req.url === '/overlay.js') { + this.handleOverlay(res); + } else if (req.url === '/favicon.ico') { + this.handleFavicon(res); + } else if (req.url?.startsWith('/restart/')) { + const [_, __, service_name] = req.url.split('/'); + const restart_cmd = new DevRestart([service_name, '-e', this.environment], this.config); + restart_cmd.app = this.app; + + await restart_cmd.run(); + + res.writeHead(200); + res.end(`Restarted ${service_name}.`); + } else if (req.url?.startsWith('/logs/')) { + const [_, __, service_name] = req.url.split('/'); + const logs_cmd = new Logs([service_name, '-e', this.environment, '--raw'], this.config); + logs_cmd.app = this.app; + + res.writeHead(200); + res.write(`Logs can be viewed in the CLI with the command: 'architect logs -e ${this.environment} ${service_name}'`); + res.write('\n----------------\n\n'); + + // Overwrite log function to instead send data to our response. + logs_cmd.log = (...message: string[]) => { + for (const m of message) { + // log writes include ansi characters because we're using chalk to color text. + res.write(stripAnsi(m)); + } + res.write('\n'); + }; + + await logs_cmd.run(); + + res.end(); + } else { + res.writeHead(200); + let service_rows = ''; + for (const service_key of this.services) { + service_rows += ''; + + service_rows += ` + ${service_key.name} + Restart + View Logs + `; + + service_rows += ''; + } + + res.write(` + + + Architect Control + + + + + + + + + ${service_rows} +
Service
+ + + `); + res.end(); } }); server.on('error', err => console.log(err)); + console.log(`Starting overlay server: http://localhost:${port}`); server.listen(port); } + + private handleOverlay(res: http.ServerResponse): void { + try { + // eslint-disable-next-line unicorn/prefer-module + const file_path = path.join(__dirname, '../../static/overlay.js'); + const file = fs.readFileSync(file_path); + res.writeHead(200, { 'Content-Type': 'text/javascript' }); + res.end(file); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Server Error'); + } + } + + private handleFavicon(res: http.ServerResponse): void { + try { + // eslint-disable-next-line unicorn/prefer-module + const file_path = path.join(__dirname, '../../static/favicon.ico'); + const file = fs.readFileSync(file_path); + res.writeHead(200, { 'Content-Type': 'image/x-icon' }); + res.end(file); + } catch { + // No favicon for you :( + } + } +} + +/** + * Remove ansi characters from a string. + * Pulled from https://github.com/chalk/strip-ansi - TODO replace with actual dependency + * if we're going to hijack logs the way we currently are. + */ +function stripAnsi(string: string) { + const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', + ].join('|'); + return string.replace(new RegExp(pattern, 'g'), ''); } diff --git a/src/static/favicon.ico b/src/static/favicon.ico new file mode 100644 index 000000000..5bced214f Binary files /dev/null and b/src/static/favicon.ico differ diff --git a/src/static/overlay.js b/src/static/overlay.js index 5ac60f38e..cd1dd13ff 100644 --- a/src/static/overlay.js +++ b/src/static/overlay.js @@ -12,6 +12,10 @@ Architect.copyToClipboard = function (element, text) { tooltipText.innerHTML = 'Copied to clipboard!'; }; +Architect.run = function (url) { + fetch(url).then(res => console.log('Success')).catch(err => console.log('Failed')); +}; + Architect.outFunc = function (element) { const tooltipText = element.querySelector('.tooltiptext'); if (tooltipText.dataset.text) { @@ -29,6 +33,7 @@ Architect.appendHTML = function () { const script = document.querySelector('#architect-script'); const environment = script.dataset.environment; const service = script.dataset.service; + const overlay_url = script.dataset.overlayUrl; var styles = document.createElement('style'); styles.innerHTML = ` @@ -129,24 +134,23 @@ Architect.appendHTML = function () { wrapper.innerHTML = `