Compare commits

...

43 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
d9d1ff8dfe Use a jarargs variable in config.py instead of placing it everywhere. 2018-09-26 19:13:49 +02:00
93f61e5523 Add .gitignore file. 2018-09-26 18:57:22 +02:00
b7ac8bf061 Use jenkins-cli.jar instead of the web. 2018-09-26 18:57:22 +02:00
0cf7f9e978 Add sync_jobs.py and a corresponding README file. 2018-09-26 18:11:53 +02:00
19d55ecfd2 Add the Jenkins template for new jobs. 2018-09-26 18:11:30 +02:00
49 changed files with 12922 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
jenkins_creds.py
*.pyc

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

69
jenkins_backend/README.md Normal file
View File

@@ -0,0 +1,69 @@
toaster.do Jenkins backend
==========================
These python modules serve as the Jenkins backend to the Devuan SDK's
Web interface. They are to be executed by the web CGI when a new build
is requested.
Configuration
-------------
This backend is configured using two files. `config.py` and
`jenkins_creds.py`. The former contains variables that tell the backend
in what way to work and where to look for files. The latter -
`jenkins_creds.py` is not included in the git repository as it contains
important credentials which allow the backend to work with Jenkins' API.
The `jenkins_creds.py` file should look like the following:
```
jenkins_host = 'https://sdk.dyne.org:4443'
jenkins_cred = 'toaster:thetoasterpassword'
```
These files will be read and imported by `sync_jobs.py` when ran.
Usage
-----
```
usage: sync_jobs.py [-h] [-a] [-d] [-n] [-r] jobname
positional arguments:
jobname
optional arguments:
-h, --help show this help message and exit
-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,
codename, and a timestamp.
In case of vm-sdk or live-sdk, these would look like:
```
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_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_ascii-1537977964
```
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.

15
jenkins_backend/config.py Normal file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3
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'
# jar parameters
jarargs = ['java', '-jar', jarpath, '-s', jenkins_host, '-auth', jenkins_cred]
# Physical path to where jobs are held
jobpath = '/srv/toaster'

184
jenkins_backend/sync_jobs.py Executable file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""
Module for backend talk with Jenkins executed by the web/CGI
"""
from argparse import ArgumentParser
from subprocess import run, PIPE
from os.path import join
from shutil import rmtree
import html
from config import (jarargs, jobpath, pypath)
def add_job(jobname):
"""
Function for adding a job to Jenkins.
"""
info = jobname.split('-')
desc = 'WebSDK build for: %s\nStarted: %s' % (info[0], info[2])
sdk = info[1].split('_')[0]
arch = info[1].split('_')[1]
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('_')[3]
zshcmd = '\
load devuan %s %s && %s && build_image_dist' % (board, blendfile, relvars)
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),
('CODENAME', codename),
('COMMAND', command),
('BLENDDIR', blenddir)]
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])
addargs = jarargs.copy()
addargs.append('create-job')
addargs.append(jobname.replace('@', 'AT'))
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('-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.:
# - parazyd@dyne.org-vm_amd64-198374198
# - parazyd@dyne.org-arm_armhf_raspi2-2198361991
args = parser.parse_args()
if args.add:
if args.dryrun:
print('Would add:', args.jobname)
return
print('Adding job:', args.jobname)
add_job(args.jobname)
elif args.delete:
if args.dryrun:
print('Would remove:', args.jobname)
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__':
main()

View File

@@ -0,0 +1,67 @@
<?xml version='1.1' encoding='UTF-8'?>
<project>
<actions/>
<description>{{{DESC}}}</description>
<keepDependencies>false</keepDependencies>
<properties>
<com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty plugin="gitlab-plugin@1.5.10">
<gitLabConnection></gitLabConnection>
</com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty>
</properties>
<scm class="hudson.plugins.git.GitSCM" plugin="git@3.9.1">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>https://git.devuan.org/sdk/{{{SDK}}}-sdk</url>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/master</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="list"/>
<extensions/>
</scm>
<assignedNode>{{{ARCH}}}</assignedNode>
<canRoam>false</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers/>
<concurrentBuild>false</concurrentBuild>
<builders>
<hudson.tasks.Shell>
<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>
<publishers>
<hudson.tasks.ArtifactArchiver>
<artifacts>dist/*</artifacts>
<allowEmptyArchive>false</allowEmptyArchive>
<onlyIfSuccessful>false</onlyIfSuccessful>
<fingerprint>false</fingerprint>
<defaultExcludes>true</defaultExcludes>
<caseSensitive>true</caseSensitive>
</hudson.tasks.ArtifactArchiver>
</publishers>
<buildWrappers>
<hudson.plugins.ws__cleanup.PreBuildCleanup plugin="ws-cleanup@0.34">
<deleteDirs>false</deleteDirs>
<cleanupParameter></cleanupParameter>
<externalDelete></externalDelete>
</hudson.plugins.ws__cleanup.PreBuildCleanup>
</buildWrappers>
</project>