Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/commands/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
29 changes: 22 additions & 7 deletions src/common/docker-compose/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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=</head>`,
`traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.replacement=<script id="architect-script" async type="text/javascript" src="http://localhost:${overlay_port}" data-environment="${environment}" data-service="${node_to.config.metadata.ref}"></script></head>`,
`traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.replacement=<script id="architect-script" async type="text/javascript" src="http://localhost:${overlay_port}/overlay.js" data-overlay-url="http://localhost:${overlay_port}" data-environment="${environment}" data-service="${node_to.config.metadata.ref}"></script></head>`,
`traefik.http.routers.${traefik_service}.middlewares=${traefik_service}-rewritebody@docker`,
);
}
Expand Down Expand Up @@ -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<ServiceValue> {
const services = DockerComposeUtils.getLocalServiceNames(compose_file);
const answers: { service: ServiceValue } = await inquirer.prompt([
{
when: !service_name,
type: 'autocomplete',
Expand All @@ -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) {
Expand Down
134 changes: 124 additions & 10 deletions src/common/overlay/overlay-server.ts
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is a pretty cool idea!

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 += '<tr>';

service_rows += `
<td>${service_key.name}</td>
<td><a href="#" onclick="fetch('http://localhost:${port}/restart/${service_key.name}')">Restart</a></td>
<td><a href="http://localhost:${port}/logs/${service_key.name}" target="_blank">View Logs</a></td>
`;

service_rows += '</tr>';
}

res.write(`
<!DOCTYPE html>
<head>
<title>Architect Control</title>
</head>
<body>
<table>
<tr>
<th>Service</th>
<th></th>
<th></th>
</tr>
${service_rows}
</table>
</body>
</html>
`);
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'), '');
}
Binary file added src/static/favicon.ico
Binary file not shown.
24 changes: 14 additions & 10 deletions src/static/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 = `
Expand Down Expand Up @@ -129,24 +134,23 @@ Architect.appendHTML = function () {
wrapper.innerHTML = `
<div class="dropdown-content">
<div class="tooltip">
<a href="#" onclick="Architect.copyToClipboard(this, 'architect logs -e ${environment} ${service}')" onmouseout="Architect.outFunc(this)" style="border-top-left-radius: 5px;">
<span class="tooltiptext">View the logs from the CLI</span>
Logs
<a href="#" onclick="Architect.run('${overlay_url}/restart/${service}')">
<span class="tooltiptext">Restart this service</span>
Restart
</a>
</div>

<div class="tooltip">
<a href="#" onclick="Architect.copyToClipboard(this, 'architect exec -e ${environment} ${service} -- ls')" onmouseout="Architect.outFunc(this)">
<span class="tooltiptext">Execute a command from the CLI</span>
Exec
<a href="${overlay_url}/logs/${service}" target="_blank" style="border-top-left-radius: 5px;">
<span class="tooltiptext">View the logs</span>
Logs
</a>

</div>

<div class="tooltip">
<a href="#" onclick="Architect.copyToClipboard(this, 'architect dev:restart -e ${environment} ${service}')" onmouseout="Architect.outFunc(this)">
<span class="tooltiptext">Restart this service from the CLI</span>
Restart
<a href="#" onclick="Architect.copyToClipboard(this, 'architect exec -e ${environment} ${service} -- ls')" onmouseout="Architect.outFunc(this)">
<span class="tooltiptext">Execute a command from the CLI</span>
Exec
</a>
</div>

Expand Down