Skip to content

Commit

Permalink
feat: add vnc-enabled base image (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
gavin-k-lee authored Apr 6, 2021
1 parent bfe0ae2 commit 0a667be
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/build_and_push_to_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
EXTENSIONS:
- cuda9.2
- cuda-tf
- vnc

steps:
- name: Docker Login
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ extensions = \
bioc \
r \
cuda-9.2 \
cuda-10.0-tf
cuda-10.0-tf \
vnc

DOCKER_PREFIX?=renku/renkulab
DOCKER_LABEL?=latest
Expand Down Expand Up @@ -91,3 +92,10 @@ cuda: py
--build-arg TENSORFLOW_VERSION=$(TENSORFLOW_VERSION) \
-t $(DOCKER_PREFIX)-cuda-tf:$(DOCKER_LABEL) && \
docker tag $(DOCKER_PREFIX)-cuda-tf:$(DOCKER_LABEL) $(DOCKER_PREFIX)-cuda-tf:$(GIT_MASTER_HEAD_SHA)

vnc: py
docker build docker/vnc \
--build-arg BASE_IMAGE=renku/renkulab-py:$(GIT_MASTER_HEAD_SHA)$(RENKU_TAG) \
-t $(DOCKER_PREFIX)-vnc:$(DOCKER_LABEL)$(RENKU_TAG) && \
docker tag $(DOCKER_PREFIX)-vnc:$(DOCKER_LABEL)$(RENKU_TAG) $(DOCKER_PREFIX)-vnc:$(GIT_MASTER_HEAD_SHA)$(RENKU_TAG)

58 changes: 58 additions & 0 deletions docker/vnc/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
ARG BASE_IMAGE=renku/renkulab-py:latest
FROM ${BASE_IMAGE}

LABEL maintainer="Swiss Data Science Center <[email protected]>"

USER root

RUN apt-get update \
&& apt-get install -yq --no-install-recommends \
dbus-x11 \
firefox \
net-tools \
less \
xfce4 \
xfce4-panel \
xfce4-session \
xfce4-settings \
xorg \
xubuntu-icon-theme \
xterm \
&& apt-get autoremove --purge \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* \
&& find /var/log -type f -exec cp /dev/null \{\} \;

#################################################################
# Install noVNC

ENV novnc_version=1.1.0

RUN cd /opt && \
curl -sSfL https://github.com/novnc/noVNC/archive/v${novnc_version}.tar.gz | tar -zxf -

RUN sed -i -e "s,'websockify',window.location.pathname.slice(1),g" /opt/noVNC-${novnc_version}/app/ui.js \
&& chmod a+rX -R /opt/noVNC-${novnc_version}

COPY --chown=root:root vnc_renku.html /opt/noVNC-${novnc_version}

#################################################################
# Install TigerVNC

ENV tigervnc_version=1.9.0

RUN curl -sSfL https://bintray.com/tigervnc/stable/download_file?file_path=tigervnc-${tigervnc_version}.x86_64.tar.gz | tar -zxf - -C /usr/local --strip=2

#################################################################
# Install the jupyter extensions
USER ${NB_USER}

RUN conda install jupyter-server-proxy numpy websockify -c conda-forge \
&& jupyter labextension install @jupyterlab/server-proxy \
&& conda clean -y --all

COPY jupyter_notebook_config.py /home/jovyan/.jupyter/jupyter_notebook_config.py

COPY post-init.sh /post-init.sh

38 changes: 38 additions & 0 deletions docker/vnc/jupyter_notebook_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os

xstartup = 'dbus-launch xfce4-session'

vnc_socket = os.path.join(os.getenv('HOME'), '.vnc', 'socket')

noVNC_version = '1.1.0'

c.ServerProxy.servers = {
'vnc': {
'command': [
'/opt/conda/bin/websockify',
'-v',
'--web', '/opt/noVNC-' + noVNC_version,
'--heartbeat', '30',
'5901',
'--unix-target', vnc_socket,
'--',
'vncserver',
'-verbose',
'-xstartup', xstartup,
'-geometry', '1024x768',
'-SecurityTypes', 'None',
'-rfbunixpath', vnc_socket,
'-fg',
':1'
],
'absolute_url': False,
'port': 5901,
'timeout': 10,
'mappath': {'/': '/vnc_renku.html'},
'launcher_entry': {
'enabled': True,
'title': 'VNC'
}
}
}

4 changes: 4 additions & 0 deletions docker/vnc/post-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

# Make the default terminal folder the project folder in the vnc
echo "cd /work/${CI_PROJECT}" >> ~/.bashrc
253 changes: 253 additions & 0 deletions docker/vnc/vnc_renku.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="en">
<head>

<!--
noVNC example: lightweight example using minimal UI and features
This is a self-contained file which doesn't import WebUtil or external CSS.
Copyright (C) 2019 The noVNC Authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
Connect parameters are provided in query string:
http://example.com/?host=HOST&port=PORT&scale=true
-->
<title>noVNC</title>

<meta charset="utf-8">

<style>

body {
margin: 0;
background-color: dimgrey;
height: 100%;
display: flex;
flex-direction: column;
}
html {
height: 100%;
}

#top_bar {
background-color: #6e84a3;
color: white;
font: bold 12px Helvetica;
padding: 6px 5px 4px 5px;
border-bottom: 1px outset;
}
#status {
text-align: center;
}
#sendCtrlAltDelButton {
position: fixed;
top: 0px;
right: 0px;
border: 1px outset;
padding: 5px 5px 4px 5px;
cursor: pointer;
}
#fullScreenButton {
position: fixed;
top: 0px;
left: 0px;
border: 1px outset;
padding: 5px 5px 4px 5px;
cursor: pointer;
}

#screen {
flex: 1; /* fill remaining space */
overflow: hidden;
}

.selectedButton {
border-color: rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.5);
}

</style>

<script type="module" crossorigin="anonymous" src="app/ui.js"></script>
<script type="module" crossorigin="anonymous">
// RFB holds the API to connect and communicate with a VNC server
import RFB from './core/rfb.js';

let rfb;
let desktopName;


function updateFullScreen() {
if (
!document.fullscreenElement
&& !document.mozFullScreen
&& !document.webkitIsFullScreen
&& !document.msFullscreenElement) {
document.getElementById('fullScreenButton')
.classList.remove("selectedButton");
} else {
document.getElementById('fullScreenButton')
.classList.add("selectedButton");
}
}

// Toggle full screen
function toggleFullscreen() {
if (document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (document.body.msRequestFullscreen) {
document.body.msRequestFullscreen();
}
}
updateFullScreen()
}

// When this function is called we have
// successfully connected to a server
function connectedToServer(e) {
status("Connected to " + desktopName);
}

// This function is called when we are disconnected
function disconnectedFromServer(e) {
if (e.detail.clean) {
status("Disconnected");
} else {
status("Something went wrong, connection is closed");
}
}

// When this function is called, the server requires
// credentials to authenticate
function credentialsAreRequired(e) {
const password = prompt("Password Required:");
rfb.sendCredentials({ password: password });
}

// When this function is called we have received
// a desktop name from the server
function updateDesktopName(e) {
desktopName = document.URL+e.detail.name;
}

// Since most operating systems will catch Ctrl+Alt+Del
// before they get a chance to be intercepted by the browser,
// we provide a way to emulate this key sequence.
function sendCtrlAltDel() {
rfb.sendCtrlAltDel();
return false;
}

// Show a status text in the top bar
function status(text) {
document.getElementById('status').textContent = text;
}

// This function extracts the value of one variable from the
// query string. If the variable isn't defined in the URL
// it returns the default value instead.
function readQueryVariable(name, defaultValue) {
// A URL with a query parameter can look like this:
// https://www.example.com?myqueryparam=myvalue
//
// Note that we use location.href instead of location.search
// because Firefox < 53 has a bug w.r.t location.search
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = document.location.href.match(re);

if (match) {
// We have to decode the URL since want the cleartext value
return decodeURIComponent(match[1]);
}

return defaultValue;
}

if (document.addEventListener) {
document.addEventListener('fullscreenchange', updateFullScreen);
document.addEventListener('webkitfullscreenchange', updateFullScreen);
document.addEventListener('mozfullscreenchange', updateFullScreen);
document.addEventListener('MSFullscreenChange', updateFullScreen);
}

document.getElementById('sendCtrlAltDelButton')
.onclick = sendCtrlAltDel;

document.getElementById('fullScreenButton')
.onclick = toggleFullscreen;

// Read parameters specified in the URL query string
// By default, use the host and port of server that served this file
const host = readQueryVariable('host', window.location.hostname);
let port = readQueryVariable('port', window.location.port);
const password = readQueryVariable('password');
const path = readQueryVariable('path', window.location.pathname.slice(1));

// | | | | | |
// | | | Connect | | |
// v v v v v v

status("Connecting");

// Build the websocket URL used to connect
let url;
if (window.location.protocol === "https:") {
url = 'wss';
} else {
url = 'ws';
}
url += '://' + host;
if(port) {
url += ':' + port;
}
url += '/' + path;

// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('screen'), url,
{ credentials: { password: password } });

// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);

// Set parameters that can be changed on an active connection
rfb.viewOnly = readQueryVariable('view_only', false);
rfb.scaleViewport = readQueryVariable('scale', true);
rfb.resizeSession = readQueryVariable('resize', true);

</script>
</head>

<body>
<div id="top_bar">
<div id="status">Loading</div>
<div id="sendCtrlAltDelButton">Send CtrlAltDel</div>
<div id="fullScreenButton">Full Screen</div>
</div>
<div id="screen">
<!-- This is where the remote screen will appear -->
</div>
</body>
</html>

0 comments on commit 0a667be

Please sign in to comment.