Compare commits

...

38 Commits

Author SHA1 Message Date
f77763887c left note about deleting remote dir on remove jobs 2019-01-28 15:55:16 +01:00
420b230978 joblimit quick fix 2019-01-15 18:20:26 +01:00
d8c1ed992f fix email activation refresh 2019-01-15 18:11:41 +01:00
1835ef8be1 remove linting 2019-01-15 17:34:29 +01:00
032d8beae7 minor updates of deps, versioning and formatting 2018-10-23 08:37:29 +02:00
965e277948 more code cleanup to include standard functions in session
also now using local font awesome 4.7.0
2018-10-14 21:36:03 +02:00
30fb7ae1a2 added profiles, for joblimit and future features
also fixes to file upload, parameter reading and error handling
2018-10-14 19:13:47 +02:00
f28dcb2888 further cleanup and lock-down of required deps for views 2018-10-14 13:26:50 +02:00
687dec948e mustache templating, reorganisation and cleanup of code 2018-10-14 12:53:35 +02:00
527201ad89 improved file upload and font 2018-10-14 09:58:49 +02:00
fa63f76791 added codemirror optional js for syntax highlight of dockerfiles 2018-10-14 09:36:18 +02:00
81e0954a71 more web usage simplification and dashboard in place 2018-10-13 23:30:00 +02:00
de76c4887d deeper refactoring of web and failjure error propagation
this includes improvements that should be backported to the leiningen
template for dyne apps
2018-10-13 13:01:12 +02:00
d162d16116 moar logos to stand on the shoulders of giants 2018-10-13 13:01:12 +02:00
b1bb0a28ec Format the zsh command more nicely. 2018-10-11 14:55:48 +02:00
4da1117a3f Introduce stage3 downloads.
This expands the sync_jobs.py parameter to contain the release codename,
which is respectfully documented in the README.md file.
2018-10-11 14:51:52 +02:00
2be199a214 switched css to bulma, removed all javascript 2018-10-10 02:04:56 +02:00
fe944519ac completed basic gui functionalities linking to run and start 2018-10-08 16:41:55 +02:00
1e60140a6c Correct zshcmd for each of the sdks. 2018-10-08 14:20:49 +02:00
3ab620a588 Remove the blenddir after completing the job. 2018-10-08 14:18:41 +02:00
c216f578ce Copy over the blenddir when adding a job. 2018-10-08 14:12:42 +02:00
a26432d665 Remove the jobpath directory when a job is deleted. 2018-10-08 13:56:21 +02:00
c277e7a6a8 db storage, config fixes and some reformatting
started using intellij cursive for some live debugging, which altered
the indentation of files
2018-10-08 12:50:26 +02:00
a7deb67864 nrepl open on ring server and humanize deps 2018-10-08 12:50:26 +02:00
d245ab9939 fixup! Do not replace AT with @ when listing jobs. 2018-10-08 11:54:41 +02:00
c371a446d1 Do not replace AT with @ when listing jobs.
This partially reverts 8122c24339
2018-10-06 10:00:37 +02:00
5dd4ca18a9 further development with correct listing and adding of jobs 2018-10-05 19:28:52 +02:00
912a6d34a3 Add '-s' flag for querying status of a job. 2018-10-05 17:49:35 +02:00
8122c24339 Replace 'AT' with '@' when listing jobs. 2018-10-05 17:18:39 +02:00
23b96552a9 Allow listing jobs by account. 2018-10-05 17:16:07 +02:00
47397c18ab first working prototype to upload jobs to jenkins and list them 2018-10-05 17:13:15 +02:00
956d86743d Add a -l flag for listing the jobs in Jenkins. 2018-10-05 16:44:56 +02:00
234eb327ea Use a variable for the path(s) to our needed files. 2018-10-05 16:21:58 +02:00
7fce1b2cde dockerfile build and run 2018-10-05 15:12:13 +02:00
4c033ac86c base frontend website with authentication 2018-10-03 00:00:07 +02:00
507e941540 When adding a job, add it to a specific Jenkins view as well. 2018-09-26 19:37:35 +02:00
055933c5d1 Remove html_escape() and use the standard html library instead. 2018-09-26 19:27:01 +02:00
cff9839d83 Implement a flag and function for starting a job build. 2018-09-26 19:19:16 +02:00
48 changed files with 12718 additions and 39 deletions

View File

@@ -0,0 +1,19 @@
FROM dyne/clojure
LABEL maintainer="Denis Roio <jaromil@dyne.org>" \
homepage="https://github.com/dyne/agiladmin"
# ENV VERSION=AUTO_STRICT
EXPOSE 3000
WORKDIR /app
COPY . /app
RUN mkdir -p /usr/share/man/man1/ \
&& apt-get update \
&& apt-get install -y -q --allow-downgrades --no-install-recommends \
mongodb \
&& apt-get clean \
&& lein deps
CMD /etc/init.d/mongodb start \
&& lein ring server-headless

11
clojure_frontend/docker/build Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
target="$1"
if [ "$target" = "" ]; then target=develop; fi
if test -d src/toaster; then
docker build -f docker/Dockerfile -t dyne/toaster.do:$target .
else
echo "Launch from base source directiory. Usage:"
echo "./docker/build"
fi

5
clojure_frontend/docker/run Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
target="$1"
if [ "$target" = "" ]; then target=develop; fi
docker run -p 3000:3000 -it dyne/toaster.do:$target $*

45
clojure_frontend/package-lock.json generated Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "toaster",
"version": "0.1.0-SNAPSHOT",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"bulma": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.2.tgz",
"integrity": "sha512-6JHEu8U/1xsyOst/El5ImLcZIiE2JFXgvrz8GGWbnDLwTNRPJzdAM0aoUM1Ns0avALcVb6KZz9NhzmU53dGDcQ=="
},
"codemirror": {
"version": "5.40.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.40.2.tgz",
"integrity": "sha512-yoWuvEiD3v5vTwdoMc/wu/Ld6dh9K/yEiEBTKOPGM+/pN0gTAqFNtrLHv1IJ1UJvzFpNRvMi92XCi3+8/iIaEw=="
},
"dockerlint": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/dockerlint/-/dockerlint-0.3.9.tgz",
"integrity": "sha512-gps1IlRWx0hqhG7qZNYoF/Ae8wpnnPDGV0eYC60FdH2UscS4hZ+NFYX3Pusj/GImjLD/Pxkp/wib7CBb63yzZw==",
"requires": {
"sty": "^0.6.1",
"subarg": "^1.0.0"
}
},
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"sty": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/sty/-/sty-0.6.1.tgz",
"integrity": "sha1-3j+5rlcLxgp0RyRfDewxIT6i1ag="
},
"subarg": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
"integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=",
"requires": {
"minimist": "^1.1.0"
}
}
}
}

View File

@@ -0,0 +1,55 @@
(defproject toaster "0.1.0-SNAPSHOT"
:description "Basic compojure based authenticated website"
:url "http://dyne.org"
:min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.9.0"]
[org.clojure/data.csv "0.1.4"]
[compojure "1.6.1"]
[ring/ring-defaults "0.3.2"]
[ring-middleware-accept "2.0.3"]
;; mustache templates
[de.ubercode.clostache/clostache "1.4.0"]
;; error handling
[failjure "1.3.0"]
;; logging done right with timbre
[com.taoensso/timbre "4.10.0"]
;; authentication library
[org.clojars.dyne/just-auth "0.4.0"]
;; web forms made easy
[formidable "0.1.10"]
;; parsing configs if any
[io.forward/yaml "1.0.9"]
;; Data validation
[prismatic/schema "1.1.9"]
;; filesystem utilities
[me.raynes/fs "1.4.6"]
[me.raynes/conch "0.8.0"]
;; time from joda-time
[clj-time "0.15.0"]
[clojure-humanize "0.2.2"]
[de.ubercode.clostache/clostache "1.4.0"]]
:aliases {"test" "midje"}
:source-paths ["src"]
:resource-paths ["resources"]
:plugins [[lein-ring "0.12.4"]]
:ring {:init toaster.ring/init
:handler toaster.handler/app
:nrepl {:start? true
:host "localhost"
:port 40231}}
:uberwar {:init toaster.ring/init
:handler toaster.handler/app}
:mail toaster.handler
:npm {:dependencies [[dockerlint "0.3.9"]
[bulma "0.7.2"]
[codemirror "5.40.2"]]}
:profiles { :dev {:dependencies [[javax.servlet/servlet-api "2.5"]
[ring/ring-mock "0.3.2"]
[midje "1.9.4"]]
:plugins [[lein-midje "3.1.3"]
[lein-npm "0.6.2"]]
:aot :all
:main toaster.handler}
:uberjar {:aot :all
:main toaster.handler}}
)

View File

@@ -0,0 +1,6 @@
---
init:
success: "Initialisation completed"
failure: "Initialization failed"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,346 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

View File

@@ -0,0 +1,5 @@
/*!
* Font Awesome Free 5.0.6 by @fontawesome - http://fontawesome.com
* License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(/static/fonts/fa-regular-400.eot);src:url(/static/fonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(/static/fonts/fa-regular-400.woff2) format("woff2"),url(/static/fonts/fa-regular-400.woff) format("woff"),url(/static/fonts/fa-regular-400.ttf) format("truetype"),url(/static/fonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,46 @@
.jsondiffpatch-annotated-delta {
font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace;
font-size: 12px;
margin: 0;
padding: 0 0 0 12px;
display: inline-block;
}
.jsondiffpatch-annotated-delta pre {
font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace;
font-size: 12px;
margin: 0;
padding: 0;
display: inline-block;
}
.jsondiffpatch-annotated-delta td {
margin: 0;
padding: 0;
}
.jsondiffpatch-annotated-delta td pre:hover {
font-weight: bold;
}
td.jsondiffpatch-delta-note {
font-style: italic;
padding-left: 10px;
}
.jsondiffpatch-delta-note > div {
margin: 0;
padding: 0;
}
.jsondiffpatch-delta-note pre {
font-style: normal;
}
.jsondiffpatch-annotated-delta .jsondiffpatch-delta-note {
color: #777;
}
.jsondiffpatch-annotated-delta tr:hover {
background: #ffc;
}
.jsondiffpatch-annotated-delta tr:hover > td.jsondiffpatch-delta-note {
color: black;
}
.jsondiffpatch-error {
background: red;
color: white;
font-weight: bold;
}

View File

@@ -0,0 +1,149 @@
.jsondiffpatch-delta {
font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace;
font-size: 12px;
margin: 0;
padding: 0 0 0 12px;
display: inline-block;
}
.jsondiffpatch-delta pre {
font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace;
font-size: 12px;
margin: 0;
padding: 0;
display: inline-block;
}
ul.jsondiffpatch-delta {
list-style-type: none;
padding: 0 0 0 20px;
margin: 0;
}
.jsondiffpatch-delta ul {
list-style-type: none;
padding: 0 0 0 20px;
margin: 0;
}
.jsondiffpatch-added .jsondiffpatch-property-name,
.jsondiffpatch-added .jsondiffpatch-value pre,
.jsondiffpatch-modified .jsondiffpatch-right-value pre,
.jsondiffpatch-textdiff-added {
background: #bbffbb;
}
.jsondiffpatch-deleted .jsondiffpatch-property-name,
.jsondiffpatch-deleted pre,
.jsondiffpatch-modified .jsondiffpatch-left-value pre,
.jsondiffpatch-textdiff-deleted {
background: #ffbbbb;
text-decoration: line-through;
}
.jsondiffpatch-unchanged,
.jsondiffpatch-movedestination {
color: gray;
}
.jsondiffpatch-unchanged,
.jsondiffpatch-movedestination > .jsondiffpatch-value {
transition: all 0.5s;
-webkit-transition: all 0.5s;
overflow-y: hidden;
}
.jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged,
.jsondiffpatch-unchanged-showing .jsondiffpatch-movedestination > .jsondiffpatch-value {
max-height: 100px;
}
.jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged,
.jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value {
max-height: 0;
}
.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value,
.jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value {
display: block;
}
.jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged,
.jsondiffpatch-unchanged-visible .jsondiffpatch-movedestination > .jsondiffpatch-value {
max-height: 100px;
}
.jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged,
.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value {
max-height: 0;
}
.jsondiffpatch-unchanged-showing .jsondiffpatch-arrow,
.jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow {
display: none;
}
.jsondiffpatch-value {
display: inline-block;
}
.jsondiffpatch-property-name {
display: inline-block;
padding-right: 5px;
vertical-align: top;
}
.jsondiffpatch-property-name:after {
content: ': ';
}
.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after {
content: ': [';
}
.jsondiffpatch-child-node-type-array:after {
content: '],';
}
div.jsondiffpatch-child-node-type-array:before {
content: '[';
}
div.jsondiffpatch-child-node-type-array:after {
content: ']';
}
.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after {
content: ': {';
}
.jsondiffpatch-child-node-type-object:after {
content: '},';
}
div.jsondiffpatch-child-node-type-object:before {
content: '{';
}
div.jsondiffpatch-child-node-type-object:after {
content: '}';
}
.jsondiffpatch-value pre:after {
content: ',';
}
li:last-child > .jsondiffpatch-value pre:after,
.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after {
content: '';
}
.jsondiffpatch-modified .jsondiffpatch-value {
display: inline-block;
}
.jsondiffpatch-modified .jsondiffpatch-right-value {
margin-left: 5px;
}
.jsondiffpatch-moved .jsondiffpatch-value {
display: none;
}
.jsondiffpatch-moved .jsondiffpatch-moved-destination {
display: inline-block;
background: #ffffbb;
color: #888;
}
.jsondiffpatch-moved .jsondiffpatch-moved-destination:before {
content: ' => ';
}
ul.jsondiffpatch-textdiff {
padding: 0;
}
.jsondiffpatch-textdiff-location {
color: #bbb;
display: inline-block;
min-width: 60px;
}
.jsondiffpatch-textdiff-line {
display: inline-block;
}
.jsondiffpatch-textdiff-line-number:after {
content: ',';
}
.jsondiffpatch-error {
background: red;
color: white;
font-weight: bold;
}

View File

@@ -0,0 +1,72 @@
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
background: white;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@@ -0,0 +1,110 @@
/* css for [json-html.core] */
.jh-type-string, .jh-type-bool, .jh-type-number{
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 20px;
color: #333;
float: left;
width: 140px;
padding-top: 5px;
text-align: right;
margin-bottom: 10px;
}
.jh-root, .jh-type-object, .jh-type-array, .jh-key, .jh-value, .jh-root tr{
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
}
.jh-key, .jh-value{
margin: 0;
padding: 0.2em;
}
.jh-empty-collection:before {
content: '[]';
}
.jh-empty-map:before {
content: '{}';
}
.jh-empty-set:before {
content: '#{}';
}
.jh-empty-string:before {
content: '""';
}
.jh-value{
border-left: 1px solid #ddd;
}
.jh-type-bool, .jh-type-number{
font-weight: bold;
color: #333;
}
.jh-type-string{
color: #333;
}
.jh-type-date{
color: #333;
}
.jh-array-key{
font-size: small;
text-align: center;
}
.jh-object-key, .jh-array-key{
color: #444;
vertical-align: top;
}
.jh-type-object > tbody > tr:nth-child(odd), .jh-type-array > tbody > tr:nth-child(odd){
background-color: #f5f5f5;
}
.jh-type-object > tbody > tr:nth-child(even), .jh-type-array > tbody > tr:nth-child(even){
background-color: #fff;
}
.jh-type-object, .jh-type-array{
width: 100%;
border-collapse: collapse;
}
.jh-root{
border: 1px solid #ccc;
margin: 0.2em;
}
th.jh-key{
text-align: left;
}
.jh-type-object > tbody > tr, .jh-type-array > tr{
border: 1px solid #ddd;
border-bottom: none;
}
.jh-type-object > tbody > tr:last-child, .jh-type-array > tbody > tr:last-child{
border-bottom: 1px solid #ddd;
}
.jh-type-object > tbody > tr:hover, .jh-type-array > tbody > tr:hover{
border: 1px solid #F99927;
}
.jh-empty{
color: #999;
font-size: small;
}

View File

@@ -0,0 +1,133 @@
body {
/* padding-top: 65px; */
}
/* horizontal margins on wide screens */
@media (min-width: 768px) {
.container { width: 36em; }
}
code {
font-size: .8em;
/* display: block; */
white-space: pre-wrap;
}
.editor {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
/* .btn { */
/* margin: 0 1.5em 0 1.5em; */
/* } */
.edit-project {
margin-top: 2em;
margin-bottom: 2em;
}
h1, h2, h3, h4 { }
html,body {
font-family: 'Ubuntu', sans-serif;
font-size: 14px;
font-weight: 300;
font-smooth: always;
-webkit-font-smoothing: antialiased;
}
.hero.is-success {
background: #F2F6FA;
}
.hero .nav, .hero.is-success .nav {
-webkit-box-shadow: none;
box-shadow: none;
}
.box {
margin-top: 5rem;
}
.avatar {
margin-top: -70px;
padding-bottom: 20px;
}
.avatar img {
padding: 5px;
background: #fff;
border-radius: 50%;
-webkit-box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1);
box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1);
}
input {
font-weight: 300;
}
p {
font-weight: 700;
}
p.subtitle {
padding-top: 1rem;
}
/* inputfile js for bulma from https://jsfiddle.net/chintanbanugaria/uzva5byy/ */
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
z-index: -1;
margin-top:10px;
}
.inputfile + label {
/* 20px */
width: 50%;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
display: inline-block;
overflow: hidden;
padding-left: 0.75em;
padding-right: 0.75em;
border: 1px solid #dbdbdb;
height: 2.285em;
box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1);
line-height: 1.5rem;
border-radius: 3px;
padding-top: 0.3em;
/* 10px 20px */
}
.no-js .inputfile + label {
display: none;
}
.inputfile:focus + label,
.inputfile.has-focus + label {
border-color: #00d1b2;
}
.inputfile + label * {
/* pointer-events: none; */
/* in case of FastClick lib use */
}
.inputfile + label svg {
width: 1em;
height: 1em;
line-height: 1.5rem;
vertical-align: middle;
margin-top: -0.25em;
/* 4px */
margin-right: 0.25em;
/* 4px */
}
.inputfile-2 + label {
border: 1px solid #dbdbdb;
}
.inputfile-2:focus + label,
.inputfile-2.has-focus + label,
.inputfile-2 + label:hover {
border-color: #00d1b2;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,211 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("../../addon/mode/simple"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror", "../../addon/mode/simple"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var from = "from";
var fromRegex = new RegExp("^(\\s*)\\b(" + from + ")\\b", "i");
var shells = ["run", "cmd", "entrypoint", "shell"];
var shellsAsArrayRegex = new RegExp("^(\\s*)(" + shells.join('|') + ")(\\s+\\[)", "i");
var expose = "expose";
var exposeRegex = new RegExp("^(\\s*)(" + expose + ")(\\s+)", "i");
var others = [
"arg", "from", "maintainer", "label", "env",
"add", "copy", "volume", "user",
"workdir", "onbuild", "stopsignal", "healthcheck", "shell"
];
// Collect all Dockerfile directives
var instructions = [from, expose].concat(shells).concat(others),
instructionRegex = "(" + instructions.join('|') + ")",
instructionOnlyLine = new RegExp("^(\\s*)" + instructionRegex + "(\\s*)(#.*)?$", "i"),
instructionWithArguments = new RegExp("^(\\s*)" + instructionRegex + "(\\s+)", "i");
CodeMirror.defineSimpleMode("dockerfile", {
start: [
// Block comment: This is a line starting with a comment
{
regex: /^\s*#.*$/,
sol: true,
token: "comment"
},
{
regex: fromRegex,
token: [null, "keyword"],
sol: true,
next: "from"
},
// Highlight an instruction without any arguments (for convenience)
{
regex: instructionOnlyLine,
token: [null, "keyword", null, "error"],
sol: true
},
{
regex: shellsAsArrayRegex,
token: [null, "keyword", null],
sol: true,
next: "array"
},
{
regex: exposeRegex,
token: [null, "keyword", null],
sol: true,
next: "expose"
},
// Highlight an instruction followed by arguments
{
regex: instructionWithArguments,
token: [null, "keyword", null],
sol: true,
next: "arguments"
},
{
regex: /./,
token: null
}
],
from: [
{
regex: /\s*$/,
token: null,
next: "start"
},
{
// Line comment without instruction arguments is an error
regex: /(\s*)(#.*)$/,
token: [null, "error"],
next: "start"
},
{
regex: /(\s*\S+\s+)(as)/i,
token: [null, "keyword"],
next: "start"
},
// Fail safe return to start
{
token: null,
next: "start"
}
],
single: [
{
regex: /(?:[^\\']|\\.)/,
token: "string"
},
{
regex: /'/,
token: "string",
pop: true
}
],
double: [
{
regex: /(?:[^\\"]|\\.)/,
token: "string"
},
{
regex: /"/,
token: "string",
pop: true
}
],
array: [
{
regex: /\]/,
token: null,
next: "start"
},
{
regex: /"(?:[^\\"]|\\.)*"?/,
token: "string"
}
],
expose: [
{
regex: /\d+$/,
token: "number",
next: "start"
},
{
regex: /[^\d]+$/,
token: null,
next: "start"
},
{
regex: /\d+/,
token: "number"
},
{
regex: /[^\d]+/,
token: null
},
// Fail safe return to start
{
token: null,
next: "start"
}
],
arguments: [
{
regex: /^\s*#.*$/,
sol: true,
token: "comment"
},
{
regex: /"(?:[^\\"]|\\.)*"?$/,
token: "string",
next: "start"
},
{
regex: /"/,
token: "string",
push: "double"
},
{
regex: /'(?:[^\\']|\\.)*'?$/,
token: "string",
next: "start"
},
{
regex: /'/,
token: "string",
push: "single"
},
{
regex: /[^#"']+[\\`]$/,
token: null
},
{
regex: /[^#"']+$/,
token: null,
next: "start"
},
{
regex: /[^#"']+/,
token: null
},
// Fail safe return to start
{
token: null,
next: "start"
}
],
meta: {
lineComment: "#"
}
});
CodeMirror.defineMIME("text/x-dockerfile", "dockerfile");
});

View File

@@ -0,0 +1,216 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineSimpleMode = function(name, states) {
CodeMirror.defineMode(name, function(config) {
return CodeMirror.simpleMode(config, states);
});
};
CodeMirror.simpleMode = function(config, states) {
ensureState(states, "start");
var states_ = {}, meta = states.meta || {}, hasIndentation = false;
for (var state in states) if (state != meta && states.hasOwnProperty(state)) {
var list = states_[state] = [], orig = states[state];
for (var i = 0; i < orig.length; i++) {
var data = orig[i];
list.push(new Rule(data, states));
if (data.indent || data.dedent) hasIndentation = true;
}
}
var mode = {
startState: function() {
return {state: "start", pending: null,
local: null, localState: null,
indent: hasIndentation ? [] : null};
},
copyState: function(state) {
var s = {state: state.state, pending: state.pending,
local: state.local, localState: null,
indent: state.indent && state.indent.slice(0)};
if (state.localState)
s.localState = CodeMirror.copyState(state.local.mode, state.localState);
if (state.stack)
s.stack = state.stack.slice(0);
for (var pers = state.persistentStates; pers; pers = pers.next)
s.persistentStates = {mode: pers.mode,
spec: pers.spec,
state: pers.state == state.localState ? s.localState : CodeMirror.copyState(pers.mode, pers.state),
next: s.persistentStates};
return s;
},
token: tokenFunction(states_, config),
innerMode: function(state) { return state.local && {mode: state.local.mode, state: state.localState}; },
indent: indentFunction(states_, meta)
};
if (meta) for (var prop in meta) if (meta.hasOwnProperty(prop))
mode[prop] = meta[prop];
return mode;
};
function ensureState(states, name) {
if (!states.hasOwnProperty(name))
throw new Error("Undefined state " + name + " in simple mode");
}
function toRegex(val, caret) {
if (!val) return /(?:)/;
var flags = "";
if (val instanceof RegExp) {
if (val.ignoreCase) flags = "i";
val = val.source;
} else {
val = String(val);
}
return new RegExp((caret === false ? "" : "^") + "(?:" + val + ")", flags);
}
function asToken(val) {
if (!val) return null;
if (val.apply) return val
if (typeof val == "string") return val.replace(/\./g, " ");
var result = [];
for (var i = 0; i < val.length; i++)
result.push(val[i] && val[i].replace(/\./g, " "));
return result;
}
function Rule(data, states) {
if (data.next || data.push) ensureState(states, data.next || data.push);
this.regex = toRegex(data.regex);
this.token = asToken(data.token);
this.data = data;
}
function tokenFunction(states, config) {
return function(stream, state) {
if (state.pending) {
var pend = state.pending.shift();
if (state.pending.length == 0) state.pending = null;
stream.pos += pend.text.length;
return pend.token;
}
if (state.local) {
if (state.local.end && stream.match(state.local.end)) {
var tok = state.local.endToken || null;
state.local = state.localState = null;
return tok;
} else {
var tok = state.local.mode.token(stream, state.localState), m;
if (state.local.endScan && (m = state.local.endScan.exec(stream.current())))
stream.pos = stream.start + m.index;
return tok;
}
}
var curState = states[state.state];
for (var i = 0; i < curState.length; i++) {
var rule = curState[i];
var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex);
if (matches) {
if (rule.data.next) {
state.state = rule.data.next;
} else if (rule.data.push) {
(state.stack || (state.stack = [])).push(state.state);
state.state = rule.data.push;
} else if (rule.data.pop && state.stack && state.stack.length) {
state.state = state.stack.pop();
}
if (rule.data.mode)
enterLocalMode(config, state, rule.data.mode, rule.token);
if (rule.data.indent)
state.indent.push(stream.indentation() + config.indentUnit);
if (rule.data.dedent)
state.indent.pop();
var token = rule.token
if (token && token.apply) token = token(matches)
if (matches.length > 2 && rule.token && typeof rule.token != "string") {
state.pending = [];
for (var j = 2; j < matches.length; j++)
if (matches[j])
state.pending.push({text: matches[j], token: rule.token[j - 1]});
stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0));
return token[0];
} else if (token && token.join) {
return token[0];
} else {
return token;
}
}
}
stream.next();
return null;
};
}
function cmp(a, b) {
if (a === b) return true;
if (!a || typeof a != "object" || !b || typeof b != "object") return false;
var props = 0;
for (var prop in a) if (a.hasOwnProperty(prop)) {
if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) return false;
props++;
}
for (var prop in b) if (b.hasOwnProperty(prop)) props--;
return props == 0;
}
function enterLocalMode(config, state, spec, token) {
var pers;
if (spec.persistent) for (var p = state.persistentStates; p && !pers; p = p.next)
if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode == p.mode) pers = p;
var mode = pers ? pers.mode : spec.mode || CodeMirror.getMode(config, spec.spec);
var lState = pers ? pers.state : CodeMirror.startState(mode);
if (spec.persistent && !pers)
state.persistentStates = {mode: mode, spec: spec.spec, state: lState, next: state.persistentStates};
state.localState = lState;
state.local = {mode: mode,
end: spec.end && toRegex(spec.end),
endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false),
endToken: token && token.join ? token[token.length - 1] : token};
}
function indexOf(val, arr) {
for (var i = 0; i < arr.length; i++) if (arr[i] === val) return true;
}
function indentFunction(states, meta) {
return function(state, textAfter, line) {
if (state.local && state.local.mode.indent)
return state.local.mode.indent(state.localState, textAfter, line);
if (state.indent == null || state.local || meta.dontIndentStates && indexOf(state.state, meta.dontIndentStates) > -1)
return CodeMirror.Pass;
var pos = state.indent.length - 1, rules = states[state.state];
scan: for (;;) {
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (rule.data.dedent && rule.data.dedentIfLineStart !== false) {
var m = rule.regex.exec(textAfter);
if (m && m[0]) {
pos--;
if (rule.next || rule.push) rules = states[rule.next || rule.push];
textAfter = textAfter.slice(m[0].length);
continue scan;
}
}
}
break;
}
return pos < 0 ? 0 : state.indent[pos];
};
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
<div class="box">
<h1 class="title">Upload a Dockerfile to toast</h1>
<p>Choose the file in your computer and click 'Submit' to proceed to validation.</p>
<form action="dockerfile" class="form" enctype="multipart/form-data" method="post">
<div class="has-label is-fullwidth">
<label class="file-label">
<input name="file" class="file-input inputfile inputfile-2" id="file" type="file" />
<label for="file"><span id="filename">Choose a Dockerfile...</span></label>
<span class="file-cta"><span class="file-icon"><i class="fa fa-upload"></i></span><span class="file-label">Upload</span></span>
</label></div>
<div class="field">
<div class="control">
<input class="button is-block is-info is-large is-fullwidth" name="submit" type="submit" value="submit" />
</div></div>
</form>
<script>
var file = document.getElementById("file");
file.onchange = function(){
if(file.files.length > 0) {
document.getElementById('filename').innerHTML = file.files[0].name;
}
};
</script>
</div>

View File

@@ -0,0 +1,28 @@
<footer class="footer">
<div class="content has-text-centered">
<p class="has-text-grey">toaster.do transforms your Docker prototype into an installable <a href="https://devuan.org">Devuan GNU+Linux</a> image, choose any supported target architecture.</p>
<div class="columns is-variable is-8">
<div class="column is-one-fifth">
<figure class="image"><img alt="European project DECODE (H2020 nr. 732546)" src="/static/img/ec_logo.png" /></figure>
</div>
<div class="column is-one-fifth">
<a href="https://decodeproject.eu">
<figure class="image"><img alt="DECODE project" src="/static/img/decode_logo.png" /></figure>
</a>
</div>
<div class="column is-one-fifth">
<a href="https://www.dyne.org">
<figure class="image"><img alt="Software by Dyne.org" src="/static/img/swbydyne.png" title="Software by Dyne.org" /></figure>
</a>
</div>
<div class="column is-one-fifth">
<a href="https://devuan.org">
<figure class="image"><img alt="powered by Devuan GNU+Linux" src="/static/img/devuan_logo.png" /></figure>
</a>
</div>
<div class="column is-one-fifth">
<figure class="image"><img alt="Affero GPLv3 License" src="/static/img/AGPLv3.png" title="Affero GPLv3 License" /></figure>
</div>
</div>
</div>
</footer>

View File

@@ -0,0 +1,27 @@
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container has-text-centered">
<div class="column is-4 is-offset-4">
<h3 class="title has-text-grey">toaster.do</h3>
<h4 class="subtitle has-text-grey">from Docker to VM in a few clicks, powered by <a href="https://decodeproject.eu">DECODEproject.EU</a></h4>
<p class="subtitle has-text-grey">please login to operate</p>
<div class="box">
<figure class="avatar"><img src="/static/img/cafudda.jpg" /></figure>
<form action="/login" method="post">
<div class="field">
<div class="control has-icons-left">
<input class="input is-large" name="username" placeholder="Email" type="email" /><span class="icon is-small is-left"><i class="fa fa-envelope fa-xs"></i></span></div>
</div>
<div class="field">
<div class="control has-icons-left"><input class="input is-large" name="password" placeholder="Password" type="password" /><span class="icon is-small is-left"><i class="fa fa-lock fa-xs"></i></span></div>
</div>
<div class="field">
<p class="control"><input class="button is-block is-info is-large has-icons-left is-fullwidth" type="submit" value="Login" /></p>
</div>
</form>
</div>
<p class="subtitle has-text-grey">...or <a href="/signup">signup for a new account</a></p>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,33 @@
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container has-text-centered">
<div class="column is-4 is-offset-4">
<h3 class="title has-text-grey">toaster.do</h3>
<h4 class="subtitle has-text-grey">sign up for a new account</h4>
<div class="box">
<figure class="avatar"><img src="/static/img/cafudda.jpg" /></figure>
<form action="/signup" method="post">
<div class="field">
<div class="control"><input class="input" name="name" placeholder="Name" type="text" /></div>
</div>
<div class="field">
<div class="control"><input class="input" name="email" placeholder="Email" type="email" /></div>
</div>
<div class="field">
<div class="control"><input class="input" name="password" placeholder="Password" type="password" /></div>
</div>
<div class="field">
<div class="control"><input class="input" name="repeat-password" placeholder="Repeat password" type="password" /></div>
</div>
<div class="field">
<div class="control"><input class="button is-block is-info is-large is-fullwidth" type="submit" value="Sign In" /></div>
</div>
</form>
</div>
<p class="subtitle has-text-grey">...or <a href="/login">login with an existing account</a></p>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,15 @@
<head>
<meta charset="utf-8" />
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>toaster.do :: rapid integration for decentralized services, from embedded to the cloud</title>
<link href="https://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" />
<script src="/static/js/codemirror.js" type="text/javascript"></script>
<script src="/static/js/codemirror-simple.js" type="text/javascript"></script>
<script src="/static/js/codemirror-dockerfile.js" type="text/javascript"></script>
<link href="/static/css/bulma.min.css" rel="stylesheet" type="text/css" />
<link href="/static/css/json-html.css" rel="stylesheet" type="text/css" />
<link href="/static/css/codemirror.css" rel="stylesheet" type="text/css" />
<link href="/static/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link href="/static/css/toaster.css" rel="stylesheet" type="text/css" />
</head>

View File

@@ -0,0 +1,110 @@
;; Copyright (C) 2015-2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.bulma
(:require [clojure.java.io :as io]
[clojure.data.csv :as csv]
[taoensso.timbre :as log]
[yaml.core :as yaml]
[hiccup.page :as page]
[hiccup.form :as hf]))
(defn button
([url text] (button url text [:p]))
([url text field] (button url text field "button"))
([url text field type]
(hf/form-to [:post url]
field ;; can be an hidden key/value field (project,
;; person, etc using hf/hidden-field)
[:p {:class "control"}
(hf/submit-button {:class (str "button " type)} text)])))
(defn button-cancel-submit [argmap]
[:div
{:class
(str "row col-md-6 btn-group btn-group-lg "
(:btn-group-class argmap))
:role "group"}
(button
(:cancel-url argmap) "Cancel"
(:cancel-params argmap)
"btn-primary btn-lg btn-danger col-md-3")
(button
(:submit-url argmap) "Submit"
(:submit-params argmap)
"btn-primary btn-lg btn-success col-md-3")])
;; highlight functions do no conversion, take the format they highlight
;; render functions take edn and convert to the highlight format
;; download functions all take an edn and convert it in target format
;; edit functions all take an edn and present an editor in the target format
(defn render-yaml
"renders an edn into an highlighted yaml"
[data]
[:span
[:pre [:code {:class "yaml"}
(yaml/generate-string data)]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn highlight-yaml
"renders a yaml text in highlighted html"
[data]
[:span
[:pre [:code {:class "yaml"}
data]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn highlight-json
"renders a json text in highlighted html"
[data]
[:span
[:pre [:code {:class "json"}
data]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn download-csv
"takes an edn, returns a csv plain/text for download"
[data]
{:headers {"Content-Type"
"text/plain; charset=utf-8"}
:body (with-out-str (csv/write-csv *out* data))})
(defn edit-edn
"renders an editor for the edn in yaml format"
[data]
[:div;; {:class "form-group"}
[:textarea {:class "form-control"
:rows "20" :data-editor "yaml"
:id "config" :name "editor"}
(yaml/generate-string data)]
[:script {:src "/static/js/ace.js"
:type "text/javascript" :charset "utf-8"}]
[:script {:type "text/javascript"}
(slurp (io/resource "public/static/js/ace-embed.js"))]
;; code to embed the ace editor on all elements in page
;; that contain the attribute "data-editor" set to the
;; mode language of choice
[:input {:class "btn btn-success btn-lg pull-top"
:type "submit" :value "submit"}]])
(def brand-img "/static/img/cafudda.jpg")

View File

@@ -0,0 +1,101 @@
;; Copyright (C) 2015-2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.config
(:require [clojure.pprint :refer [pprint]]
[clojure.string :refer [upper-case]]
[clojure.java.io :as io]
[clojure.walk :refer [keywordize-keys]]
[auxiliary.core :as aux]
[taoensso.timbre :as log]
[failjure.core :as f]
[schema.core :as s]
[yaml.core :as yaml]
[cheshire.core :refer :all]))
;; (s/defschema Config
;; {s/Keyword
;; (s/optional-key :webserver) {:port s/Num
;; :host s/Str
;; :anti-forgery s/Bool
;; :ssl-redirect s/Bool}
;; (s/optional-key :just-auth) {:email-server s/Str
;; :email-user s/Str
;; :email-pass s/Str
;; :email-address s/Str}
;; }
;; :appname s/Str
;; :paths [s/Str]
;; :filename s/Str
;; })
(def run-mode (atom :web))
(def default-settings {:webserver
{:anti-forgery false
:ssl-redirect false}})
(defn yaml-read [path]
(if (.exists (io/as-file path))
(-> path yaml/from-file keywordize-keys)))
(defn- config-read
"Read configurations from standard locations, overriding defaults or
system-wide with user specific paths. Requires the application name
and optionally default values."
([appname] (config-read appname {}))
([appname defaults & flags]
(let [home (System/getenv "HOME")
pwd (System/getenv "PWD" )
file (str appname ".yaml")
conf (-> {:appname appname
:filename file
:paths [(str "/etc/" appname "/" file)
(str home "/." appname "/" file)
(str pwd "/" file)
;; TODO: this should be resources
(str pwd "/resources/" file)
(str pwd "/test-resources/" file)]}
(conj defaults))]
(loop [[p & paths] (:paths conf)
res defaults]
(let [res (merge res
(if (.exists (io/as-file p))
(conj res (yaml-read p))))]
(if (empty? paths) (conj conf {(keyword appname) res})
(recur paths res)))))))
(defn- spy "Print out a config structure nicely formatted"
[edn]
(if (log/may-log? :debug)
(binding [*out* *err*] (pprint edn)))
edn)
(defn q [conf path] ;; query a variable inside the config
{:pre [(coll? path)]}
;; (try ;; adds an extra check every time configuration is read
;; (s/validate Config conf)
;; (catch Exception ex
;; (f/fail (log/spy :error ["Invalid configuration: " conf ex]))))
(get-in conf (into [:toaster] path)))
(defn load-config [name default]
(log/info (str "Loading configuration: " name))
(->> (config-read name default)))

View File

@@ -0,0 +1,201 @@
;; Copyright (C) 2015-2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.handler
(:require
[clojure.string :as str]
[clojure.java.io :as io]
[clojure.data.json :as json]
[compojure.core :refer :all]
[compojure.handler :refer :all]
[compojure.route :as route]
[compojure.response :as response]
[ring.adapter.jetty :refer :all]
[ring.middleware.session :refer :all]
[ring.middleware.accept :refer [wrap-accept]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[failjure.core :as f]
[taoensso.timbre :as log]
[just-auth.core :as auth]
[toaster.session :as s]
[toaster.config :as conf]
[toaster.ring :as ring]
[toaster.views :as views]
[toaster.profiles :as profile])
(:gen-class))
(defonce config (conf/load-config "toaster" conf/default-settings))
(defn auth-wrap
"Comfortably wrap routes using a function pointer to be called if an
account is correctly authenticated in the currently running session.
(fun) is passed 3 hash-maps args: request, config and account."
[request fun]
(f/attempt-all
[db (s/check-database)
config (s/check-config request)
account (if (conf/q config [:webserver :mock-auth])
{:email "mock@dyne.org"
:name "MockUser"
:activated true}
;; else
(s/check-account request))]
(fun request config account)
(f/when-failed [e]
(s/render [:body
(s/notify (f/message e) "is-error")
(s/resource s/login)]))))
(defroutes
app-routes
(GET "/" request
(f/attempt-all
[db (s/check-database)
conf (s/check-config request)]
(f/if-let-ok? [account (s/check-account request)]
(s/render [:body (views/dashboard account)])
;; else
(s/render [:body (s/resource s/login)]))
(f/when-failed[e]
(s/render [:body (s/error "backend missing" e)]))))
;; NEW ROUTES HERE
(POST "/dockerfile" request
(->> (fn [req conf acct]
(s/render acct
[:body
(s/upload req conf acct views/add-job)
(views/dashboard acct)]))
(auth-wrap request)))
(POST "/remove" request
(->> (fn [req conf acct]
(s/render acct [:body
(views/remove-job req conf acct)
(views/dashboard acct)]))
(auth-wrap request)))
(POST "/start" request
(->> (fn [req conf acct]
(s/render acct [:body
(views/start-job req conf acct)
(views/dashboard acct)]))
(auth-wrap request)))
(POST "/view" request
(->> (fn [req conf acct]
(s/render acct [:body
(views/view-job req conf acct)
(views/dashboard acct)]))
(auth-wrap request)))
(GET "/error" request
(->> (fn [req conf acct]
(s/render acct [:body
(s/notify "Generic Error Page" "is-error")
(views/dashboard acct)]))
(auth-wrap request)))
;; JUST-AUTH ROUTES
(GET "/login" request (s/render-template s/login))
(POST "/login" request
(f/attempt-all
[username (s/param request :username)
password (s/param request :password)
logged (auth/sign-in
@ring/auth username password {})]
;; TODO: pass :ip-address in last argument map
(let [session {:session {:config config
:auth logged}}]
(conj session
(s/render logged [:body (views/dashboard logged)])))
(f/when-failed [e]
(s/render
[:body
(s/error "Login failed" e)]))))
(GET "/logout" request
(conj {:session {:config config}}
(s/render [:body
[:h1 {:class "title"} "Logged out."]])))
(GET "/signup" request (s/render-template s/signup))
(POST "/signup" request
(s/adduser request
(fn [name email]
(s/render
[:body
(s/notify (str "Account created: "
name " &lt;" email "&gt;") "is-success")
[:h1 {:class "title"} "Check email for activation."]]))))
(GET "/activate/:email/:activation-id"
[email activation-id :as request]
(let [activation-uri
(str "http://"
(get-in request [:headers "host"])
"/activate/" email "/" activation-id)]
(f/attempt-all
[act (auth/activate-account
@ring/auth email
{:activation-link activation-uri})]
(s/render email
[:body
(s/notify "Account succesfully activated" "is-success")
(views/dashboard email)])
(f/when-failed [e]
(s/render
[:body
(s/error "Failure activating account" e)])))))
;; -- end of JUST-AUTH
(POST "/" request
;; generic endpoint for canceled operations
(s/render (s/check-account request)
(s/notify
(s/param request :message) "is-error")))
(route/resources "/")
(route/not-found (s/render [:body (s/notify "Page Not Found" "is-error")]))
) ;; end of routes
(def app
(-> (wrap-defaults app-routes ring/app-defaults)
(wrap-accept {:mime ["text/html"]
;; preference in language, fallback to english
:language ["en" :qs 0.5
"it" :qs 1
"nl" :qs 1
"hr" :qs 1]})
(wrap-session)))
;; for uberjar
(defn -main []
(println "Starting ring server")
(ring/init ring/app-defaults)
;(run-jetty app {:port 6060
; :host "localhost"
; :join? true})
)

View File

@@ -0,0 +1,90 @@
(ns toaster.jobs
(:require
[clojure.string :as str]
[clojure.java.io :as io]
[clj-time.core :as time]
[clj-time.coerce :as tc]
[clj-storage.core :as db]
[failjure.core :as f]
[taoensso.timbre :as log :refer [debug]]
[me.raynes.conch :as sh :refer [with-programs]]
[toaster.config :refer :all]
[toaster.ring :refer [jobs]]
[toaster.profiles :as profile]
[hiccup.form :as hf]))
;; list of arm targets to chose from
;"beagleboneblack"
;"chromeacer"
;"chromeveyron"
;"droid4"
;"n900"
;"odroidxu4"
;"odroidxu"
;"ouya"
;"raspi1"
;"raspi2"
;"raspi3"
;"rock64"
;"sunxi"
;"turbox-twister"
(defn- ssh-host [config]
(str (q config [:jenkins :user]) "@" (q config [:jenkins :host])))
(defn sync_jobs [config arg1 arg2]
(with-programs
[ssh]
(try
(-> (ssh "-i" (q config [:jenkins :key])
(ssh-host config) "sync_jobs.py"
arg1 arg2)
(str/split #"\n"))
(catch Exception e (f/fail (str "ERROR in sync_jobs.py "
arg1 " " arg2 " - " (.getMessage e)))))))
(defn- dockerlint [path]
(with-programs [node]
(try
(node "node_modules/dockerlint/bin/dockerlint.js" path)
(catch Exception e
(f/fail (str "ERROR in dockerlint - " (.getMessage e)))))))
(defn count [id]
(f/attempt-all
[joblist (db/query @jobs {:email id})]
(clojure.core/count joblist)
(f/when-failed [e]
(f/fail "jobs/count :: " (f/message e)))))
(defn add [path config account]
(with-programs
[ssh scp node]
(let [tstamp (tc/to-long (time/now))
jobname (str (:email account) "-vm_amd64_ascii-" tstamp)
jobdir (str "/srv/toaster/" jobname)]
(f/if-let-ok? [joblimit (profile/get-joblimit (:email account))]
(if (>= (count (:email account)) joblimit)
(f/fail "Job limit is reached, trash some to free slots")
(f/attempt-all
[ ;; r_lint (dockerlint path)
r_mkdir (ssh "-i" (q config [:jenkins :key])
(ssh-host config) "mkdir" "-p" jobdir)
r_scp (scp "-i" (q config [:jenkins :key])
path (str (ssh-host config) ":" jobdir "/Dockerfile"))
r_job (sync_jobs config "-a" jobname)
r_store (db/store! @jobs :jobid
{:jobid jobname
:email (:email account)
:account (dissoc account :password :activation-link)
;; :lint (if (.contains r_lint "is OK") true false)
:timestamp tstamp
:type "vm_amd64_ascii"
:dockerfile (slurp path)})]
{;; :lint r_lint
:job r_job}
(f/when-failed [e]
(f/fail (str "Job add '" jobname "' failure :: " (f/message e))))))
(f/fail (str "Cannot find profile " (:email account) " :: "
(f/message joblimit)))))))

View File

@@ -0,0 +1,38 @@
(ns toaster.profiles
(:require
[clojure.string :as str]
[clojure.java.io :as io]
[clj-time.core :as time]
[clj-time.coerce :as tc]
[clj-storage.db.mongo :refer [create-mongo-store]]
[clj-storage.core :as db]
[failjure.core :as f]
[taoensso.timbre :as log :refer [debug]]
[me.raynes.conch :as sh :refer [with-programs]]
[toaster.config :refer :all]
[toaster.ring :refer [jobs profiles]]
[toaster.bulma :refer [render-yaml]]
[hiccup.form :as hf]))
(defn create [id]
(let [profile {:email id
:joblimit 3 ; default
:roles ["noob"]
:lastlogin (tc/to-long (time/now))}]
(db/store! @profiles :email profile)
(render-yaml profile)))
(defn- fetch-profile [id]
(f/if-let-ok? [profile (db/fetch @profiles id)]
profile
(f/fail (str "Profile not found " id " :: " (f/message profile)))))
(defn get-joblimit [id]
;; TODO: quick fix for default joblimit but needs better solution
(let [jl (f/ok-> id fetch-profile (get :joblimit))]
(if (nil? jl) 3 jl)))
(defn has-role [id role]
(f/ok-> id fetch-profile (get :roles)
clojure.core/set (contains? role)))

View File

@@ -0,0 +1,107 @@
;; Copyright (C) 2015-2017 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.ring
(:require
[clojure.java.io :as io]
[toaster.config :as conf]
[taoensso.timbre :as log]
[failjure.core :as f]
[clj-storage.db.mongo :refer [get-mongo-db create-mongo-store]]
[just-auth.core :as auth]
[just-auth.db.just-auth :as auth-db]
[auxiliary.translation :as trans]
[compojure.core :refer :all]
[compojure.handler :refer :all]
[ring.middleware.session :refer :all]
[ring.middleware.accept :refer [wrap-accept]]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]))
;; generic webapp stores
(def config (atom {}))
(def db (atom {}))
(def accts (atom {}))
(def auth (atom {}))
;; app specific stores
(def jobs (atom {}))
(def profiles (atom {}))
(defn init []
(log/merge-config!
{:level :debug
;; #{:trace :debug :info :warn :error :fatal :report}
;; Control log filtering by
;; namespaces/patterns. Useful for turning off
;; logging in noisy libraries, etc.:
;; i.e: :ns-whitelist ["agiladmin.*" "just-auth.*"]
:ns-blacklist ["org.eclipse.jetty.*"
"org.mongodb.driver.cluster"]})
;; ------------------
;; load configuration
(reset! config (conf/load-config
(or (System/getenv "toaster_conf") "toaster")
conf/default-settings))
;; --------------------------------
;; initialize authentication stores
(let [justauth-conf (conf/q @config [:just-auth])]
;; connect database (TODO: take parameters from configuration)
(reset! db (get-mongo-db (:mongo-url justauth-conf)))
;; create authentication stores in db
(f/attempt-all
[auth-stores (auth-db/create-auth-stores @db)]
[(trans/init "lang/auth-en.yml" "lang/english.yaml")
(reset! accts auth-stores)
(reset! auth (auth/email-based-authentication
auth-stores
;; TODO: replace with email taken from config
(dissoc (conf/q @config [:just-auth])
:mongo-url :mongo-user :mongo-pass)
{:criteria #{:email :ip-address}
:type :block
:time-window-secs 10
:threshold 5}))]
;; (select-keys auth-stores [:account-store
;; :password-recovery-store])
(f/when-failed [e]
(log/error (str (trans/locale [:init :failure])
" - " (f/message e))))))
;; ----------------------
;; initialize jobs stores
(reset! jobs (create-mongo-store @db :job-store))
;; initialize profile stores
(reset! profiles (create-mongo-store @db :profile-store))
;; ------------------------------
;; log all results worth noticing
(log/info (str (trans/locale [:init :success])))
(log/debug @auth))
(def app-defaults
(-> site-defaults
(assoc-in [:cookies] true)
(assoc-in [:security :anti-forgery]
(conf/q @config [:webserver :anti-forgery]))
(assoc-in [:security :ssl-redirect]
(conf/q @config [:webserver :ssl-redirect]))
(assoc-in [:security :hsts] true)))

View File

@@ -0,0 +1,173 @@
(ns toaster.session
(:refer-clojure :exclude [get])
(:require
[clojure.java.io :as io]
[toaster.config :as conf]
[taoensso.timbre :as log]
[failjure.core :as f]
[just-auth.core :as auth]
[hiccup.page :as page :refer [html5]]
[clostache.parser :refer [render-resource]]
[toaster.ring :as ring]))
(defn resource
"renders a template, optionally passing it an hash-map of parameters."
([template] (resource template {}))
([template params] (render-resource template params)))
(defonce login "templates/body_loginform.html")
(defonce signup "templates/body_signupform.html")
(defonce head "templates/html_head.html")
(defonce footer "templates/body_footer.html")
(defn param [request param]
(let [p (if (coll? param)
(into [:params] param)
(conj [:params] param))
value (get-in request p)]
(if (nil? value)
(f/fail (str "Parameter not found: " p))
value)))
;; TODO: not working?
(defn get [req arrk]
{:pre (coll? arrk)}
(if-let [value (get-in req (conj [:session] arrk))]
value
(f/fail (str "Value not found in session: " (str arrk)))))
(defn check-config [request]
;; reload configuration from file all the time if in debug mode
(if-let [session (:session request)]
(if (contains? session :config)
(:config session)
(conf/load-config "toaster" conf/default-settings))
(f/fail "Session not found.")))
(defn check-account [request]
;; check if login is present in session
(f/attempt-all
[login (get-in request [:session :auth :email])
user (auth/get-account @ring/auth login)]
user
(f/when-failed [e]
(->> e f/message
(str "Unauthorized access: ")
f/fail))))
(defn check-database []
(if-let [db @ring/db]
db
(f/fail "No connection to database.")))
(defn notify
"render a notification message without ending the page"
([msg] (notify msg ""))
([msg class]
;; support also is-error (not included as notify class in bulma
(let [tclass (if (= class "is-error") "is-danger" class)]
(cond ;; log to console using timbre
(= tclass "is-danger") (log/error msg)
(= tclass "is-warning") (log/warn msg)
(= tclass "is-success") (log/info msg)
(= tclass "is-primary") (log/debug msg))
[:div {:class (str "notification " tclass " has-text-centered")} msg]
)))
;; shortcut
(defn error
([msg] (notify msg "is-danger"))
([msg fail] (notify (str msg " :: " (f/message fail)) "is-danger")))
(defn render
"render a full webpage using headers, navbar, body and footer"
([body] (render nil body))
([account body]
{:headers {"Content-Type"
"text/html; charset=utf-8"}
:body (page/html5
(resource head)
(conj body (resource footer)))}))
(defn render-template
"render an html from resources using headers, navbar, body and footer"
([body] (render-template nil body))
([account body]
{:headers {"Content-Type"
"text/html; charset=utf-8"}
:body (str "<!DOCTYPE html>\n<html>"
(resource head)
"\n<!-- body -->\n"
(resource body)
"\n<!-- end of body -->\n"
(resource footer))}))
(defn render-html
"render an html from resources using headers, navbar, body and footer"
([body] (render-template nil body))
([account body]
{:headers {"Content-Type"
"text/html; charset=utf-8"}
:body (str "<!DOCTYPE html>\n<html>"
(resource head)
"\n<!-- body -->\n"
body ;; html text string
"\n<!-- end of body -->\n"
(resource footer))}))
(defn render-error [err] (->> "is-danger" (notify err) render))
(defn fail [msg err] (f/fail (str msg " :: " (f/message err))))
(defn upload
"manages the upload of a file and calls a function with its path.
(callback) is called with 3 args: path, config and account"
[request config account callback]
(f/attempt-all
[tempfile (param request [:file :tempfile])
filename (param request [:file :filename])
filesize (param request [:file :size])]
(if (> filesize 64000)
;; TODO: put filesize limit in config
(f/fail "file too big to upload (64KB limit)")
;; else
(let [file (io/copy tempfile (io/file "/tmp" filename))
path (str "/tmp/" filename)]
(io/delete-file tempfile)
(if (not (.exists (io/file path)))
(f/fail (str "uploaded file not found: " filename))
;; file is now in 'tmp' var
(callback path config account))))
(f/when-failed [e]
(error "Upload file error" e))))
(defn adduser
"manages the creation of a user (pending activation) and calls a fun callback.
(fun) takes 2 args: the name and email of the user."
[request fun]
(f/attempt-all
[name (param request :name)
email (param request :email)
password (param request :password)
repeat-password (param request :repeat-password)
activation {:activation-uri
(get-in request [:headers "host"])}]
(if (not= password repeat-password)
(f/fail "repeated password did not match")
(f/try*
(f/attempt-all
[signup (auth/sign-up @ring/auth
name
email
password
activation
[])]
(fun name email)
(f/when-failed [e]
(fail (str "failure creating account '"email"'") e)))))
(f/when-failed [e]
(render
[:body
(error "Sign-up failure" e)
(resource signup)]))))

View File

@@ -0,0 +1,117 @@
;; Copyright (C) 2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.views
(:require
[clojure.string :as str :refer [replace]]
[clojure.contrib.humanize :as humanize :refer [datetime]]
;; [clojure.data.json :as json :refer [read-str]]
[toaster.bulma :as web :refer [button render-yaml]]
[toaster.session :as s :refer [notify resource param]]
[toaster.ring :as ring :refer [jobs]]
[toaster.jobs :as job :refer [add sync_jobs]]
[failjure.core :as f :refer [attempt-all when-failed if-let-ok?]]
[auxiliary.string :refer [strcasecmp]]
[toaster.config :refer [q]]
[taoensso.timbre :as log]
;; [clj-time.core :as time]
[clj-time.coerce :as tc]
[clj-storage.core :as db]
[hiccup.form :as hf]))
;; TODO: templated
(defn- box-list [account joblist]
[:div {:class "box"}
[:h1 {:class "title"} (str "List all toaster jobs for " (:name account))]
[:table {:class "table is-fullwidth is-hoverable"}
[:thead nil
[:tr nil [:th nil "Date"] [:th nil "Type"] [:th nil "Status"] [:th nil "Actions"]]]
[:tbody nil
(for [j joblist]
(let [type (:type j) ;; (-> j (str/split #"-") second)
tstamp (:timestamp j) ;; (-> j (str/split #"-") last)
jobid (:jobid j)
joburl (str/replace jobid #"@" "AT")]
[:tr nil
[:td {:class "date"} (-> tstamp Long/valueOf tc/from-long humanize/datetime)]
[:td {:class "job"} [:a {:href (str "https://sdk.dyne.org:4443/view/web-sdk-builds/job/" joburl)} type]]
[:td {:class "status"} [:a {:href (str "https://sdk.dyne.org:4443/job/" joburl "/lastBuild/console")}
[:img {:src (str "https://sdk.dyne.org:4443/job/" joburl "/badge/icon")}]]]
[:td {:class "actions"}
[:div {:class "field is-grouped"}
(web/button "/view" "\uD83D\uDC41" (hf/hidden-field "jobid" jobid))
(web/button "/start" "▶" (hf/hidden-field "jobid" jobid))
(web/button "/remove" "\uD83D\uDDD1" (hf/hidden-field "jobid" jobid))]]]
))]]])
(defn add-job [path config account]
(f/attempt-all
[newjob (job/add path config account)]
(s/notify "New toaster job succesfully added" "is-success")
;; else when job/add is not-ok
(f/when-failed [e]
(s/error "Error adding job" e))))
(defn dashboard
([account] (dashboard {} {} account))
([request config account]
(f/attempt-all
[joblist (db/query @ring/jobs {:email (:email account)})]
[:div {:class "container has-text-centered"}
[:span
;(if (> 0 (count joblist))
(box-list account joblist)
;)
(s/resource "templates/body_addjob.html") ]]
(f/when-failed [e]
(s/error "Job list failure" e)))))
(defn remove-job [request config account]
(f/attempt-all
[jobid (s/param request :jobid)
jobfound (db/query @ring/jobs {:jobid jobid})
r_rmjob (db/delete! @ring/jobs jobid)
r_sync (job/sync_jobs config "-d" jobid)
;; TODO: rm -rf the jobdir created via ssh in jobs/add
]
(s/notify (str "Job removed :: " jobid) "is-primary")
(f/when-failed [e]
(s/error "Failure removing job" e))))
(defn start-job [request config account]
(f/attempt-all
[jobid (s/param request :jobid)
jobfound (db/query @ring/jobs {:jobid jobid})
r_sync (job/sync_jobs config "-r" jobid)]
(s/notify (str "Job started: " jobid) "is-success")
(f/when-failed [e]
(s/error "Failure starting job" e))))
(defn view-job [request config account]
(f/attempt-all
[jobid (s/param request :jobid)
jobfound (db/fetch @ring/jobs jobid)
dockerfile (-> jobfound :dockerfile)]
[:div {:class "box"}
[:h1 {:class "title"} (str "Viewing job: " jobid)]
[:form [:textarea {:id "code" :name "code" } dockerfile]]
[:script "var editor = CodeMirror.fromTextArea(document.getElementById(\"code\"),
{ lineNumbers: true, mode: \"dockerfile\" });"]]
(f/when-failed [e]
(s/error "Failure viewing job" e))))

View File

@@ -0,0 +1,345 @@
;; Copyright (C) 2015-2018 Dyne.org foundation
;; Sourcecode designed, written and maintained by
;; Denis Roio <jaromil@dyne.org>
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU Affero General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU Affero General Public License for more details.
;; You should have received a copy of the GNU Affero General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
(ns toaster.webpage
(:require [clojure.java.io :as io]
[clojure.data.json :as json]
[clojure.data.csv :as csv]
[yaml.core :as yaml]
[toaster.config :as conf]
[taoensso.timbre :as log]
[failjure.core :as f]
[toaster.ring :as ring]
[hiccup.page :as page]
[hiccup.form :as hf]))
(declare render)
(declare render-head)
(declare navbar-guest)
(declare navbar-account)
(declare render-footer)
(declare render-yaml)
(declare render-edn)
(declare render-error)
(declare render-error-page)
(declare render-static)
(declare render-error)
(defn q [req]
"wrapper to retrieve parameters"
;; TODO: sanitise and check for irregular chars
(get-in req (conj [:params] req)))
(defn button
([url text] (button url text [:p]))
([url text field] (button url text field "btn-secondary btn-lg"))
([url text field type]
(hf/form-to [:post url]
field ;; can be an hidden key/value field (project,
;; person, etc using hf/hidden-field)
(hf/submit-button {:class (str "btn " type)} text))))
(defn button-cancel-submit [argmap]
[:div
{:class
(str "row col-md-6 btn-group btn-group-lg "
(:btn-group-class argmap))
:role "group"}
(button
(:cancel-url argmap) "Cancel"
(:cancel-params argmap)
"btn-primary btn-lg btn-danger col-md-3")
(button
(:submit-url argmap) "Submit"
(:submit-params argmap)
"btn-primary btn-lg btn-success col-md-3")])
(defn reload-session [request]
;; TODO: validation of all data loaded via prismatic schema
(conf/load-config "toaster" conf/default-settings)
)
(defn render
([body]
{:headers {"Content-Type"
"text/html; charset=utf-8"}
:body (page/html5
(render-head)
[:body ;; {:class "static"}
navbar-guest
[:div {:class "container-fluid"} body]
(render-footer)])})
([account body]
{:headers {"Content-Type"
"text/html; charset=utf-8"}
:body (page/html5
(render-head)
[:body (if (empty? account)
navbar-guest
navbar-account)
[:div {:class "container-fluid"} body]
(render-footer)])}))
(defn render-success
"render a successful message without ending the page"
[succ]
[:div {:class "alert alert-success" :role "alert"}
[:span {:class "far fa-check-circle"
:aria-hidden "true" :style "padding: .5em"}]
[:span {:class "sr-only"} "Success:" ]
succ])
(defn render-error
"render an error message without ending the page"
[err]
[:div {:class "alert alert-danger" :role "alert"}
[:span {:class "far fa-meh"
:aria-hidden "true" :style "padding: .5em"}]
[:span {:class "sr-only"} "Error:" ]
err])
(defn render-error-page
([] (render-error-page {} "Unknown"))
([err] (render-error-page {} err))
([session error]
(render
[:div {:class "container-fluid"}
(render-error error)
(if-not (empty? session)
[:div {:class "config"}
[:h2 "Environment dump:"]
(render-yaml session)])])))
(defn render-head
([] (render-head
"toaster" ;; default title
"toaster"
"https://toaster.dyne.org")) ;; default desc
([title desc url]
[:head [:meta {:charset "utf-8"}]
[:meta {:http-equiv "X-UA-Compatible" :content "IE=edge,chrome=1"}]
[:meta
{:name "viewport"
:content "width=device-width, initial-scale=1, maximum-scale=1"}]
[:title title]
;; javascript scripts
(page/include-js "/static/js/jquery-3.2.1.min.js")
(page/include-js "/static/js/bootstrap.min.js")
;; cascade style sheets
(page/include-css "/static/css/bootstrap.min.css")
(page/include-css "/static/css/json-html.css")
(page/include-css "/static/css/highlight-tomorrow.css")
(page/include-css "/static/css/formatters-styles/html.css")
(page/include-css "/static/css/formatters-styles/annotated.css")
(page/include-css "/static/css/fa-regular.min.css")
(page/include-css "/static/css/fontawesome.min.css")
(page/include-css "/static/css/toaster.css")]))
(def navbar-brand
[:div {:class "navbar-header"}
[:button {:class "navbar-toggle" :type "button"
:data-toggle "collapse"
:data-target "#navbarResponsive"
:aria-controls "navbarResponsive"
:aria-expanded "false"
:aria-label "Toggle navigation"}
[:span {:class "sr-only"} "Toggle navigation"]
[:span {:class "icon-bar"}]
[:span {:class "icon-bar"}]
[:span {:class "icon-bar"}]]
[:a {:class "navbar-item " :href "/"}
[:img {:src "/static/img/whale_toast.jpg"}]]])
(def navbar-guest
[:nav
{:class "navbar navbar-default navbar-fixed-top navbar-expand-md navbar-expand-lg"}
navbar-brand
[:div {:class "collapse navbar-collapse" :id "navbarResponsive"}
[:ul {:class "nav navbar-nav hidden-sm md-auto ml-auto"}
;; --
[:li {:class "divider" :role "separator"}]
[:li {:class "nav-item"}
[:a {:class "nav-link far fa-address-card"
:href "/login"} " Login"]]
]]])
(def navbar-account
[:nav {:class "navbar navbar-default navbar-fixed-top navbar-expand-md navbar-expand-lg"}
navbar-brand
[:div {:class "collapse navbar-collapse" :id "navbarResponsive"}
[:ul {:class "nav navbar-nav hidden-sm ml-auto"}
;; --
[:li {:class "divider" :role "separator"}]
;; LIST OF RELEVANT LINKS AFTER LOGIN
[:li {:class "nav-item"}
[:a {:class "nav-link far fa-address-card"
:href "/list"} " List"]]
;; [:li {:class "nav-item"}
;; [:a {:class "nav-link far fa-paper-plane"
;; :href "/projects/list"} " Projects"]]
[:li {:class "nav-item"}
[:a {:class "nav-link far fa-plus-square"
:href "/upload"} " Add"]]
;; [:li {:class "nav-item"}
;; [:a {:class "nav-link far fa-save"
;; :href "/reload"} " Reload"]]
;; --
;; [:li {:role "separator" :class "divider"} ]
;; [:li {:class "nav-item"}
;; [:a {:class "nav-link far fa-file-code"
;; :href "/config"} " Configuration"]]
]]])
(defn render-footer []
[:footer {:class "row" :style "margin: 20px"}
[:hr]
[:div {:class "footer col-lg-3"}
[:img {:src "/static/img/AGPLv3.png" :style "margin-top: 2.5em"
:alt "Affero GPLv3 License"
:title "Affero GPLv3 License"} ]]
[:div {:class "footer col-lg-3"}
[:a {:href "https://www.dyne.org"}
[:img {:src "/static/img/swbydyne.png"
:alt "Software by Dyne.org"
:title "Software by Dyne.org"}]]]
])
(defn render-static [body]
(page/html5 (render-head)
[:body {:class "fxc static"}
navbar-guest
[:div {:class "container"} body]
(render-footer)
]))
;; highlight functions do no conversion, take the format they highlight
;; render functions take edn and convert to the highlight format
;; download functions all take an edn and convert it in target format
;; edit functions all take an edn and present an editor in the target format
(defn render-yaml
"renders an edn into an highlighted yaml"
[data]
[:span
[:pre [:code {:class "yaml"}
(yaml/generate-string data)]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn highlight-yaml
"renders a yaml text in highlighted html"
[data]
[:span
[:pre [:code {:class "yaml"}
data]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn highlight-json
"renders a json text in highlighted html"
[data]
[:span
[:pre [:code {:class "json"}
data]]
[:script "hljs.initHighlightingOnLoad();"]])
(defn download-csv
"takes an edn, returns a csv plain/text for download"
[data]
{:headers {"Content-Type"
"text/plain; charset=utf-8"}
:body (with-out-str (csv/write-csv *out* data))})
(defn edit-edn
"renders an editor for the edn in yaml format"
[data]
[:div;; {:class "form-group"}
[:textarea {:class "form-control"
:rows "20" :data-editor "yaml"
:id "config" :name "editor"}
(yaml/generate-string data)]
[:script {:src "/static/js/ace.js"
:type "text/javascript" :charset "utf-8"}]
[:script {:type "text/javascript"}
(slurp (io/resource "public/static/js/ace-embed.js"))]
;; code to embed the ace editor on all elements in page
;; that contain the attribute "data-editor" set to the
;; mode language of choice
[:input {:class "btn btn-success btn-lg pull-top"
:type "submit" :value "submit"}]])
;; (defonce readme
;; (slurp (io/resource "public/static/README.html")))
(def login-form
[:div
[:h1 "Login for your account"
[:form {:action "/login"
:method "post"}
[:input {:type "text" :name "username"
:placeholder "Username"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "password" :name "password"
:placeholder "Password"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "submit" :value "Login"
:class "btn btn-primary btn-lg btn-block"
:style "margin-top: 1em"}]]]])
(def signup-form
[:div
[:h1 "Sign Up for a toaster account"
[:form {:action "/signup"
:method "post"}
[:input {:type "text" :name "name"
:placeholder "Name"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "text" :name "email"
:placeholder "Email"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "password" :name "password"
:placeholder "Password"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "password" :name "repeat-password"
:placeholder "Repeat password"
:class "form-control"
:style "margin-top: 1em"}]
[:input {:type "submit" :value "Sign In"
:class "btn btn-primary btn-lg btn-block"
:style "margin-top: 1em"}]]]])

View File

@@ -0,0 +1,20 @@
webserver:
anti-forgery: false
ssl-redirect: false
mock-auth: false
jenkins:
host: "bridge.toaster"
user: "jenkins"
key: "../id_ed25519"
url: "https://sdk.dyne.org:4443"
just-auth:
email-server: "mail.dyne.org"
email-user: "xxxxxxxxxxxxxxxx"
email-pass: "xxxxxxxxxxxxxxxx"
email-address: "xxxxxxxxxxxxxxxx"
email-admin: "xxxxxxxxxxxxxxxx"
mongo-url: mongodb://localhost:27017/toaster
mongo-user: toaster
mongo-pass: toaster

View File

@@ -28,7 +28,7 @@ Usage
-----
```
usage: sync_jobs.py [-h] [-a] [-d] [-n] jobname
usage: sync_jobs.py [-h] [-a] [-d] [-n] [-r] jobname
positional arguments:
jobname
@@ -38,31 +38,32 @@ optional arguments:
-a, --add
-d, --delete
-n, --dryrun
-r, --run
```
The `jobname` argument should be in a specific format. It should contain
the requester's email, which sdk was chosen, the requested architecture,
and a timestamp.
codename, and a timestamp.
In case of vm-sdk or live-sdk, these would look like:
```
parazyd@dyne.org-vm_amd64-1537977964
parazyd@dyne.org-live_amd64-1537977964
parazyd@dyne.org-vm_amd64_ascii-1537977964
parazyd@dyne.org-live_amd64_beowulf-1537977964
```
In case of arm-sdk, we also need to know the board we're building for:
```
parazyd@dyne.org-arm_armhf_sunxi-1537977964
parazyd@dyne.org-arm_armhf_ascii_sunxi-1537977964
```
All of this combined, the required command to add a new job to Jenkins
would look something like the following:
```
sync_jobs.py -a parazyd@dyne.org-vm_amd64-1537977964
sync_jobs.py -a parazyd@dyne.org-vm_amd64_ascii-1537977964
```
In case of removing an existing job, all of the above applies the same
way. You just have to use `-d` instead of `-a`.
In case of removing or building an existing job, all of the above applies the
same way. You just have to use `-d` or `-r` instead of `-a`, respectively.

View File

@@ -2,6 +2,9 @@
from jenkins_creds import (jenkins_host, jenkins_cred)
# Path to our files
pypath = '/var/lib/jenkins/toaster.do/jenkins_backend'
# Path to jenkins-cli.jar
jarpath = '/var/cache/jenkins/war/WEB-INF/jenkins-cli.jar'

View File

@@ -4,21 +4,12 @@ Module for backend talk with Jenkins executed by the web/CGI
"""
from argparse import ArgumentParser
from subprocess import run
from subprocess import run, PIPE
from os.path import join
from shutil import rmtree
import html
from config import (jarargs, jobpath)
def html_escape(string):
"""
Function for escaping certain symbols to XML-compatible sequences.
"""
html_codes = [("'", '&apos;'), ('"', '&quot;'), ('&', '&amp;'),
('<', '&lt;'), ('>', '&gt;')]
for i in html_codes:
string = string.replace(i[0], i[1])
return string
from config import (jarargs, jobpath, pypath)
def add_job(jobname):
@@ -29,53 +20,134 @@ def add_job(jobname):
desc = 'WebSDK build for: %s\nStarted: %s' % (info[0], info[2])
sdk = info[1].split('_')[0]
arch = info[1].split('_')[1]
blendfile = '%s/%s/Dockerfile' % (jobpath, jobname)
codename = info[1].split('_')[2]
blenddir = join(jobpath, jobname)
blendfile = join(blenddir, 'Dockerfile')
if codename == 'ascii':
relvars = 'release=ascii && version=2.0.0'
elif codename == 'beowulf':
relvars = 'release=beowulf && version=3.0.0'
else:
# Default to Ascii
relvars = 'release=ascii && version=2.0.0'
if sdk == 'arm':
board = info[1].split('_')[2]
zshcmd = 'load devuan %s %s' % (board, blendfile)
elif sdk == 'live':
zshcmd = 'load devuan %s %s' % (arch, blendfile)
elif sdk == 'vm':
zshcmd = 'load devuan %s' % (blendfile)
board = info[1].split('_')[3]
zshcmd = '\
load devuan %s %s && %s && build_image_dist' % (board, blendfile, relvars)
command = "zsh -f -c 'source sdk && %s && build_image_dist'" % zshcmd
command = html_escape(command)
elif sdk == 'live':
zshcmd = '\
load devuan %s %s && %s && build_iso_dist' % (arch, blendfile, relvars)
elif sdk == 'vm':
zshcmd = '\
load devuan %s && %s && build_vagrant_dist' % (blendfile, relvars)
command = "zsh -f -c 'source sdk && %s'" % zshcmd
command = html.escape(command)
replacements = [('DESC', desc),
('SDK', sdk),
('ARCH', arch),
('COMMAND', command)]
('CODENAME', codename),
('COMMAND', command),
('BLENDDIR', blenddir)]
sdk_job = open('toasterbuild.xml', encoding='utf-8').read()
sdk_job = open(join(pypath, 'toasterbuild.xml'), encoding='utf-8').read()
for i in replacements:
sdk_job = sdk_job.replace('{{{%s}}}' % i[0], i[1])
jarargs.append('create-job')
jarargs.append(jobname.replace('@', 'AT'))
addargs = jarargs.copy()
addargs.append('create-job')
addargs.append(jobname.replace('@', 'AT'))
return run(jarargs, input=sdk_job.encode())
run(addargs, input=sdk_job.encode())
viewargs = jarargs.copy()
viewargs.append('add-job-to-view')
viewargs.append('web-sdk-builds')
viewargs.append(jobname.replace('@', 'AT'))
return run(viewargs)
def del_job(jobname):
"""
Function for deleting a Jenkins job.
"""
rmtree(join(jobpath, jobname), ignore_errors=True)
jarargs.append('delete-job')
jarargs.append(jobname.replace('@', 'AT'))
return run(jarargs)
def run_job(jobname):
"""
Function for running a Jenkins job.
"""
jarargs.append('build')
jarargs.append(jobname.replace('@', 'AT'))
return run(jarargs)
def list_jobs(account):
"""
Function for listing Jenkins jobs.
"""
jarargs.append('list-jobs')
jarargs.append('web-sdk-builds')
if account == 'all':
return run(jarargs)
joblist = run(jarargs, stdout=PIPE)
joblist = joblist.stdout.decode()
parsedlist = []
for i in joblist.split():
if i.startswith(account.replace('@', 'AT')):
parsedlist.append(i)
print('\n'.join(parsedlist))
def status_job(jobname):
"""
Function for querying status of a certain job.
"""
jarargs.append('console')
jarargs.append(jobname)
jarargs.append('-n')
jarargs.append('1')
console = run(jarargs, stdout=PIPE)
console = console.stdout.decode()
if 'SUCCESS' in console:
print('Last build succeeded')
elif 'FAILURE' in console:
print('Last build failed')
elif 'no build' in console:
print('No last build')
else:
print('Build is in progress')
def main():
"""
Main routine.
"""
parser = ArgumentParser()
parser.add_argument('-n', '--dryrun', action='store_true')
parser.add_argument('-a', '--add', action='store_true')
parser.add_argument('-d', '--delete', action='store_true')
parser.add_argument('-n', '--dryrun', action='store_true')
parser.add_argument('-r', '--run', action='store_true')
parser.add_argument('-l', '--list', action='store_true')
parser.add_argument('-s', '--status', action='store_true')
parser.add_argument('jobname')
# NOTE: jobname should be email-arch-date, and a predefined directory
# somewhere on the filesystem. e.g.:
@@ -96,6 +168,16 @@ def main():
return
print('Removing job:', args.jobname)
del_job(args.jobname)
elif args.run:
if args.dryrun:
print('Would build:', args.jobname)
return
print('Building job:', args.jobname)
run_job(args.jobname)
elif args.list:
list_jobs(args.jobname)
elif args.status:
status_job(args.jobname)
if __name__ == '__main__':

View File

@@ -34,8 +34,16 @@
<builders>
<hudson.tasks.Shell>
<command>
git submodule update --init --recursive --checkout
{{{COMMAND}}}
scp -r sdk:{{{BLENDDIR}}} {{{BLENDDIR}}} || exit 1
git submodule update --init --recursive --checkout || exit 1
mkdir -p tmp
cd tmp
wget https://sdk.dyne.org:4443/job/devuan-{{{CODENAME}}}-{{{ARCH}}}-stage3/lastSuccessfulBuild/artifact/tmp/bootstrap-devuan-{{{ARCH}}}-stage3.tgz
cd -
{{{COMMAND}}} || exit 1
rm -rf {{{BLENDDIR}}}
</command>
</hudson.tasks.Shell>
</builders>