From f0fdf780d41926c405ac2bbaead895ffdac4a0a9 Mon Sep 17 00:00:00 2001 From: Izak Lipnik Date: Fri, 10 Nov 2017 17:34:49 +0100 Subject: [PATCH] feat(base-image): build base image during setup process (closes #277) --- package-lock.json | 2 +- package.json | 1 - src/api/image-builder.ts | 70 +++- src/api/server-routes.ts | 7 +- src/api/utils.ts | 1 + src/app/app-routing.module.ts | 3 - .../app-images/app-images.component.html | 105 ++++-- .../app-images/app-images.component.ts | 310 ++++++++++-------- .../app-selectbox/app-selectbox.component.ts | 9 +- .../app-setup/app-setup.component.ts | 2 + .../app-terminal/app-terminal.component.ts | 21 +- src/app/services/api.service.ts | 4 + src/app/styles/images.sass | 67 ++++ 13 files changed, 424 insertions(+), 178 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b84cfa7d..402417208 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "abstruse", - "version": "1.3.1", + "version": "1.3.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b7952eea2..206be7cc2 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,6 @@ "monaco-editor": "^0.10.1", "multer": "^1.3.0", "ng2-datepicker": "^2.1.3", - "ngx-slimscroll": "^3.4.1", "ngx-uploader": "^4.1.0", "node-rsa": "^0.4.2", "node-sass": "^4.5.3", diff --git a/src/api/image-builder.ts b/src/api/image-builder.ts index f8971708f..c62c57caf 100644 --- a/src/api/image-builder.ts +++ b/src/api/image-builder.ts @@ -11,6 +11,7 @@ export interface ImageData { name: string; dockerfile: string; initsh: string; + base: boolean; } export interface ImageBuildOutput { @@ -21,9 +22,14 @@ export interface ImageBuildOutput { export const imageBuilder: Subject = new Subject(); export const imageBuilderObs = imageBuilder.share(); +export function buildAbstruseBaseImage(): void { + buildDockerImage(defaultBaseImage); +} + export function buildDockerImage(data: ImageData): void { prepareDirectory(data).then(() => { - const folderPath = getFilePath(`images/${data.name}`); + const folderPath = + data.base ? getFilePath(`base-images/${data.name}`) : getFilePath(`images/${data.name}`); const src = glob.sync(folderPath + '/**/*').map(filePath => filePath.split('/').pop()); let msg: LogMessageType = { @@ -75,7 +81,8 @@ export function buildDockerImage(data: ImageData): void { } function prepareDirectory(data: ImageData): Promise { - const folderPath = getFilePath(`images/${data.name}`); + const folderPath = + data.base ? getFilePath(`base-images/${data.name}`) : getFilePath(`images/${data.name}`); const dockerFilePath = join(folderPath, 'Dockerfile'); const initShFilePath = join(folderPath, 'init.sh'); const essentialFolderPath = getFilePath(`docker-essential`); @@ -97,7 +104,15 @@ function prepareDirectory(data: ImageData): Promise { export function getImages(): Promise { return new Promise((resolve, reject) => { - const imagesDir = getFilePath(`images`); + Promise.all([getImagesInDirectory('images'), getImagesInDirectory('base-images')]) + .then(imgs => resolve(imgs.reduce((a, b) => a.concat(b)))); + }); +} + + +function getImagesInDirectory(path: string): Promise { + return new Promise((resolve, reject) => { + const imagesDir = getFilePath(path); fs.readdir(imagesDir).then(dirs => { docker.listImages() @@ -126,8 +141,8 @@ export function getImages(): Promise { }).filter(Boolean); Promise.all(imgs.map(img => { - const dockerfile = getFilePath(`images/${img.name}/Dockerfile`); - const initsh = getFilePath(`images/${img.name}/init.sh`); + const dockerfile = getFilePath(`${path}/${img.name}/Dockerfile`); + const initsh = getFilePath(`${path}/${img.name}/init.sh`); if (fs.existsSync(dockerfile) && fs.existsSync(initsh)) { return fs.readFile(dockerfile) @@ -135,6 +150,7 @@ export function getImages(): Promise { return fs.readFile(initsh).then(initshContents => { img.dockerfile = dockerfileContents.toString(); img.initsh = initshContents.toString(); + img.base = path === 'base-images'; return img; }); @@ -149,3 +165,47 @@ export function getImages(): Promise { }); }); } + +let defaultBaseImage: ImageData = { + name: 'abstruse_builder', + dockerfile: [ + 'FROM ubuntu:17.10', + '', + 'ENV DEBIAN_FRONTEND=noninteractive', + '', + '# 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 /usr/bin/abstruse-pty', + 'COPY abstruse-exec.sh /usr/bin/abstruse', + '', + '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: true +}; diff --git a/src/api/server-routes.ts b/src/api/server-routes.ts index 5fdd0c85e..8014a0cc2 100644 --- a/src/api/server-routes.ts +++ b/src/api/server-routes.ts @@ -40,7 +40,7 @@ import { import { insertEnvironmentVariable, removeEnvironmentVariable } from './db/environment-variable'; import { getLogs } from './db/log'; import { imageExists } from './docker'; -import { getImages } from './image-builder'; +import { getImages, buildAbstruseBaseImage } from './image-builder'; import { checkApiRequestAuth } from './security'; import { checkConfigPresence, @@ -770,6 +770,11 @@ export function imagesRoutes(): express.Router { .catch(err => res.status(200).json({ status: false })); }); + router.post('/build-base', (req: express.Request, res: express.Response) => { + buildAbstruseBaseImage(); + res.status(200).json({ data: true }); + }); + return router; } diff --git a/src/api/utils.ts b/src/api/utils.ts index 0df82f3bb..64d5a0521 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -55,6 +55,7 @@ export function initSetup(): Promise { return makeAbstruseDir() .then(() => makeCacheDir()) .then(() => ensureDirectory(getFilePath('images'))) + .then(() => ensureDirectory(getFilePath('base-images'))) .then(() => { const srcDir = resolve(__dirname, '../../src/files/docker-essential'); const destDir = getFilePath('docker-essential'); diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 9530bb6ef..70195ab2a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,7 +4,6 @@ import { CommonModule } from '@angular/common'; import { HttpModule } from '@angular/http'; import { FormsModule } from '@angular/forms'; import { NgUploaderModule } from 'ngx-uploader'; -import { NgSlimScrollModule } from 'ngx-slimscroll'; import { NgDatepickerModule } from 'ng2-datepicker/ng2-datepicker'; import { AuthGuardProvider, AuthGuard } from './services/auth-guard.service'; @@ -56,7 +55,6 @@ import { ToTimePipe } from './pipes/to-time.pipe'; FormsModule, HttpModule, NgUploaderModule, - NgSlimScrollModule, NgDatepickerModule ], declarations: [ @@ -94,7 +92,6 @@ import { ToTimePipe } from './pipes/to-time.pipe'; FormsModule, HttpModule, NgUploaderModule, - NgSlimScrollModule, NgDatepickerModule ] }) diff --git a/src/app/components/app-images/app-images.component.html b/src/app/components/app-images/app-images.component.html index 1321990a2..f48b39e28 100644 --- a/src/app/components/app-images/app-images.component.html +++ b/src/app/components/app-images/app-images.component.html @@ -14,7 +14,7 @@

Docker Build Images

Images - @@ -36,21 +36,8 @@

Docker Build Images

-
-
-
-

{{ i.name }}

- {{ i.version }} - - {{ i.created }} - Created {{ i.createdAgo }} ago - Size {{ i.size }} - -
-
-
-
+
@@ -58,7 +45,7 @@

{{ i.name }}

-

No Docker images built with Abstruse found.

+

No Base Images found.

It is important to build images using this form so files needed for Abstruse to work are included.

Images built using Abstruse form are later easily upgradeable as config files are stored with the application.

@@ -67,6 +54,43 @@

{{ i.name }}

+
+
+

Base Images

+
+
+
+
+

{{ i.name }}

+ {{ i.version }} + + {{ i.created }} + Size {{ i.size }} + +
+
+
+
+ +
+
+

Custom Images

+
+
+
+
+

{{ i.name }}

+ {{ i.version }} + + {{ i.created }} + Created {{ i.createdAgo }} ago + Size {{ i.size }} + +
+
+
+
+
@@ -74,14 +98,55 @@

{{ i.name }}

+
+
+
+

Warning:

+
+
+
+ Your docker file contains commands that can cause Abstruse to work incorrectly ({{dangerousCommands}}). +
+
+ Are you sure, you want to build that image? +
+
+
+ +
+
+ +
+
+

Image name

+
+
+

Image Type

+ +
+
+
+

Base Image

+ +
+
+

Dockerfile

-

init.sh

- +
+

init.sh

+ +
- +
@@ -91,7 +156,7 @@

init.sh

Building image {{ form.name }} ({{ imageBuildsText }} layers) ...

-

+            
           
diff --git a/src/app/components/app-images/app-images.component.ts b/src/app/components/app-images/app-images.component.ts index 81d468c79..5dd3f9a36 100644 --- a/src/app/components/app-images/app-images.component.ts +++ b/src/app/components/app-images/app-images.component.ts @@ -1,15 +1,14 @@ -import { Component, OnInit, OnDestroy, NgZone, Inject, EventEmitter } from '@angular/core'; +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 * as ansiUp from 'ansi_up'; -import { SlimScrollEvent, ISlimScrollOptions } from 'ngx-slimscroll'; import { Subscription } from 'rxjs/Subscription'; export interface IImage { name: string; dockerfile: string; initsh: string; + base: boolean; } export interface ImageBuildType { @@ -17,6 +16,8 @@ export interface ImageBuildType { layers: { id: string, status: string, progress: string, progressDetail: any }[]; } +export const allowedCommands: string[] = ['FROM', 'ENV', 'RUN', 'COPY']; + @Component({ selector: 'app-images', templateUrl: 'app-images.component.html' @@ -28,15 +29,19 @@ export class AppImagesComponent implements OnInit, OnDestroy { form: IImage; imageBuilds: ImageBuildType[]; imageBuildsText: string; - imageBuildLog: string; - au: any; building: boolean; success: boolean; - images: any[]; + baseImages: any[]; + baseImage: string; + customImages: any[]; tab: string; - scrollOptions: ISlimScrollOptions; - scrollEvents: EventEmitter; sub: Subscription; + approve: boolean; + terminalOptions: { size: 'small' | 'large', newline: boolean }; + terminalInput: any; + baseImageOptions: { key: any, value: string }[]; + imageTypeOptions: { key: any, value: string }[]; + dangerousCommands: string[]; constructor( private socketService: SocketService, @@ -44,9 +49,16 @@ export class AppImagesComponent implements OnInit, OnDestroy { private api: ApiService, @Inject(DOCUMENT) private document: any ) { + this.baseImages = []; + this.baseImage = ''; + this.customImages = []; + this.baseImageOptions = []; + this.dangerousCommands = []; this.loading = true; + this.approve = false; this.imageBuilds = []; - this.imageBuildLog = ''; + this.imageTypeOptions = [{ key: false, value: 'Custom Image' }, { key: true, value: 'Base Image' }]; + this.terminalOptions = { size: 'large', newline: true }; this.editorOptions = { lineNumbers: true, @@ -72,27 +84,10 @@ export class AppImagesComponent implements OnInit, OnDestroy { }; this.initEditorOptions = Object.assign({}, this.editorOptions, { language: 'plaintext' }); - - this.au = new ansiUp.default(); this.building = false; this.tab = 'images'; - this.resetForm(); - - this.scrollOptions = { - barBackground: '#666', - gridBackground: '#000', - barBorderRadius: '10', - barWidth: '7', - gridWidth: '7', - barMargin: '2px 5px', - gridMargin: '2px 5px', - gridBorderRadius: '10', - alwaysVisible: false - }; - - this.scrollEvents = new EventEmitter(); - this.images = []; + this.resetForm(!!this.baseImages.length); } ngOnInit() { @@ -101,15 +96,17 @@ export class AppImagesComponent implements OnInit, OnDestroy { this.sub = this.socketService.outputEvents .filter(event => event.type === 'imageBuildProgress') .subscribe(event => { + this.form.name = event.data.name; let output; try { - output = JSON.parse(event.data.output.replace('\r\n', '')); + output = JSON.parse(event.data.output); } catch (e) { output = null; } if (output) { this.building = true; + this.tab = 'build'; } if (output && output.id && output.progressDetail) { @@ -131,12 +128,10 @@ export class AppImagesComponent implements OnInit, OnDestroy { this.fetchImages(); this.tab = 'images'; } else { - this.imageBuildLog += this.au.ansi_to_html(output.stream); - this.scrollToBottom(); + this.zone.run(() => this.terminalInput = output.stream); } } else if (output && output.errorDetail) { - this.imageBuildLog += `${output.errorDetail.message}`; - this.scrollToBottom(); + this.zone.run(() => this.terminalInput = `${output.errorDetail.message}`); } }); @@ -144,108 +139,129 @@ export class AppImagesComponent implements OnInit, OnDestroy { this.fetchImages(); } - resetForm(): void { - this.form = { - name: 'nameless_image', - dockerfile: [ - 'FROM ubuntu:17.10', - '', - 'ENV DEBIAN_FRONTEND=noninteractive', - '', - '# 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 init.sh /home/abstruse/init.sh', - 'COPY abstruse-pty /usr/bin/abstruse-pty', - 'COPY abstruse-exec.sh /usr/bin/abstruse', - '', - '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', - '', - '# --------------------------------------------------------------------------------------------------------------------------------', - '', - '# your commands go below: ', - '# example; install Chromium', - 'RUN 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', - '', - '# example for 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') - }; + resetForm(imageType: boolean): void { + 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 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:17.10', + '', + 'ENV DEBIAN_FRONTEND=noninteractive', + '', + '# 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 /usr/bin/abstruse-pty', + 'COPY abstruse-exec.sh /usr/bin/abstruse', + '', + '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): void { - this.form.name = this.images[index].name; - this.form.dockerfile = this.images[index].dockerfile; - this.form.initsh = this.images[index].initsh; + editImage(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.tab = 'build'; } fetchImages(): void { this.loading = true; this.api.imagesList().subscribe(data => { - this.resetForm(); - this.images = data.map(image => { - if (!image.dockerfile) { - image.dockerfile = this.form.dockerfile; + this.customImages = []; + this.baseImages = []; + data.forEach(image => { + if (image.base) { + this.baseImages.push(image); + } else { + this.customImages.push(image); } - - if (!image.initsh) { - image.initsh = this.form.initsh; - } - - return 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; }); } @@ -278,21 +294,11 @@ export class AppImagesComponent implements OnInit, OnDestroy { progress: null, progressDetail: null }); + return this.imageBuilds[imageBuildIndex].layers.length - 1; } } - scrollToBottom() { - setTimeout(() => { - const ev: SlimScrollEvent = { - type: 'scrollToBottom', - easing: 'linear', - duration: 50 - }; - this.scrollEvents.emit(ev); - }); - } - ngOnDestroy() { if (this.sub) { this.sub.unsubscribe(); @@ -302,7 +308,43 @@ export class AppImagesComponent implements OnInit, OnDestroy { } buildImage(): void { + this.checkImage() ? this.approve = true : this.startBuild(); + } + + checkImage(): boolean { + this.dangerousCommands = []; + 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 }); } + + 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); + } } diff --git a/src/app/components/app-selectbox/app-selectbox.component.ts b/src/app/components/app-selectbox/app-selectbox.component.ts index 3548a5bd7..28ae24b1e 100644 --- a/src/app/components/app-selectbox/app-selectbox.component.ts +++ b/src/app/components/app-selectbox/app-selectbox.component.ts @@ -23,12 +23,11 @@ export class AppSelectboxComponent implements OnChanges { } set value(val: string) { - if (!val) { - return; + let ind = this.data.findIndex(d => d.key === val); + if (ind !== -1) { + this.index = ind; + this.onChangeCallback(this.data[this.index].key); } - - this.index = this.data.findIndex(d => d.key === val); - this.onChangeCallback(this.data[this.index].key); } ngOnChanges(changes: SimpleChanges) { diff --git a/src/app/components/app-setup/app-setup.component.ts b/src/app/components/app-setup/app-setup.component.ts index a4bbac9bc..f00edbe35 100644 --- a/src/app/components/app-setup/app-setup.component.ts +++ b/src/app/components/app-setup/app-setup.component.ts @@ -87,6 +87,8 @@ export class AppSetupComponent implements OnInit { this.step = 'db'; } }); + + this.apiService.buildAbstruseBaseImage().subscribe(() => {}); } else { this.loading = false; this.step = 'done'; diff --git a/src/app/components/app-terminal/app-terminal.component.ts b/src/app/components/app-terminal/app-terminal.component.ts index eb4313852..76db92d80 100644 --- a/src/app/components/app-terminal/app-terminal.component.ts +++ b/src/app/components/app-terminal/app-terminal.component.ts @@ -22,10 +22,10 @@ const terminalColorPallete = ['rgb(40, 42, 54)', 'rgb(255, 85, 85)', 'rgb(80, 25 }) export class AppTerminalComponent implements OnInit { @Input() data: any; - @Input() options: { size: 'normal' | 'large' }; + @Input() options: { size: 'normal' | 'large', newline: boolean }; hterm: hterm.Terminal; terminalReady: boolean; - unwritenChanges: string; + unwritenChanges: string[]; constructor( private elementRef: ElementRef, @@ -34,7 +34,7 @@ export class AppTerminalComponent implements OnInit { hterm.hterm.defaultStorage = new hterm.lib.Storage.Local(); this.hterm = new hterm.hterm.Terminal(); this.terminalReady = false; - this.unwritenChanges = ''; + this.unwritenChanges = []; } ngOnInit() { @@ -62,9 +62,9 @@ export class AppTerminalComponent implements OnInit { this.hterm.prefs_.set('color-palette-overrides', terminalColorPallete); this.terminalReady = true; - if (this.unwritenChanges) { - this.printToTerminal(this.unwritenChanges); - this.unwritenChanges = ''; + if (this.unwritenChanges.length) { + this.unwritenChanges.forEach(p => this.printToTerminal(p)); + this.unwritenChanges = []; } }; @@ -83,14 +83,19 @@ export class AppTerminalComponent implements OnInit { } if (!this.terminalReady) { - this.unwritenChanges += this.data; + this.unwritenChanges.push(this.data); } else { this.printToTerminal(this.data); } } printToTerminal(data: string) { - this.hterm.io.print(this.data); + if (this.options.newline) { + this.hterm.io.println(data); + } else { + this.hterm.io.print(data); + } + if (this.hterm.keyboard.terminal && this.hterm.keyboard.terminal.scrollPort_ && this.hterm.keyboard.terminal.scrollPort_.isScrolledEnd) { diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 856adc650..41fe1c65a 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -99,6 +99,10 @@ export class ApiService { return this.post(`${this.url}/setup/db/init`, {}); } + buildAbstruseBaseImage(): Observable { + return this.post(`${this.url}/images/build-base`, {}); + } + loginRequired(): Observable { return this.get(`${this.url}/setup/login-required`); } diff --git a/src/app/styles/images.sass b/src/app/styles/images.sass index e10bc1f0e..7da1f210f 100644 --- a/src/app/styles/images.sass +++ b/src/app/styles/images.sass @@ -87,3 +87,70 @@ .button margin: 10px 0 + .base-image-item + display: inline-block + width: 100% + height: 230px + padding: 7px + background: $white + border: 1px solid $border + + h2 + font-size: 16px + font-weight: $weight-semibold + text-align: center + + span + width: 100% + height: 20px + font-size: 12px + display: flex + align-items: center + justify-content: center + + &.time + color: $color-secondary + + &.size + font-weight: $weight-semibold + + .docker-img + fill: red !important + display: block + width: 130px + margin: 10px auto + + .button + margin: 3px auto + +.content-box + padding: 30px + background: $white + border: 1px solid $border + border-radius: 4px + width: 100% + min-height: 50px + margin-bottom: 30px + + .content-box-header + display: flex + align-items: center + justify-content: space-between + margin-bottom: 10px + + h2 + padding: 0 + margin: 0 + display: inline + + h3 + display: inline + text-transform: none + padding: 0 + margin: 0 + + .approve + display: flex + align-items: center + justify-content: center + text-align: center