abstruse
Version:
Abstruse CI
396 lines (357 loc) • 12.4 kB
text/typescript
import { Component, OnInit, OnDestroy, NgZone, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { SocketService } from '../../services/socket.service';
import { ApiService } from '../../services/api.service';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
export interface IImage {
name: string;
dockerfile: string;
initsh: string;
base: boolean;
}
export interface ImageBuildType {
name: string;
layers: { id: string, status: string, progress: string, progressDetail: any }[];
}
export const allowedCommands: string[] = ['FROM', 'ENV', 'RUN', 'COPY'];
export class AppImagesComponent implements OnInit, OnDestroy {
loading: boolean;
editorOptions: any;
initEditorOptions: any;
form: IImage;
imageBuilds: ImageBuildType[];
imageBuildsText: string;
building: boolean;
editingImage: boolean;
success: boolean;
removeingImage: boolean;
baseImages: any[];
baseImage: string;
customImages: any[];
tab: string;
sub: Subscription;
approve: boolean;
buildingError: string;
terminalOptions: { size: 'small' | 'large', newline: boolean };
terminalInput: any;
baseImageOptions: { key: any, value: string }[];
imageTypeOptions: { key: any, value: string }[];
dangerousCommands: string[];
constructor(
private socketService: SocketService,
private zone: NgZone,
private api: ApiService,
private document: any
) {
this.baseImages = [];
this.baseImage = '';
this.buildingError = '';
this.customImages = [];
this.baseImageOptions = [];
this.dangerousCommands = [];
this.loading = true;
this.approve = false;
this.imageBuilds = [];
this.imageTypeOptions = [{ key: false, value: 'Custom Image' }, { key: true, value: 'Base Image' }];
this.terminalOptions = { size: 'large', newline: true };
this.editorOptions = {
lineNumbers: true,
theme: 'abstruseTheme',
language: 'dockerfile',
minimap: {
enabled: false
},
contextMenu: false,
fontFamily: 'monaco, menlo, monospace',
fontSize: 12,
scrollBeyondLastLine: false,
roundedSelection: false,
scrollbar: {
useShadows: false,
vertical: 'hidden',
horizontal: 'hidden',
horizontalScrollbarSize: 0,
horizontalSliderSize: 0,
verticalScrollbarSize: 0,
verticalSliderSize: 0
}
};
this.initEditorOptions = Object.assign({}, this.editorOptions, { language: 'plaintext' });
this.building = false;
this.editingImage = false;
this.removeingImage = false;
this.tab = 'images';
this.resetForm(!!this.baseImages.length);
}
ngOnInit() {
this.loading = false;
this.sub = this.socketService.outputEvents
.pipe(filter(event => event.type === 'imageBuildProgress'))
.subscribe(event => {
this.form.name = event.data.name;
let output;
try {
output = JSON.parse(event.data.output);
} catch (e) {
output = null;
}
if (output) {
this.building = true;
this.tab = 'build';
}
if (output && output.id && output.progressDetail) {
const buildIndex = this.findImageBuild(event.data.name);
const layerIndex = this.findImageLayer(buildIndex, output.id);
this.zone.run(() => {
this.imageBuilds[buildIndex].layers[layerIndex] = output;
const length = this.imageBuilds[buildIndex].layers.length;
const done = this.imageBuilds[buildIndex].layers.filter(l => {
return l.status === 'Download complete' || l.status === 'Pull complete';
}).length;
this.imageBuildsText = done + '/' + length;
});
} else if (output && output.stream) {
if (output.stream.startsWith('Successfully built') || output.stream.startsWith('Successfully tagged')) {
this.building = false;
this.fetchImages();
this.tab = 'images';
} else {
this.zone.run(() => this.terminalInput = output.stream);
}
} else if (output && output.errorDetail) {
this.zone.run(() => this.terminalInput = output.errorDetail.message);
} else if (event.data.output && event.data.output.startsWith('error while building image')) {
this.building = false;
this.buildingError = event.data.output;
}
});
this.socketService.emit({ type: 'subscribeToImageBuilder' });
this.fetchImages();
}
resetForm(imageType: boolean): void {
this.buildingError = '';
this.editingImage = false;
if (imageType) {
this.form = {
name: 'nameless_image',
dockerfile: [
'FROM ' + this.baseImage,
'',
'COPY init.sh /home/abstruse/init.sh',
'',
'# your commands go below: ',
'# example; install Chromium',
'RUN sudo apt-get update && sudo apt-get install chromium-browser libgconf2-dev -y',
'',
'# example; install nvm (Node Version Manager)',
'RUN cd /home/abstruse \\',
' && curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.4/install.sh | bash \\',
' && export NVM_DIR="$HOME/.nvm" \\',
' && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"',
'',
'# example; install sqlite3',
'RUN sudo apt-get install sqlite3 -y',
'',
'# example; install docker',
'RUN curl -o /tmp/docker.tgz https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz \\',
' && mkdir /tmp/docker && tar xzf /tmp/docker.tgz -C /tmp \\',
' && sudo ln -s /tmp/docker/docker /usr/bin/docker && sudo chmod 755 /usr/bin/docker && rm -rf /tmp/docker.tgz'
].join('\n'),
initsh: [
'# export CHROME_BIN',
'export CHROME_BIN=/usr/bin/chromium-browser',
'# here you define scripts that should be loaded or static env variables',
'# example for `nvm` or Node Version Manager',
'if [ -d /home/abstruse/.nvm ]; then',
' source /home/abstruse/.nvm/nvm.sh',
'fi',
'# giving docker access to abstruse user',
'if [ -e /var/run/docker.sock ]; then',
' sudo chown -R 1000:100 /var/run/docker.sock > /dev/null 2>&1',
'fi'
].join('\n'),
base: !imageType
};
} else {
this.form = {
name: 'abstruse_builder',
dockerfile: [
'FROM ubuntu:bionic',
'',
'ENV DEBIAN_FRONTEND=noninteractive',
'ENV DISPLAY=:99',
'',
'# please do not edit between lines or image on abstruse will not work properly',
'',
'# -------------------------------------------------------------------------------------------',
'',
'RUN set -xe \\',
' && apt-get update \\',
' && apt-get install -y --no-install-recommends ca-certificates curl build-essential \\',
' && apt-get install -y --no-install-recommends libssl-dev git python \\',
' && apt-get install -y --no-install-recommends sudo \\',
' && apt-get install -y --no-install-recommends xvfb x11vnc fluxbox xterm openssh-server',
'',
'RUN useradd -u 1000 -g 100 -G sudo --shell /bin/bash -m --home-dir /home/abstruse abstruse \\',
' && echo \'abstruse ALL=(ALL) NOPASSWD:ALL\' >> /etc/sudoers \\',
' && echo \'abstruse:abstrusePass\' | chpasswd',
'',
'COPY fluxbox /etc/init.d/',
'COPY x11vnc /etc/init.d/',
'COPY xvfb /etc/init.d/',
'COPY entry.sh /',
'',
'COPY abstruse-pty-amd64 /usr/bin/abstruse-pty',
'',
'USER abstruse',
'WORKDIR /home/abstruse/build',
'',
'RUN cd /home/abstruse && sudo chown -Rv 1000:100 /home/abstruse',
'',
'RUN sudo chmod +x /entry.sh /etc/init.d/* /usr/bin/abstruse*',
'CMD ["/entry.sh"]',
'',
'EXPOSE 22 5900'
].join('\n'),
initsh: '',
base: !imageType
};
}
}
editImage(index: number, base: boolean): void {
this.editingImage = true;
this.updateForm(index, base);
this.tab = 'build';
}
removeImage(index: number, base: boolean): void {
this.removeingImage = true;
window.scrollTo(0, 0);
this.updateForm(index, base);
}
updateForm(index: number, base: boolean): void {
if (base) {
this.form.name = this.baseImages[index].name;
this.form.dockerfile = this.baseImages[index].dockerfile;
this.form.initsh = this.baseImages[index].initsh;
} else {
this.form.name = this.customImages[index].name;
this.form.dockerfile = this.customImages[index].dockerfile;
this.form.initsh = this.customImages[index].initsh;
}
this.form.base = base;
this.buildingError = '';
}
fetchImages(): void {
this.loading = true;
this.api.imagesList().subscribe(data => {
this.customImages = [];
this.baseImages = [];
data.forEach(image => {
if (image.base) {
this.baseImages.push(image);
} else {
this.customImages.push(image);
}
});
this.baseImageOptions = [];
if (this.baseImages.length) {
this.baseImages.forEach(i => this.baseImageOptions.push({ key: i.name, value: i.name }));
this.baseImage = this.baseImages[0].name;
}
this.resetForm(!!this.baseImages.length);
this.loading = false;
});
}
findImageBuild(imageName: string): number {
const index = this.imageBuilds.findIndex(ibuild => ibuild.name === imageName);
if (index !== -1) {
return index;
} else {
this.imageBuilds.push({
name: imageName,
layers: []
});
return this.imageBuilds.length - 1;
}
}
findImageLayer(imageBuildIndex: number, id: string): number {
const index = this.imageBuilds[imageBuildIndex].layers.findIndex(layer => {
return layer.id === id;
});
if (index !== -1) {
return index;
} else {
this.imageBuilds[imageBuildIndex].layers.push({
id: id,
status: null,
progress: null,
progressDetail: null
});
return this.imageBuilds[imageBuildIndex].layers.length - 1;
}
}
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe();
}
this.socketService.emit({ type: 'unsubscribeFromImageBuilder' });
}
buildImage(): void {
this.buildingError = '';
if (this.checkImage()) {
this.approve = true;
window.scrollTo(0, 0);
} else {
this.startBuild();
}
}
checkImage(): boolean {
this.dangerousCommands = [];
if (!this.form.base) {
let image = this.form.dockerfile.split('\n').filter(i => {
return i[0] !== '#' && i.length && i[0] !== ' ';
});
image.forEach(c => {
let command = c.split(' ');
if (command) {
if (allowedCommands.indexOf(command[0]) === -1
&& this.dangerousCommands.indexOf(command[0]) === -1) {
this.dangerousCommands.push(command[0]);
}
}
});
}
return !!this.dangerousCommands.length;
}
startBuild(): void {
this.building = true;
this.approve = false;
this.socketService.emit({ type: 'buildImage', data: this.form });
}
startDelete(): void {
this.removeingImage = false;
if (this.form.base) {
let index = this.baseImages.findIndex(i => i.name === this.form.name);
this.baseImages.splice(index, 1);
} else {
let index = this.customImages.findIndex(i => i.name === this.form.name);
this.customImages.splice(index, 1);
}
this.socketService.emit({ type: 'deleteImage', data: this.form });
}
changeBaseImageSelect(e: Event): void {
let tmp = this.form.dockerfile.split('\n');
if (tmp) {
tmp[0] = `FROM ${e}`;
}
this.form.dockerfile = tmp.join('\n');
}
changeImageTypeSelect(e: Event): void {
this.resetForm(!e);
}
}