Add codes

This commit is contained in:
cuigh
2017-09-26 20:50:09 +08:00
parent 2e0b3cec68
commit da9b47de15
188 changed files with 60383 additions and 0 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.js linguist-language=Go
*.css linguist-language=Go
*.jet linguist-language=Go

37
.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# Created by .ignore support plugin (hsz.mobi)
### Go template
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
.vscode
.idea
vendor
# debug files
/debug
/swirl

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM golang:alpine AS build
WORKDIR /go/src/github.com/cuigh/swirl/
ADD . .
#RUN dep ensure
RUN go build
FROM alpine:3.6
MAINTAINER cuigh <noname@live.com>
WORKDIR /app
COPY --from=build /go/src/github.com/cuigh/swirl/swirl .
COPY --from=build /go/src/github.com/cuigh/swirl/config ./config/
COPY --from=build /go/src/github.com/cuigh/swirl/assets ./assets/
COPY --from=build /go/src/github.com/cuigh/swirl/views ./views/
EXPOSE 8001
ENTRYPOINT ["/app/swirl"]

12
Gopkg.toml Normal file
View File

@@ -0,0 +1,12 @@
[[constraint]]
name = "github.com/docker/docker"
source = "github.com/moby/moby"
branch = "master"
[[constraint]]
name = "github.com/cuigh/auxo"
branch = "master"
[[constraint]]
name = "github.com/go-ldap/ldap"
branch = "master"

111
README.md Normal file
View File

@@ -0,0 +1,111 @@
# SWIRL
Swirl is a web management tool for Docker, focused on swarm cluster.
## Features
* Swarm components management
* Image and container management
* Compose management with deployment support
* LDAP authentication support
* Full permission control based on RBAC model
* Scale out as you want
* And more...
## Snapshots
### Dashboard
![Dashboard](docs/images/dashboard.png)
### Service list
![Service list](docs/images/service-list.png)
### Compose list
![Compose list](docs/images/compose-list.png)
### Role editing
![Role editing](docs/images/role-edit.png)
### Settings
![Setting](docs/images/setting.png)
## Configuration
### With config file
All options can be set with `config/app.conf`.
```xml
<config>
<app>
<add key="name" value="swirl"/>
</app>
<web>
<add key="address" value=":8001"/>
<!-- default authorize mode, valid options: *(everyone)/?(login user)/!(authorized explicitly) -->
<add key="authorize_mode" value="?"/>
</web>
<swirl>
<!-- optional -->
<add key="docker_endpoint" value="tcp://docker-proxy:2375"/>
<!-- required, valid options: mongo -->
<add key="db_type" value="mongo"/>
<!-- required, database connection string, must match with db.type option -->
<add key="db_address" value="localhost:27017/swirl"/>
</swirl>
</config>
```
### With environment variables
Only three main options can be set by environment variables for now.
| Name | Value |
| --------------- | ------------------------------------------------|
| DB_TYPE | mongo |
| DB_ADDRESS | localhost:27017/swirl |
| DOCKER_ENDPOINT | tcp://docker-proxy:2375 |
### With swarm config
Docker support mounting configuration file through swarm from v17.06.
## Deployment
### Stand alone
Just copy the swirl binary and config dir to the host, run it.
```bash
nohup swirl >swirl.log &
```
### Docker
```bash
docker run -d -p 8001:8001 -v /var/run/docker.sock:/var/run/docker.sock --name=swirl cuigh/swirl
```
### Docker swarm
```bash
docker service create \
--name=swirl \
--publish=8001:8001/tcp \
--constraint=node.role==manager \
--mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
cuigh/swirl
```
## Build
**Swirl** use `dep` as dependency management tool.
## License
This product is licensed to you under the MIT License. You may not use this product except in compliance with the License. See LICENSE and NOTICE for more information.

13071
assets/bulma/bulma.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

213
assets/codemirror/addon/comment.js vendored Normal file
View File

@@ -0,0 +1,213 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://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";
var noOptions = {};
var nonWS = /[^\s\u00a0]/;
var Pos = CodeMirror.Pos;
function firstNonWS(str) {
var found = str.search(nonWS);
return found == -1 ? 0 : found;
}
CodeMirror.commands.toggleComment = function(cm) {
cm.toggleComment();
};
CodeMirror.defineExtension("toggleComment", function(options) {
if (!options) options = noOptions;
var cm = this;
var minLine = Infinity, ranges = this.listSelections(), mode = null;
for (var i = ranges.length - 1; i >= 0; i--) {
var from = ranges[i].from(), to = ranges[i].to();
if (from.line >= minLine) continue;
if (to.line >= minLine) to = Pos(minLine, 0);
minLine = from.line;
if (mode == null) {
if (cm.uncomment(from, to, options)) mode = "un";
else { cm.lineComment(from, to, options); mode = "line"; }
} else if (mode == "un") {
cm.uncomment(from, to, options);
} else {
cm.lineComment(from, to, options);
}
}
});
// Rough heuristic to try and detect lines that are part of multi-line string
function probablyInsideString(cm, pos, line) {
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
}
function getMode(cm, pos) {
var mode = cm.getMode()
return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
}
CodeMirror.defineExtension("lineComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = getMode(self, from);
var firstLine = self.getLine(from.line);
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
var commentString = options.lineComment || mode.lineComment;
if (!commentString) {
if (options.blockCommentStart || mode.blockCommentStart) {
options.fullLines = true;
self.blockComment(from, to, options);
}
return;
}
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
var pad = options.padding == null ? " " : options.padding;
var blankLines = options.commentBlankLines || from.line == to.line;
self.operation(function() {
if (options.indent) {
var baseString = null;
for (var i = from.line; i < end; ++i) {
var line = self.getLine(i);
var whitespace = line.slice(0, firstNonWS(line));
if (baseString == null || baseString.length > whitespace.length) {
baseString = whitespace;
}
}
for (var i = from.line; i < end; ++i) {
var line = self.getLine(i), cut = baseString.length;
if (!blankLines && !nonWS.test(line)) continue;
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
}
} else {
for (var i = from.line; i < end; ++i) {
if (blankLines || nonWS.test(self.getLine(i)))
self.replaceRange(commentString + pad, Pos(i, 0));
}
}
});
});
CodeMirror.defineExtension("blockComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = getMode(self, from);
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) {
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
self.lineComment(from, to, options);
return;
}
if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return
var end = Math.min(to.line, self.lastLine());
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
var pad = options.padding == null ? " " : options.padding;
if (from.line > end) return;
self.operation(function() {
if (options.fullLines != false) {
var lastLineHasText = nonWS.test(self.getLine(end));
self.replaceRange(pad + endString, Pos(end));
self.replaceRange(startString + pad, Pos(from.line, 0));
var lead = options.blockCommentLead || mode.blockCommentLead;
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
if (i != end || lastLineHasText)
self.replaceRange(lead + pad, Pos(i, 0));
} else {
self.replaceRange(endString, to);
self.replaceRange(startString, from);
}
});
});
CodeMirror.defineExtension("uncomment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = getMode(self, from);
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
// Try finding line comments
var lineString = options.lineComment || mode.lineComment, lines = [];
var pad = options.padding == null ? " " : options.padding, didSomething;
lineComment: {
if (!lineString) break lineComment;
for (var i = start; i <= end; ++i) {
var line = self.getLine(i);
var found = line.indexOf(lineString);
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
if (found == -1 && nonWS.test(line)) break lineComment;
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
lines.push(line);
}
self.operation(function() {
for (var i = start; i <= end; ++i) {
var line = lines[i - start];
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
if (pos < 0) continue;
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
didSomething = true;
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
}
});
if (didSomething) return true;
}
// Try block comments
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) return false;
var lead = options.blockCommentLead || mode.blockCommentLead;
var startLine = self.getLine(start), open = startLine.indexOf(startString)
if (open == -1) return false
var endLine = end == start ? startLine : self.getLine(end)
var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
if (close == -1 && start != end) {
endLine = self.getLine(--end);
close = endLine.indexOf(endString);
}
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
if (close == -1 ||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
return false;
// Avoid killing block comments completely outside the selection.
// Positions of the last startString before the start of the selection, and the first endString after it.
var lastStart = startLine.lastIndexOf(startString, from.ch);
var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
// Positions of the first endString after the end of the selection, and the last startString before it.
firstEnd = endLine.indexOf(endString, to.ch);
var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
self.operation(function() {
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
Pos(end, close + endString.length));
var openEnd = open + startString.length;
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
if (lead) for (var i = start + 1; i <= end; ++i) {
var line = self.getLine(i), found = line.indexOf(lead);
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
var foundEnd = found + lead.length;
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
}
});
return true;
});
});

View File

@@ -0,0 +1,341 @@
/* 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-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: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.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;
overflow: auto;
}
.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; }

File diff suppressed because it is too large Load Diff

613
assets/codemirror/keymap/sublime.js vendored Normal file
View File

@@ -0,0 +1,613 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// A rough approximation of Sublime Text's keybindings
// Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/edit/matchbrackets"));
else if (typeof define == "function" && define.amd) // AMD
define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var map = CodeMirror.keyMap.sublime = {fallthrough: "default"};
var cmds = CodeMirror.commands;
var Pos = CodeMirror.Pos;
var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
var ctrl = mac ? "Cmd-" : "Ctrl-";
// This is not exactly Sublime's algorithm. I couldn't make heads or tails of that.
function findPosSubword(doc, start, dir) {
if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1));
var line = doc.getLine(start.line);
if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0));
var state = "start", type;
for (var pos = start.ch, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) {
var next = line.charAt(dir < 0 ? pos - 1 : pos);
var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o";
if (cat == "w" && next.toUpperCase() == next) cat = "W";
if (state == "start") {
if (cat != "o") { state = "in"; type = cat; }
} else if (state == "in") {
if (type != cat) {
if (type == "w" && cat == "W" && dir < 0) pos--;
if (type == "W" && cat == "w" && dir > 0) { type = "w"; continue; }
break;
}
}
}
return Pos(start.line, pos);
}
function moveSubword(cm, dir) {
cm.extendSelectionsBy(function(range) {
if (cm.display.shift || cm.doc.extend || range.empty())
return findPosSubword(cm.doc, range.head, dir);
else
return dir < 0 ? range.from() : range.to();
});
}
var goSubwordCombo = mac ? "Ctrl-" : "Alt-";
cmds[map[goSubwordCombo + "Left"] = "goSubwordLeft"] = function(cm) { moveSubword(cm, -1); };
cmds[map[goSubwordCombo + "Right"] = "goSubwordRight"] = function(cm) { moveSubword(cm, 1); };
if (mac) map["Cmd-Left"] = "goLineStartSmart";
var scrollLineCombo = mac ? "Ctrl-Alt-" : "Ctrl-";
cmds[map[scrollLineCombo + "Up"] = "scrollLineUp"] = function(cm) {
var info = cm.getScrollInfo();
if (!cm.somethingSelected()) {
var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local");
if (cm.getCursor().line >= visibleBottomLine)
cm.execCommand("goLineUp");
}
cm.scrollTo(null, info.top - cm.defaultTextHeight());
};
cmds[map[scrollLineCombo + "Down"] = "scrollLineDown"] = function(cm) {
var info = cm.getScrollInfo();
if (!cm.somethingSelected()) {
var visibleTopLine = cm.lineAtHeight(info.top, "local")+1;
if (cm.getCursor().line <= visibleTopLine)
cm.execCommand("goLineDown");
}
cm.scrollTo(null, info.top + cm.defaultTextHeight());
};
cmds[map["Shift-" + ctrl + "L"] = "splitSelectionByLine"] = function(cm) {
var ranges = cm.listSelections(), lineRanges = [];
for (var i = 0; i < ranges.length; i++) {
var from = ranges[i].from(), to = ranges[i].to();
for (var line = from.line; line <= to.line; ++line)
if (!(to.line > from.line && line == to.line && to.ch == 0))
lineRanges.push({anchor: line == from.line ? from : Pos(line, 0),
head: line == to.line ? to : Pos(line)});
}
cm.setSelections(lineRanges, 0);
};
map["Shift-Tab"] = "indentLess";
cmds[map["Esc"] = "singleSelectionTop"] = function(cm) {
var range = cm.listSelections()[0];
cm.setSelection(range.anchor, range.head, {scroll: false});
};
cmds[map[ctrl + "L"] = "selectLine"] = function(cm) {
var ranges = cm.listSelections(), extended = [];
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
extended.push({anchor: Pos(range.from().line, 0),
head: Pos(range.to().line + 1, 0)});
}
cm.setSelections(extended);
};
map["Shift-Ctrl-K"] = "deleteLine";
function insertLine(cm, above) {
if (cm.isReadOnly()) return CodeMirror.Pass
cm.operation(function() {
var len = cm.listSelections().length, newSelection = [], last = -1;
for (var i = 0; i < len; i++) {
var head = cm.listSelections()[i].head;
if (head.line <= last) continue;
var at = Pos(head.line + (above ? 0 : 1), 0);
cm.replaceRange("\n", at, null, "+insertLine");
cm.indentLine(at.line, null, true);
newSelection.push({head: at, anchor: at});
last = head.line + 1;
}
cm.setSelections(newSelection);
});
cm.execCommand("indentAuto");
}
cmds[map[ctrl + "Enter"] = "insertLineAfter"] = function(cm) { return insertLine(cm, false); };
cmds[map["Shift-" + ctrl + "Enter"] = "insertLineBefore"] = function(cm) { return insertLine(cm, true); };
function wordAt(cm, pos) {
var start = pos.ch, end = start, line = cm.getLine(pos.line);
while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start;
while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end;
return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)};
}
cmds[map[ctrl + "D"] = "selectNextOccurrence"] = function(cm) {
var from = cm.getCursor("from"), to = cm.getCursor("to");
var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel;
if (CodeMirror.cmpPos(from, to) == 0) {
var word = wordAt(cm, from);
if (!word.word) return;
cm.setSelection(word.from, word.to);
fullWord = true;
} else {
var text = cm.getRange(from, to);
var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
var cur = cm.getSearchCursor(query, to);
var found = cur.findNext();
if (!found) {
cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
found = cur.findNext();
}
if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to()))
return CodeMirror.Pass
cm.addSelection(cur.from(), cur.to());
}
if (fullWord)
cm.state.sublimeFindFullWord = cm.doc.sel;
};
function addCursorToSelection(cm, dir) {
var ranges = cm.listSelections(), newRanges = [];
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
var newAnchor = cm.findPosV(range.anchor, dir, "line");
var newHead = cm.findPosV(range.head, dir, "line");
var newRange = {anchor: newAnchor, head: newHead};
newRanges.push(range);
newRanges.push(newRange);
}
cm.setSelections(newRanges);
}
var addCursorToLineCombo = mac ? "Shift-Cmd" : 'Alt-Ctrl';
cmds[map[addCursorToLineCombo + "Up"] = "addCursorToPrevLine"] = function(cm) { addCursorToSelection(cm, -1); };
cmds[map[addCursorToLineCombo + "Down"] = "addCursorToNextLine"] = function(cm) { addCursorToSelection(cm, 1); };
function isSelectedRange(ranges, from, to) {
for (var i = 0; i < ranges.length; i++)
if (ranges[i].from() == from && ranges[i].to() == to) return true
return false
}
var mirror = "(){}[]";
function selectBetweenBrackets(cm) {
var ranges = cm.listSelections(), newRanges = []
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], pos = range.head, opening = cm.scanForBracket(pos, -1);
if (!opening) return false;
for (;;) {
var closing = cm.scanForBracket(pos, 1);
if (!closing) return false;
if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) {
newRanges.push({anchor: Pos(opening.pos.line, opening.pos.ch + 1),
head: closing.pos});
break;
}
pos = Pos(closing.pos.line, closing.pos.ch + 1);
}
}
cm.setSelections(newRanges);
return true;
}
cmds[map["Shift-" + ctrl + "Space"] = "selectScope"] = function(cm) {
selectBetweenBrackets(cm) || cm.execCommand("selectAll");
};
cmds[map["Shift-" + ctrl + "M"] = "selectBetweenBrackets"] = function(cm) {
if (!selectBetweenBrackets(cm)) return CodeMirror.Pass;
};
cmds[map[ctrl + "M"] = "goToBracket"] = function(cm) {
cm.extendSelectionsBy(function(range) {
var next = cm.scanForBracket(range.head, 1);
if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos;
var prev = cm.scanForBracket(range.head, -1);
return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head;
});
};
var swapLineCombo = mac ? "Cmd-Ctrl-" : "Shift-Ctrl-";
cmds[map[swapLineCombo + "Up"] = "swapLineUp"] = function(cm) {
if (cm.isReadOnly()) return CodeMirror.Pass
var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = [];
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], from = range.from().line - 1, to = range.to().line;
newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch),
head: Pos(range.head.line - 1, range.head.ch)});
if (range.to().ch == 0 && !range.empty()) --to;
if (from > at) linesToMove.push(from, to);
else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
at = to;
}
cm.operation(function() {
for (var i = 0; i < linesToMove.length; i += 2) {
var from = linesToMove[i], to = linesToMove[i + 1];
var line = cm.getLine(from);
cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
if (to > cm.lastLine())
cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine");
else
cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
}
cm.setSelections(newSels);
cm.scrollIntoView();
});
};
cmds[map[swapLineCombo + "Down"] = "swapLineDown"] = function(cm) {
if (cm.isReadOnly()) return CodeMirror.Pass
var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1;
for (var i = ranges.length - 1; i >= 0; i--) {
var range = ranges[i], from = range.to().line + 1, to = range.from().line;
if (range.to().ch == 0 && !range.empty()) from--;
if (from < at) linesToMove.push(from, to);
else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to;
at = to;
}
cm.operation(function() {
for (var i = linesToMove.length - 2; i >= 0; i -= 2) {
var from = linesToMove[i], to = linesToMove[i + 1];
var line = cm.getLine(from);
if (from == cm.lastLine())
cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine");
else
cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine");
cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine");
}
cm.scrollIntoView();
});
};
cmds[map[ctrl + "/"] = "toggleCommentIndented"] = function(cm) {
cm.toggleComment({ indent: true });
}
cmds[map[ctrl + "J"] = "joinLines"] = function(cm) {
var ranges = cm.listSelections(), joined = [];
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i], from = range.from();
var start = from.line, end = range.to().line;
while (i < ranges.length - 1 && ranges[i + 1].from().line == end)
end = ranges[++i].to().line;
joined.push({start: start, end: end, anchor: !range.empty() && from});
}
cm.operation(function() {
var offset = 0, ranges = [];
for (var i = 0; i < joined.length; i++) {
var obj = joined[i];
var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head;
for (var line = obj.start; line <= obj.end; line++) {
var actual = line - offset;
if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1);
if (actual < cm.lastLine()) {
cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length));
++offset;
}
}
ranges.push({anchor: anchor || head, head: head});
}
cm.setSelections(ranges, 0);
});
};
cmds[map["Shift-" + ctrl + "D"] = "duplicateLine"] = function(cm) {
cm.operation(function() {
var rangeCount = cm.listSelections().length;
for (var i = 0; i < rangeCount; i++) {
var range = cm.listSelections()[i];
if (range.empty())
cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0));
else
cm.replaceRange(cm.getRange(range.from(), range.to()), range.from());
}
cm.scrollIntoView();
});
};
if (!mac) map[ctrl + "T"] = "transposeChars";
function sortLines(cm, caseSensitive) {
if (cm.isReadOnly()) return CodeMirror.Pass
var ranges = cm.listSelections(), toSort = [], selected;
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (range.empty()) continue;
var from = range.from().line, to = range.to().line;
while (i < ranges.length - 1 && ranges[i + 1].from().line == to)
to = ranges[++i].to().line;
if (!ranges[i].to().ch) to--;
toSort.push(from, to);
}
if (toSort.length) selected = true;
else toSort.push(cm.firstLine(), cm.lastLine());
cm.operation(function() {
var ranges = [];
for (var i = 0; i < toSort.length; i += 2) {
var from = toSort[i], to = toSort[i + 1];
var start = Pos(from, 0), end = Pos(to);
var lines = cm.getRange(start, end, false);
if (caseSensitive)
lines.sort();
else
lines.sort(function(a, b) {
var au = a.toUpperCase(), bu = b.toUpperCase();
if (au != bu) { a = au; b = bu; }
return a < b ? -1 : a == b ? 0 : 1;
});
cm.replaceRange(lines, start, end);
if (selected) ranges.push({anchor: start, head: Pos(to + 1, 0)});
}
if (selected) cm.setSelections(ranges, 0);
});
}
cmds[map["F9"] = "sortLines"] = function(cm) { sortLines(cm, true); };
cmds[map[ctrl + "F9"] = "sortLinesInsensitive"] = function(cm) { sortLines(cm, false); };
cmds[map["F2"] = "nextBookmark"] = function(cm) {
var marks = cm.state.sublimeBookmarks;
if (marks) while (marks.length) {
var current = marks.shift();
var found = current.find();
if (found) {
marks.push(current);
return cm.setSelection(found.from, found.to);
}
}
};
cmds[map["Shift-F2"] = "prevBookmark"] = function(cm) {
var marks = cm.state.sublimeBookmarks;
if (marks) while (marks.length) {
marks.unshift(marks.pop());
var found = marks[marks.length - 1].find();
if (!found)
marks.pop();
else
return cm.setSelection(found.from, found.to);
}
};
cmds[map[ctrl + "F2"] = "toggleBookmark"] = function(cm) {
var ranges = cm.listSelections();
var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []);
for (var i = 0; i < ranges.length; i++) {
var from = ranges[i].from(), to = ranges[i].to();
var found = cm.findMarks(from, to);
for (var j = 0; j < found.length; j++) {
if (found[j].sublimeBookmark) {
found[j].clear();
for (var k = 0; k < marks.length; k++)
if (marks[k] == found[j])
marks.splice(k--, 1);
break;
}
}
if (j == found.length)
marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false}));
}
};
cmds[map["Shift-" + ctrl + "F2"] = "clearBookmarks"] = function(cm) {
var marks = cm.state.sublimeBookmarks;
if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear();
marks.length = 0;
};
cmds[map["Alt-F2"] = "selectBookmarks"] = function(cm) {
var marks = cm.state.sublimeBookmarks, ranges = [];
if (marks) for (var i = 0; i < marks.length; i++) {
var found = marks[i].find();
if (!found)
marks.splice(i--, 0);
else
ranges.push({anchor: found.from, head: found.to});
}
if (ranges.length)
cm.setSelections(ranges, 0);
};
map["Alt-Q"] = "wrapLines";
var cK = ctrl + "K ";
function modifyWordOrSelection(cm, mod) {
cm.operation(function() {
var ranges = cm.listSelections(), indices = [], replacements = [];
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (range.empty()) { indices.push(i); replacements.push(""); }
else replacements.push(mod(cm.getRange(range.from(), range.to())));
}
cm.replaceSelections(replacements, "around", "case");
for (var i = indices.length - 1, at; i >= 0; i--) {
var range = ranges[indices[i]];
if (at && CodeMirror.cmpPos(range.head, at) > 0) continue;
var word = wordAt(cm, range.head);
at = word.from;
cm.replaceRange(mod(word.word), word.from, word.to);
}
});
}
map[cK + ctrl + "Backspace"] = "delLineLeft";
cmds[map["Backspace"] = "smartBackspace"] = function(cm) {
if (cm.somethingSelected()) return CodeMirror.Pass;
cm.operation(function() {
var cursors = cm.listSelections();
var indentUnit = cm.getOption("indentUnit");
for (var i = cursors.length - 1; i >= 0; i--) {
var cursor = cursors[i].head;
var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor);
var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize"));
// Delete by one character by default
var deletePos = cm.findPosH(cursor, -1, "char", false);
if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) {
var prevIndent = new Pos(cursor.line,
CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit));
// Smart delete only if we found a valid prevIndent location
if (prevIndent.ch != cursor.ch) deletePos = prevIndent;
}
cm.replaceRange("", deletePos, cursor, "+delete");
}
});
};
cmds[map[cK + ctrl + "K"] = "delLineRight"] = function(cm) {
cm.operation(function() {
var ranges = cm.listSelections();
for (var i = ranges.length - 1; i >= 0; i--)
cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete");
cm.scrollIntoView();
});
};
cmds[map[cK + ctrl + "U"] = "upcaseAtCursor"] = function(cm) {
modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); });
};
cmds[map[cK + ctrl + "L"] = "downcaseAtCursor"] = function(cm) {
modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); });
};
cmds[map[cK + ctrl + "Space"] = "setSublimeMark"] = function(cm) {
if (cm.state.sublimeMark) cm.state.sublimeMark.clear();
cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
};
cmds[map[cK + ctrl + "A"] = "selectToSublimeMark"] = function(cm) {
var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
if (found) cm.setSelection(cm.getCursor(), found);
};
cmds[map[cK + ctrl + "W"] = "deleteToSublimeMark"] = function(cm) {
var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
if (found) {
var from = cm.getCursor(), to = found;
if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; }
cm.state.sublimeKilled = cm.getRange(from, to);
cm.replaceRange("", from, to);
}
};
cmds[map[cK + ctrl + "X"] = "swapWithSublimeMark"] = function(cm) {
var found = cm.state.sublimeMark && cm.state.sublimeMark.find();
if (found) {
cm.state.sublimeMark.clear();
cm.state.sublimeMark = cm.setBookmark(cm.getCursor());
cm.setCursor(found);
}
};
cmds[map[cK + ctrl + "Y"] = "sublimeYank"] = function(cm) {
if (cm.state.sublimeKilled != null)
cm.replaceSelection(cm.state.sublimeKilled, null, "paste");
};
map[cK + ctrl + "G"] = "clearBookmarks";
cmds[map[cK + ctrl + "C"] = "showInCenter"] = function(cm) {
var pos = cm.cursorCoords(null, "local");
cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2);
};
var selectLinesCombo = mac ? "Ctrl-Shift-" : "Ctrl-Alt-";
cmds[map[selectLinesCombo + "Up"] = "selectLinesUpward"] = function(cm) {
cm.operation(function() {
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (range.head.line > cm.firstLine())
cm.addSelection(Pos(range.head.line - 1, range.head.ch));
}
});
};
cmds[map[selectLinesCombo + "Down"] = "selectLinesDownward"] = function(cm) {
cm.operation(function() {
var ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (range.head.line < cm.lastLine())
cm.addSelection(Pos(range.head.line + 1, range.head.ch));
}
});
};
function getTarget(cm) {
var from = cm.getCursor("from"), to = cm.getCursor("to");
if (CodeMirror.cmpPos(from, to) == 0) {
var word = wordAt(cm, from);
if (!word.word) return;
from = word.from;
to = word.to;
}
return {from: from, to: to, query: cm.getRange(from, to), word: word};
}
function findAndGoTo(cm, forward) {
var target = getTarget(cm);
if (!target) return;
var query = target.query;
var cur = cm.getSearchCursor(query, forward ? target.to : target.from);
if (forward ? cur.findNext() : cur.findPrevious()) {
cm.setSelection(cur.from(), cur.to());
} else {
cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0)
: cm.clipPos(Pos(cm.lastLine())));
if (forward ? cur.findNext() : cur.findPrevious())
cm.setSelection(cur.from(), cur.to());
else if (target.word)
cm.setSelection(target.from, target.to);
}
};
cmds[map[ctrl + "F3"] = "findUnder"] = function(cm) { findAndGoTo(cm, true); };
cmds[map["Shift-" + ctrl + "F3"] = "findUnderPrevious"] = function(cm) { findAndGoTo(cm,false); };
cmds[map["Alt-F3"] = "findAllUnder"] = function(cm) {
var target = getTarget(cm);
if (!target) return;
var cur = cm.getSearchCursor(target.query);
var matches = [];
var primaryIndex = -1;
while (cur.findNext()) {
matches.push({anchor: cur.from(), head: cur.to()});
if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch)
primaryIndex++;
}
cm.setSelections(matches, primaryIndex);
};
map["Shift-" + ctrl + "["] = "fold";
map["Shift-" + ctrl + "]"] = "unfold";
map[cK + ctrl + "0"] = map[cK + ctrl + "J"] = "unfoldAll";
map[ctrl + "I"] = "findIncremental";
map["Shift-" + ctrl + "I"] = "findIncrementalReverse";
map[ctrl + "H"] = "replace";
map["F3"] = "findNext";
map["Shift-F3"] = "findPrev";
CodeMirror.normalizeKeyMap(map);
});

118
assets/codemirror/mode/yaml.js vendored Normal file
View File

@@ -0,0 +1,118 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://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.defineMode("yaml", function() {
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i');
return {
token: function(stream, state) {
var ch = stream.peek();
var esc = state.escaped;
state.escaped = false;
/* comments */
if (ch == "#" && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
stream.skipToEnd();
return "comment";
}
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))
return "string";
if (state.literal && stream.indentation() > state.keyCol) {
stream.skipToEnd(); return "string";
} else if (state.literal) { state.literal = false; }
if (stream.sol()) {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
/* document start */
if(stream.match(/---/)) { return "def"; }
/* document end */
if (stream.match(/\.\.\./)) { return "def"; }
/* array list item */
if (stream.match(/\s*-\s+/)) { return 'meta'; }
}
/* inline pairs/lists */
if (stream.match(/^(\{|\}|\[|\])/)) {
if (ch == '{')
state.inlinePairs++;
else if (ch == '}')
state.inlinePairs--;
else if (ch == '[')
state.inlineList++;
else
state.inlineList--;
return 'meta';
}
/* list seperator */
if (state.inlineList > 0 && !esc && ch == ',') {
stream.next();
return 'meta';
}
/* pairs seperator */
if (state.inlinePairs > 0 && !esc && ch == ',') {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
stream.next();
return 'meta';
}
/* start of value of a pair */
if (state.pairStart) {
/* block literals */
if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; };
/* references */
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; }
/* numbers */
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; }
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; }
/* keywords */
if (stream.match(keywordRegex)) { return 'keyword'; }
}
/* pairs (associative arrays) -> key */
if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) {
state.pair = true;
state.keyCol = stream.indentation();
return "atom";
}
if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; }
/* nothing found, continue */
state.pairStart = false;
state.escaped = (ch == '\\');
stream.next();
return null;
},
startState: function() {
return {
pair: false,
pairStart: false,
keyCol: 0,
inlinePairs: 0,
inlineList: 0,
literal: false,
escaped: false
};
}
};
});
CodeMirror.defineMIME("text/x-yaml", "yaml");
CodeMirror.defineMIME("text/yaml", "yaml");
});

View File

@@ -0,0 +1,99 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

File diff suppressed because one or more lines are too long

10253
assets/jquery/jquery-3.2.1.js vendored Normal file

File diff suppressed because it is too large Load Diff

4
assets/jquery/jquery-3.2.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2550
assets/lyicon/css/lyicon.css Executable file

File diff suppressed because it is too large Load Diff

BIN
assets/lyicon/fonts/lyicon.eot Executable file

Binary file not shown.

796
assets/lyicon/fonts/lyicon.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
assets/lyicon/fonts/lyicon.ttf Executable file

Binary file not shown.

BIN
assets/lyicon/fonts/lyicon.woff Executable file

Binary file not shown.

252
assets/swirl/css/swirl.css Normal file
View File

@@ -0,0 +1,252 @@
html,
body {
height: 100%;
}
.page-wrap {
min-height: 100%;
margin-bottom: -120px;
}
.page-push,
.footer {
height: 120px;
}
.nav-left {
overflow-x: hidden;
}
.icon.is-huge {
height: 4rem;
width: 4rem;
}
.icon.is-huge .fa {
font-size: 70px;
}
.notification.is-top {
position: fixed;
top: 0;
width: 100%;
z-index: 10000;
}
.modal-card-error {
background-color: #ff3860;
color: #fff;
padding: 5px 20px;
}
.is-vertical-marginless {
margin-top: 0;
margin-bottom: 0;
}
.is-vertical-paddingless {
padding-top: 0;
padding-bottom: 0;
}
.navbar-item.is-narrow {
padding-left: 0.25rem;
padding-right: 0.25rem
}
.progress.is-narrow:not(:last-child) {
margin-bottom: 0.5rem;
}
@media (max-width: 768px) {
.is-divider-vertical {
display: none;
}
}
.hero.is-mini .hero-body {
padding-bottom: 0.5rem;
padding-top: 0.5rem;
}
fieldset {
padding: 0;
margin: 0;
border: 0;
min-width: 0;
}
fieldset:not(:last-child) {
margin-bottom: 0.75rem;
}
.lead {
width: 100%;
margin-bottom: 10px !important;
font-style: italic;
color: #999;
}
.lead.is-bordered {
border-bottom: 1px solid #e5e5e5;
}
.lead.is-1 {
font-size: 3rem;
}
.lead.is-2 {
font-size: 2.5rem;
}
.lead.is-3 {
font-size: 2rem;
}
.lead.is-4 {
font-size: 1.5rem;
}
.lead.is-5 {
font-size: 1.25rem;
}
.lead.is-6 {
font-size: 1rem;
}
.lead.is-7 {
font-size: 0.75rem;
}
dl {
margin-top: 0;
margin-bottom: 20px;
}
dt,
dd {
line-height: 1.5rem;
}
dt {
font-weight: bold;
}
dd {
margin-left: 0;
}
@media (min-width: 769px) {
.is-horizontal dt {
float: left;
width: 160px;
clear: left;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.is-horizontal dd {
margin-left: 180px;
}
}
.is-vertical-middle {
vertical-align: middle !important;
}
.has-bg-white-ter {
background-color: whitesmoke !important;
}
.is-bottom-marginless {
margin-bottom: 0 !important;
}
.is-bottom-paddingless {
padding-bottom: 0 !important;
}
.tag:not(body).is-grey {
background-color: #7a7a7a;
color: whitesmoke;
}
.block {
font-size: 1rem;
}
.block:not(:last-child) {
margin-bottom: 1.5rem;
}
.block .block-header {
background-color: whitesmoke;
border: 1px solid lightgray;
border-bottom: none;
border-radius: 3px 3px 0 0;
font-weight: 700;
line-height: 1.25;
padding: 0.5em 0.75em;
}
.block .block-body {
padding: 1em 1.25em;
}
.block .block-body.is-bordered {
border: 1px solid lightgray;
border-radius: 0 0 3px 3px;
}
.columns.is-tight {
margin-left: 0;
margin-right: 0;
margin-top: 0;
}
.columns.is-tight > .column {
margin: 0;
padding: 0.25rem !important;
}
.columns.is-tight:not(:last-child) {
margin-bottom: 1.5rem;
}
.columns.is-tight:last-child {
margin-bottom: 0;
}
.content pre {
border-radius: 5px;
}
.tabs-content {
border: 1px solid #dbdbdb;
border-radius: 5px;
background-color: whitesmoke;
padding: 20px;
}
.tabs-content:not(:last-child) {
margin-bottom: 1.5rem;
}
.has-no-border {
border: none !important;
}
.has-no-top-border {
border-top: 0 !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.CodeMirror {
font-size: 0.8em;
border: 1px solid #ddd;
height: 450px !important;
}

BIN
assets/swirl/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

1918
assets/swirl/js/swirl.js Normal file

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,45 @@
///<reference path="../core/core.ts" />
namespace Swirl.Config {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Table = Swirl.Core.ListTable;
export class ListPage {
private table: Table;
constructor() {
this.table = new Table("#table-items");
// bind events
this.table.on("delete-config", this.deleteConfig.bind(this));
$("#btn-delete").click(this.deleteConfigs.bind(this));
}
private deleteConfig(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:eq(1)").text().trim();
let id = $tr.find(":checkbox:first").val();
Modal.confirm(`Are you sure to remove config: <strong>${name}</strong>?`, "Delete config", (dlg, e) => {
$ajax.post("delete", {ids: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
private deleteConfigs(e: JQueryEventObject) {
let ids = this.table.selectedKeys();
if (ids.length == 0) {
Modal.alert("Please select one or more items.");
return;
}
Modal.confirm(`Are you sure to remove ${ids.length} configs?`, "Delete configs", (dlg, e) => {
$ajax.post("delete", {ids: ids.join(",")}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
this.table.selectedRows().remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,10 @@
///<reference path="../core/core.ts" />
namespace Swirl.Config {
import OptionTable = Swirl.Core.OptionTable;
export class NewPage {
constructor() {
new OptionTable("#table-labels");
}
}
}

View File

@@ -0,0 +1,387 @@
/*!
* Swirl Ajax Library v1.0.0
* Copyright 2017 cuigh. All rights reserved.
*
* @author cuigh(noname@live.com)
*/
///<reference path="bulma.ts"/>
namespace Swirl.Core {
export type AjaxErrorHandler = (xhr: JQueryXHR, textStatus: string, error: string) => void;
/**
* AjaxResult
*/
export class AjaxResult {
success: boolean;
code?: number;
message?: string;
data?: any;
url?: string;
}
/**
* AjaxOptions
*/
export class AjaxOptions {
private static defaultOptions: AjaxOptions = new AjaxOptions();
/**
* request url
*/
url: string;
/**
* request method, GET/POST...
*/
method: AjaxMethod;
/**
* request data
*/
data?: Object;
/**
* request timeout time(ms)
*
* @type {number}
*/
timeout?: number = 30000;
/**
* send request by asynchronous
*
* @type {boolean}
*/
async?: boolean = true;
/**
* response data type
*/
dataType?: "text" | "html" | "json" | "jsonp" | "xml" | "script" | string;
/**
* AJAX trigger element for indicator
*
* @type {(Element | JQuery)}
*/
trigger?: Element | JQuery;
/**
* data encoder for POST request
*/
encoder: "none" | "form" | "json" = "json";
/**
* previous filter
*/
preHandler: (options: AjaxOptions) => void;
/**
* post filter
*/
postHandler: (options: AjaxOptions) => void;
/**
* error handler
*/
errorHandler: AjaxErrorHandler;
/**
* get default options
*
* @returns {AjaxOptions}
*/
static getDefaultOptions(): AjaxOptions {
return AjaxOptions.defaultOptions;
}
/**
* set default options
*
* @param options
*/
static setDefaultOptions(options: AjaxOptions) {
if (options) {
AjaxOptions.defaultOptions = options;
}
}
}
/**
* AJAX request method
*/
export enum AjaxMethod {
GET,
POST,
PUT,
DELETE,
HEAD,
TRACE,
OPTIONS,
CONNECT,
PATCH
}
/**
* AJAX Request
*/
export class AjaxRequest {
static preHandler: (options: AjaxOptions) => void = options => {
options.trigger && $(options.trigger).prop("disabled", true);
};
static postHandler: (options: AjaxOptions) => void = options => {
options.trigger && $(options.trigger).prop("disabled", false);
};
static errorHandler: (xhr: JQueryXHR, textStatus: string, error: string) => void = (xhr, status, err) => {
let msg: string;
if (xhr.responseJSON) {
// auxo web framework return: {code: 0, message: "xxx"}
let err = xhr.responseJSON;
msg = err.message;
if (err.code) {
msg += `(${err.code})`
}
} else if (xhr.status >= 400) {
msg = xhr.responseText || err || status;
} else {
return
}
Notification.show("danger", `AJAX: ${msg}`)
};
protected options: AjaxOptions;
constructor(url: string, method: AjaxMethod, data?: any) {
this.options = $.extend({
url: url,
method: method,
data: data,
preHandler: AjaxRequest.preHandler,
postHandler: AjaxRequest.postHandler,
errorHandler: AjaxRequest.errorHandler,
}, AjaxOptions.getDefaultOptions());
}
/**
* set pre handler
*
* @param handler
* @return {AjaxRequest}
*/
preHandler(handler: (options: AjaxOptions) => void): this {
this.options.preHandler = handler;
return this;
}
/**
* set post handler
*
* @param handler
* @return {AjaxRequest}
*/
postHandler(handler: (options: AjaxOptions) => void): this {
this.options.postHandler = handler;
return this;
}
/**
* set error handler
*
* @param handler
* @return {AjaxRequest}
*/
errorHandler(handler: AjaxErrorHandler): this {
this.options.errorHandler = handler;
return this;
}
/**
* set request timeout
*
* @param timeout
* @returns {AjaxRequest}
*/
timeout(timeout: number): this {
this.options.timeout = timeout;
return this;
}
/**
* set async option
*
* @param async
* @returns {AjaxRequest}
*/
async(async: boolean): this {
this.options.async = async;
return this;
}
/**
* set trigger element
*
* @param {(Element | JQuery)} elem
* @returns {AjaxRequest}
*/
trigger(elem: Element | JQuery): this {
this.options.trigger = elem;
return this;
}
/**
* get response as JSON
*
* @template T JSON data type
* @param {(r: T) => void} [callback] callback function
*/
json<T>(callback?: (r: T) => void): void | Promise<T> {
return this.result<T>("json", callback);
}
/**
* get response as text
*
* @param {(r: string) => void} [callback] callback function
*/
text(callback?: (r: string) => void): void | Promise<string> {
return this.result<string>("text", callback);
}
/**
* get response as HTML
*
* @param {(r: string) => void} [callback] callback function
*/
html(callback?: (r: string) => void): void | Promise<string> {
return this.result<string>("html", callback);
}
protected result<T>(dataType: string, callback?: (r: T) => void): void | Promise<T> {
this.options.dataType = dataType;
this.options.preHandler && this.options.preHandler(this.options);
let settings = this.buildSettings();
if (callback) {
$.ajax(settings).done(callback).always(() => {
this.options.postHandler && this.options.postHandler(this.options);
}).fail((xhr: JQueryXHR, textStatus: string, error: string) => {
this.options.errorHandler && this.options.errorHandler(xhr, textStatus, error);
});
} else {
return new Promise<T>((resolve, _) => {
$.ajax(settings).done((r: T) => {
resolve(r);
}).always(() => {
AjaxRequest.postHandler && AjaxRequest.postHandler(this.options);
}).fail((xhr: JQueryXHR, textStatus: string, error: string) => {
AjaxRequest.errorHandler && AjaxRequest.errorHandler(xhr, textStatus, error);
});
});
}
}
protected buildSettings(): JQueryAjaxSettings {
return {
url: this.options.url,
method: AjaxMethod[this.options.method],
data: this.options.data,
dataType: this.options.dataType,
timeout: this.options.timeout,
async: this.options.async,
};
}
}
/**
* AJAX GET Request
*/
export class AjaxGetRequest extends AjaxRequest {
/**
* get JSON response by jsonp
*
* @template T JSON data type
* @param {(r: T) => void} [callback] callback function
*/
jsonp<T>(callback?: (r: T) => void): void | Promise<T> {
return this.result<T>("jsonp", callback);
}
}
/**
* AJAX POST Request
*/
export class AjaxPostRequest extends AjaxRequest {
/**
* set encoder
*
* @param encoder
* @returns {AjaxPostRequest}
*/
encoder(encoder: "none" | "form" | "json"): this {
this.options.encoder = encoder;
return this;
}
protected buildSettings(): JQueryAjaxSettings {
let settings = super.buildSettings();
switch (this.options.encoder) {
case "none":
settings.contentType = false;
break;
case "json":
settings.contentType = "application/json; charset=UTF-8";
settings.data = JSON.stringify(this.options.data);
break;
case "form":
settings.contentType = "application/x-www-form-urlencoded; charset=UTF-8";
break;
}
return settings;
}
}
/**
* AJAX static entry class
*/
export class Ajax {
private constructor() {
}
/**
* Send GET request
*
* @static
* @param {string} url request url
* @param {Object} [args] request data
* @returns {Ajax} Ajax request instance
*/
static get(url: string, args?: Object): AjaxGetRequest {
return new AjaxGetRequest(url, AjaxMethod.GET, args);
}
/**
* Send POST request
*
* @static
* @param {string} url request url
* @param {(string | Object)} [data] request data
* @returns {Ajax} Ajax request instance
*/
static post(url: string, data?: string | Object): AjaxPostRequest {
return new AjaxPostRequest(url, AjaxMethod.POST, data);
}
}
/**
* AJAX interface(仅用于实现 $ajax 快捷对象)
*/
export interface AjaxStatic {
/**
* Send GET request
*
* @static
* @param {string} url request url
* @param {Object} [args] request data
* @returns {Ajax} Ajax request instance
*/
get(url: string, args?: Object): AjaxGetRequest;
/**
* Send POST request
*
* @static
* @param {string} url request url
* @param {(string | Object)} [data] request data
* @returns {Ajax} Ajax request instance
*/
post(url: string, data?: string | Object): AjaxPostRequest;
}
}
let $ajax: Swirl.Core.AjaxStatic = Swirl.Core.Ajax;

View File

@@ -0,0 +1,211 @@
namespace Swirl.Core {
export class Modal {
private static initialized: boolean;
private static active: Modal;
private $modal: JQuery;
private deletable: boolean;
constructor(modal: string | Element | JQuery) {
this.$modal = $(modal);
this.find(".modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .dismiss").click(e => this.close());
}
static initialize() {
if (!Modal.initialized) {
$('.modal-trigger').click(function () {
let target = $(this).data('target');
let dlg = new Modal("#" + target)
dlg.show();
});
$(document).on('keyup', function (e) {
// ESC Key
if (e.keyCode === 27) {
Modal.active && Modal.active.close();
}
});
Modal.initialized = true;
}
}
static close() {
Modal.active && Modal.active.close();
}
static current(): Modal {
return Modal.active;
}
static alert(content: string, title?: string, callback?: (dlg: Modal, e: JQueryEventObject) => void): Modal {
title = title || "Prompt";
let $elem = $(`<div class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">${title}</p>
<button class="delete"></button>
</header>
<section class="modal-card-body">${content}</section>
<footer class="modal-card-foot">
<button class="button is-primary">OK</button>
</footer>
</div>
</div>`).appendTo("body");
let dlg = new Modal($elem);
callback = callback || function (dlg: Modal, e: JQueryEventObject) { dlg.close(); };
$elem.find(".modal-card-foot>button:first").click(e => callback(dlg, e));
dlg.deletable = true;
dlg.show();
return dlg;
}
static confirm(content: string, title?: string, callback?: (dlg: Modal, e: JQueryEventObject) => void): Modal {
title = title || "Confirm";
let $elem = $(`<div class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">${title}</p>
<button class="delete"></button>
</header>
<section class="modal-card-body">${content}</section>
<footer class="modal-card-foot">
<button class="button is-primary">Confirm</button>
<button class="button dismiss">Cancel</button>
</footer>
</div>
</div>`).appendTo("body");
let dlg = new Modal($elem);
if (callback) {
$elem.find(".modal-card-foot>button:first").click(e => callback(dlg, e));
}
dlg.deletable = true;
dlg.show();
return dlg;
}
show() {
Modal.active && Modal.active.close();
$('html').addClass('is-clipped');
this.$modal.addClass('is-active').focus();
this.error();
Modal.active = this;
}
close() {
$('html').removeClass('is-clipped');
if (this.deletable) {
this.$modal.remove();
} else {
this.$modal.removeClass('is-active');
}
Modal.active = null;
}
error(msg?: string) {
let $error = this.find(".modal-card-error");
if (msg) {
if (!$error.length) {
$error = $('<section class="modal-card-error"></section>').insertAfter(this.find(".modal-card-body"));
}
$error.html(msg).show();
} else {
$error.hide();
}
}
find(selector: string | Element | JQuery): JQuery {
if (typeof selector === "string") {
return this.$modal.find(selector);
} else if (selector instanceof Element) {
return this.$modal.find(selector);
}
return this.$modal.find(selector);
}
}
export type NotificationType = "primary" | "info" | "success" | "warning" | "danger";
export interface NotificationOptions {
type: NotificationType;
time: number; // display time(in seconds), 0 is always hide
message: string;
}
export class Notification {
private $elem: JQuery;
private options: NotificationOptions;
constructor(options: NotificationOptions) {
this.options = $.extend({}, {
type: "primary",
time: 5,
}, options);
}
static show(type: NotificationType, msg: string, time?: number): Notification {
let n = new Notification({
type: type,
message: msg,
time: time,
});
n.show();
return n;
}
private show() {
this.$elem = $(`<div class="notification is-${this.options.type} has-text-centered is-marginless is-radiusless is-top" style="display:none">${this.options.message}</div>`).appendTo("body").fadeIn(250);
if (this.options.time > 0) {
setTimeout(() => this.hide(), this.options.time * 1000);
}
}
hide() {
this.$elem.fadeTo("slow", 0.01, () => this.$elem.remove());
}
}
export class Tab {
private static initialized: boolean;
private $tab: JQuery;
private $content: JQuery;
private active: number;
constructor(tab: string | Element | JQuery, content: string | Element | JQuery) {
this.$tab = $(tab);
this.$content = $(content);
this.active = this.$tab.find("li.is-active").index();
if (this.active == -1) {
this.select(0);
}
this.$tab.find("li").click(e => {
let $li = $(e.target).closest("li");
if (!$li.hasClass("is-active")) {
this.select($li.index());
}
});
}
static initialize() {
if (!Tab.initialized) {
$('.tabs').each((i, elem) => {
let target = $(elem).data("target");
new Tab(elem, "#" + target);
});
Tab.initialized = true;
}
}
select(index: number) {
if (this.active != index) {
this.$tab.find(`li.is-active`).removeClass("is-active");
this.$tab.find(`li:eq(${index})`).addClass("is-active");
this.$content.children(":visible").hide();
$(this.$content.children()[index]).show();
this.active = index;
}
}
}
}

View File

@@ -0,0 +1,82 @@
///<reference path="bulma.ts"/>
///<reference path="ajax.ts"/>
///<reference path="validator.ts"/>
///<reference path="form.ts"/>
///<reference path="dispatcher.ts"/>
///<reference path="table.ts"/>
namespace Swirl.Core {
class BulmaMarker implements ValidationMarker {
setError($input: JQuery, errors: string[]) {
let $field = this.getField($input);
// update input state
$input.removeClass('is-success').addClass('is-danger');
// set errors into errors block
let $errors = $field.find('div.errors');
if (!$errors.length) {
$errors = $('<div class="errors"/>').appendTo($field);
}
$errors.empty().append($.map(errors, (err: string) => `<p class="help is-danger">${err}</p>`)).show();
}
clearError($input: JQuery) {
let $field = this.getField($input);
// update input state
$input.removeClass('is-danger').addClass('is-success');
// clear errors
let $errors = $field.find("div.errors");
$errors.empty().hide();
}
reset($input: JQuery): void {
let $field = this.getField($input);
// update input state
$input.removeClass('is-danger is-success');
// clear errors
let $errors = $field.find("div.errors");
$errors.empty().hide();
}
private getField($input: JQuery): JQuery {
let $field = $input.closest(".field");
if ($field.hasClass("has-addons") || $field.hasClass("is-grouped")) {
$field = $field.parent();
}
return $field;
}
}
/**
* Initialize bulma adapters
*/
$(() => {
// menu burger
$('.navbar-burger').click(function () {
let $el = $(this);
let $target = $('#' + $el.data('target'));
$el.toggleClass('is-active');
$target.toggleClass('is-active');
});
// Modal
Modal.initialize();
// Tab
Tab.initialize();
// AJAX
AjaxRequest.preHandler = opts => opts.trigger && $(opts.trigger).addClass("is-loading");
AjaxRequest.postHandler = opts => opts.trigger && $(opts.trigger).removeClass("is-loading");
// Validator
Validator.setMarker(new BulmaMarker());
// Form
Form.automate();
});
}

View File

@@ -0,0 +1,71 @@
namespace Swirl.Core {
/**
* Dispatcher
*/
export class Dispatcher {
private attr: string;
private events: { [index: string]: (e: JQueryEventObject) => any } = {};
constructor(attr?: string) {
this.attr = attr || "action";
}
/**
* 创建一个 Dispatcher 并绑定事件到页面元素上
*
* @param elem
* @param event
* @returns {Dispatcher}
*/
static bind(elem: string | JQuery | Element | Document, event: string = "click"): Dispatcher {
return new Dispatcher().bind(elem, event);
}
/**
* 注册动作事件
*
* @param action
* @param handler
* @returns {Mtime.Util.Dispatcher}
*/
on(action: string, handler: (e: JQueryEventObject) => any): Dispatcher {
this.events[action] = handler;
return this;
}
/**
* 移除动作事件
*
* @param action
* @returns {Mtime.Util.Dispatcher}
*/
off(action: string): Dispatcher {
delete this.events[action];
return this;
}
/**
* 绑定事件到页面元素上
*
* @param elem
* @param event
* @returns {Mtime.Util.Dispatcher}
*/
bind(elem: string | JQuery | Element | Document, event: string = "click"): Dispatcher {
$(elem).on(event, this.handle.bind(this));
return this;
}
private handle(e: JQueryEventObject): any {
let action = $(e.target).closest("[data-" + this.attr + "]").data(this.attr);
if (action) {
let handler = this.events[action];
if (handler) {
e.stopPropagation();
handler(e);
}
}
}
}
}

View File

@@ -0,0 +1,420 @@
/*!
* Swirl Form Library v1.0.0
* Copyright 2017 cuigh. All rights reserved.
* see also: https://github.com/A1rPun/transForm.js
*
* @author cuigh(noname@live.com)
*/
///<reference path="validator.ts"/>
namespace Swirl.Core {
/**
* Form options
*/
export class FormOptions {
delimiter: string = ".";
skipDisabled: boolean = true;
skipReadOnly: boolean = false;
skipEmpty: boolean = false;
useIdOnEmptyName: boolean = true;
triggerChange: boolean = false;
}
interface Entry {
name: string;
value?: any;
}
/**
* Form
*/
export class Form {
private form: JQuery;
private options: FormOptions;
private validator: Validator;
/**
* Creates an instance of Form.
*
* @param {(string | Element | JQuery)} elem Form html element
* @param {FormOptions} options Form options
*
* @memberOf Form
*/
constructor(elem: string | Element | JQuery, options?: FormOptions) {
this.form = $(elem);
this.options = $.extend(new FormOptions(), options);
this.validator = Validator.bind(this.form);
}
/**
* Reset form
*
* @memberOf Form
*/
reset() {
(<HTMLFormElement>this.form.get(0)).reset();
if (this.validator) {
this.validator.reset();
}
}
/**
* Clear form data
*
* @memberOf Form
*/
clear() {
let inputs = this.getFields();
inputs.each((i, input) => {
this.clearInput(input);
});
if (this.validator) {
this.validator.reset();
}
}
/**
* Submit form by AJAX
*
* @param {string} url submit url
* @returns {Mtime.Net.AjaxPostRequest}
*
* @memberOf Form
*/
submit(url?: string): AjaxPostRequest {
let data = this.serialize();
return Ajax.post(url || this.form.attr("action"), data);
}
/**
* Validate form
*
* @returns {boolean}
*/
validate(): boolean {
if (!this.validator) {
return true;
}
return this.validator.validate().length == 0;
}
/**
* Serialize form data to JSON
*
* @param {Function} [nodeCallback] custom callback for parsing input value
* @returns {Object}
*
* @memberOf Form
*/
serialize(nodeCallback?: Function): Object {
let result = {},
inputs = this.getFields();
for (let i = 0, l = inputs.length; i < l; i++) {
let input: any = inputs[i],
key = input.name || this.options.useIdOnEmptyName && input.id;
if (!key) continue;
let entry: Entry = null;
if (nodeCallback) entry = nodeCallback(input);
if (!entry) entry = this.getEntryFromInput(input, key);
if (typeof entry.value === 'undefined' || entry.value === null
|| (this.options.skipEmpty && (!entry.value || (this.isArray(entry.value) && !entry.value.length))))
continue;
this.saveEntryToResult(result, entry, input, this.options.delimiter);
}
return result;
}
/**
* Fill data to form
*
* @param {Object} data JSON data
* @param {Function} [nodeCallback] custom callback for processing input value
*
* @memberOf Form
*/
deserialize(data: Object, nodeCallback?: Function) {
let inputs = this.getFields();
for (let i = 0, l = inputs.length; i < l; i++) {
let input: any = inputs[i],
key = input.name || this.options.useIdOnEmptyName && input.id,
value = this.getFieldValue(key, data);
if (typeof value === 'undefined' || value === null) {
this.clearInput(input);
continue;
}
let mutated = nodeCallback && nodeCallback(input, value);
if (!mutated) this.setValueToInput(input, value);
}
}
/**
* automatic initialize Form components using data attributes.
*/
static automate() {
$('form[data-form]').each(function (i, elem) {
let $form = $(elem);
let form = new Form($form);
let type = $form.data("form");
if (type == "form") {
$form.submit(e => { return form.validate() });
return;
}
// ajax-json | ajax-form
$form.submit(function () {
if (!form.validate()) {
return false;
}
let request = form.submit($form.attr("action")).trigger($form.find('button[type="submit"]'));
if (type == "ajax-form") {
request.encoder("form");
}
request.json((r: AjaxResult) => {
if (r.success) {
let url = r.url || $form.data("url");
if (url) {
if (url === "-") {
location.reload();
} else {
location.href = url;
}
} else {
let msg = r.message || $form.data("message");
Notification.show("info", `SUCCESS: ${msg}`, 3);
}
} else {
let msg = r.message;
if (r.code) {
msg += `({r.code})`
}
Notification.show("danger", `FAILED: ${msg}`);
}
});
return false;
});
});
}
private getEntryFromInput(input: any, key: string): Entry {
let nodeType = input.type && input.type.toLowerCase(),
entry: Entry = { name: key },
dataType = $(input).data("type");
switch (nodeType) {
case 'radio':
if (input.checked)
entry.value = input.value === 'on' ? true : input.value;
break;
case 'checkbox':
entry.value = input.checked ? (input.value === 'on' ? true : input.value) : false;
break;
case 'select-multiple':
entry.value = [];
for (let i = 0, l = input.options.length; i < l; i++)
if (input.options[i].selected) entry.value.push(input.options[i].value);
break;
case 'file':
//Only interested in the filename (Chrome adds C:\fakepath\ for security anyway)
entry.value = input.value.split('\\').pop();
break;
case 'button':
case 'submit':
case 'reset':
break;
default:
entry.value = input.value;
}
switch (dataType) {
case "integer":
entry.value = parseInt(entry.value);
break;
case "float":
entry.value = parseFloat(entry.value);
break;
case "bool":
entry.value = (entry.value === "true") || (entry.value === "1");
break;
}
return entry;
}
private saveEntryToResult(parent: { [index: string]: any }, entry: Entry, input: HTMLElement, delimiter: string) {
//not not accept empty values in array collections
if (/\[]$/.test(entry.name) && !entry.value) return;
let parts = this.parseString(entry.name, delimiter);
for (let i = 0, l = parts.length; i < l; i++) {
let part = parts[i];
//if last
if (i === l - 1) {
parent[part] = entry.value;
} else {
let index = parts[i + 1];
if (!index || $.isNumeric(index)) {
if (!this.isArray(parent[part]))
parent[part] = [];
//if second last
if (i === l - 2) {
parent[part].push(entry.value);
} else {
if (!this.isObject(parent[part][index]))
parent[part][index] = {};
parent = parent[part][index];
}
i++;
} else {
if (!this.isObject(parent[part]))
parent[part] = {};
parent = parent[part];
}
}
}
}
private clearInput(input: any) {
let nodeType = input.type && input.type.toLowerCase();
switch (nodeType) {
case 'select-one':
input.selectedIndex = 0;
break;
case 'select-multiple':
for (let i = input.options.length; i--;)
input.options[i].selected = false;
break;
case 'radio':
case 'checkbox':
if (input.checked) input.checked = false;
break;
case 'button':
case 'submit':
case 'reset':
case 'file':
break;
default:
input.value = '';
}
if (this.options.triggerChange) {
$(input).change();
}
}
private parseString(str: string, delimiter: string): string[] {
let result: string[] = [],
split = str.split(delimiter),
len = split.length;
for (let i = 0; i < len; i++) {
let s = split[i].split('['),
l = s.length;
for (let j = 0; j < l; j++) {
let key = s[j];
if (!key) {
//if the first one is empty, continue
if (j === 0) continue;
//if the undefined key is not the last part of the string, throw error
if (j !== l - 1)
throw new Error(`Undefined key is not the last part of the name > ${str}`);
}
//strip "]" if its there
if (key && key[key.length - 1] === ']')
key = key.slice(0, -1);
result.push(key);
}
}
return result;
}
private getFields(): JQuery {
let inputs: JQuery = this.form.find("input,select,textarea").filter(':not([data-form-ignore="true"])');
if (this.options.skipDisabled) inputs = inputs.filter(":not([disabled])");
if (this.options.skipReadOnly) inputs = inputs.filter(":not([readonly])");
return inputs;
}
private getFieldValue(key: string, ref: any): any {
if (!key || !ref) return;
let parts = this.parseString(key, this.options.delimiter);
for (let i = 0, l = parts.length; i < l; i++) {
let part = ref[parts[i]];
if (typeof part === 'undefined' || part === null) return;
//if last
if (i === l - 1) {
return part;
} else {
let index = parts[i + 1];
if (index === '') {
return part;
} else if ($.isNumeric(index)) {
//if second last
if (i === l - 2)
return part[index];
else
ref = part[index];
i++;
} else {
ref = part;
}
}
}
}
private setValueToInput(input: any, value: any) {
let nodeType = input.type && input.type.toLowerCase();
switch (nodeType) {
case 'radio':
if (value == input.value) input.checked = true;
break;
case 'checkbox':
input.checked = this.isArray(value)
? this.contains(value, input.value)
: value === true || value == input.value;
break;
case 'select-multiple':
if (this.isArray(value))
for (let i = input.options.length; i--;)
input.options[i].selected = this.contains(value, input.options[i].value);
else
input.value = value;
break;
case 'button':
case 'submit':
case 'reset':
case 'file':
break;
default:
input.value = value;
}
if (this.options.triggerChange) {
$(input).change();
}
}
/*** Helper functions ***/
private contains(array: any[], value: any): boolean {
for (let item of array) {
if (item == value) return true;
}
return false;
}
private isObject(obj: any) {
return typeof obj === 'object';
}
private isArray(arr: any) {
return Array.isArray(arr);
}
}
}

View File

@@ -0,0 +1,16 @@
// namespace Swirl.Core {
// export class ListBasePage {
// protected $table: ListTable;
//
// constructor() {
// this.$table = new ListTable("#table-items");
// }
// }
//
// export class TestListPage extends ListBasePage {
// constructor() {
// super();
// this.$table.on("", null);
// }
// }
// }

View File

@@ -0,0 +1,117 @@
/*!
* Swirl Table Library v1.0.0
* Copyright 2017 cuigh. All rights reserved.
*
* @author cuigh(noname@live.com)
*/
///<reference path="dispatcher.ts"/>
namespace Swirl.Core {
export class Table {
protected $table: JQuery;
private dispatcher: Dispatcher;
constructor(table: string | Element | JQuery) {
this.$table = $(table);
this.dispatcher = Dispatcher.bind(this.$table);
}
/**
* Bind action
*
* @param action
* @param handler
* @returns {Swirl.Core.ListTable}
*/
on(action: string, handler: (e: JQueryEventObject) => any): this {
this.dispatcher.on(action, handler);
return this;
}
}
export class ListTable extends Table {
constructor(table: string | Element | JQuery) {
super(table);
this.on("check-all", e => {
let checked = (<HTMLInputElement>e.target).checked;
this.$table.find("tbody>tr").each((i, elem) => {
$(elem).find("td:first>:checkbox").prop("checked", checked);
});
});
this.on("check", () => {
let rows = this.$table.find("tbody>tr").length;
let checkedRows = this.selectedRows().length;
this.$table.find("thead>tr>th:first>:checkbox").prop("checked", checkedRows > 0 && rows == checkedRows);
})
}
/**
* Return selected rows.
*/
selectedRows(): JQuery {
return this.$table.find("tbody>tr").filter((i, elem) => {
let cb = $(elem).find("td:first>:checkbox");
return cb.prop("checked");
});
}
/**
* Return keys of selected items.
*/
selectedKeys(): string[] {
let keys: string[] = [];
this.$table.find("tbody>tr").each((i, elem) => {
let cb = $(elem).find("td:first>:checkbox");
if (cb.prop("checked")) {
keys.push(cb.val());
}
});
return keys;
}
}
export abstract class EditTable extends Table {
protected name: string;
protected index: number;
protected alias: string;
constructor(elem: string | JQuery | Element) {
super(elem);
this.name = this.$table.data("name");
this.alias = this.name.replace(".", "-");
this.index = this.$table.find("tbody>tr").length;
super.on("add-" + this.alias, this.addRow.bind(this)).on("delete-" + this.alias, OptionTable.deleteRow);
}
protected abstract render(): string;
private addRow() {
this.$table.find("tbody").append(this.render());
this.index++;
}
private static deleteRow(e: JQueryEventObject) {
$(e.target).closest("tr").remove();
}
}
export class OptionTable extends EditTable {
constructor(elem: string | JQuery | Element) {
super(elem);
}
protected render(): string {
return `<tr>
<td><input name="${this.name}s[${this.index}].name" class="input is-small" type="text"></td>
<td><input name="${this.name}s[${this.index}].value" class="input is-small" type="text"></td>
<td>
<a class="button is-small is-danger is-outlined" data-action="delete-${this.alias}">
<span class="icon is-small"><i class="fa fa-trash"></i></span>
</a>
</td>
</tr>`;
}
}
}

View File

@@ -0,0 +1,410 @@
/*!
* Swirl Validator Library v1.0.0
* Copyright 2017 cuigh. All rights reserved.
*
* @author cuigh(noname@live.com)
*/
namespace Swirl.Core {
type InputElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLButtonElement;
/**
* 输入控件验证结果
*
* @interface ValidationResult
*/
export interface ValidationResult {
input: JQuery;
errors: string[];
}
// interface ValidationRule {
// ($input: JQuery): boolean;
// }
export interface ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string};
}
export interface ValidationMarker {
setError($input: JQuery, errors: string[]): void;
clearError($input: JQuery): void;
reset($input: JQuery): void;
}
/**
* HTML5 表单元素原生验证器
*
* @class NativeRule
* @implements {ValidationRule}
*/
class NativeRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
let el = <InputElement>$input[0];
return {ok: el.checkValidity ? el.checkValidity() : true};
}
}
/**
* 必填字段验证器
*
* @class RequiredRule
* @implements {ValidationRule}
*/
class RequiredRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
return {ok: $.trim($input.val()).length > 0};
}
}
/**
* 必选字段验证器(用于 radio 和 checkbox), 示例: checked, checked(2), checked(1~2)
*
* @class CheckedRule
* @implements {ValidationRule}
*/
class CheckedRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
let count = parseInt(arg);
let siblings = $form.find(`:input:checked[name='${$input.attr("name")}']`);
return {ok: siblings.length >= count};
}
}
/**
* 电子邮箱验证器
*
* @class EmailValidator
* @implements {ValidationRule}
*/
class EmailRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
const regex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
let value = $.trim($input.val());
return {ok: !value || regex.test(value)};
}
}
/**
* HTTP/FTP 地址验证器
*
* @class UrlValidator
* @implements {ValidationRule}
*/
class UrlRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
const regex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
let value = $.trim($input.val());
return {ok: !value || regex.test(value)};
}
}
/**
* IPV4 地址验证器
*
* @class IPValidator
* @implements {ValidationRule}
*/
class IPRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
const regex = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i;
let value = $.trim($input.val());
return {ok: !value || regex.test(value)};
}
}
/**
* 字段匹配验证器(如密码)
*
* @class MatchValidator
* @implements {ValidationRule}
*/
class MatchValidator implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
return {ok: $input.val() == $('#' + arg).val()};
}
}
/**
* 字符串长度验证器
*
* @class LengthValidator
* @implements {ValidationRule}
*/
class LengthRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
let r: {ok: boolean, error?: string} = {ok: true};
if (arg) {
let len = this.getLength($.trim($input.val()));
let args = arg.split('~');
if (args.length == 1) {
if ($.isNumeric(args[0])) {
r.ok = len >= parseInt(args[0]);
}
} else {
if ($.isNumeric(args[0]) && $.isNumeric(args[1])) {
r.ok = len >= parseInt(args[0]) && len <= parseInt(args[1])
}
}
}
return r;
}
protected getLength(value: string): number {
return value.length;
}
}
/**
* 字符串宽度验证器(中文字符宽度为2)
*
* @class WidthValidator
* @extends {LengthRule}
*/
class WidthRule extends LengthRule {
protected getLength(value: string): number {
var doubleByteChars = value.match(/[^\x00-\xff]/ig);
return value.length + (doubleByteChars == null ? 0 : doubleByteChars.length);
}
}
/**
* 整数验证器
*
* @class IntegerValidator
* @implements {ValidationRule}
*/
class IntegerRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
const regex = /^\d*$/;
return {ok: regex.test($.trim($input.val()))};
}
}
/**
* 正则表达式验证器
*
* @class RegexValidator
* @implements {ValidationRule}
*/
class RegexRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
let regex = new RegExp(arg, 'i');
let value = $.trim($input.val());
return {ok: !value || regex.test(value)};
}
}
/**
* 服务器端验证器
*
* @class RemoteRule
* @implements {ValidationRule}
*/
class RemoteRule implements ValidationRule {
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
if (!arg) {
throw new Error("服务器验证地址未设置");
}
let value = $.trim($input.val());
let r: {ok: boolean, error?: string} = {ok: false};
$ajax.post(arg, {value: value}).encoder("form").async(false).json<{error: string}>(result => {
r.ok = !result.error;
r.error = result.error;
});
return r;
}
}
export interface ValidatorOptions {
}
export class Validator {
private static selector = ':input[data-v-rule]:not(:submit,:button,:reset,:image,:disabled)';
// error marker
private static marker: ValidationMarker;
// error message
private static messages: { [index: string]: string } = {
"required": "This field is required",
"checked": "Number of checked items is invalid",
"email": "Please input a valid email address",
"match": "Input confirmation doesn't match",
"length": "The length of the field does not meet the requirements",
"width": "The width of the field does not meet the requirements",
"url": "Please input a valid url",
"ip": "Please input a valid IPV4 address",
"integer": "Please input an integer",
"regex": "Input is invalid",
"remote": "Input is invalid",
};
private static rules: { [index: string]: ValidationRule } = {
"native": new NativeRule(),
"required": new RequiredRule(),
"checked": new CheckedRule(),
"email": new EmailRule(),
"match": new MatchValidator(),
"length": new LengthRule(),
"width": new WidthRule(),
"url": new UrlRule(),
"ip": new IPRule(),
"integer": new IntegerRule(),
"regex": new RegexRule(),
"remote": new RemoteRule(),
};
private form: JQuery;
private options: Object;
/**
* Creates an instance of Validator.
*
* @param {(string | HTMLElement | JQuery)} elem the parent element which contains all form inputs
* @param {*} [options] the validation options
*
* @memberOf Validator
*/
private constructor(elem: string | HTMLElement | JQuery, options?: ValidatorOptions) {
this.form = $(elem);
this.options = options;
// disable default validation of HTML5, and bind submit event
if (this.form.is("form")) {
this.form.attr("novalidate", "true");
// this.form.submit(e => {
// let results = this.validate();
// if (results != null && results.length > 0) {
// e.preventDefault();
// }
// });
}
// realtime validate events
this.form.on("click", ':radio[data-v-rule],:checkbox[data-v-rule]', this.checkValue.bind(this));
this.form.on("change", 'select[data-v-rule],input[type="file"][data-v-rule]', this.checkValue.bind(this));
this.form.on("blur", ':input[data-v-rule]:not(select,:radio,:checkbox,:file)', this.checkValue.bind(this));
}
private checkValue(e: JQueryEventObject) {
let $input = $(e.target);
let result = this.validateInput($input);
Validator.mark($input, result);
}
/**
* 创建验证器并绑定到表单
*
* @static
* @param {(string | HTMLElement | JQuery)} elem 验证表单或其它容器元素
* @param {ValidatorOptions} [options]
* @returns {Validator} 选项
*
* @memberOf Validator
*/
static bind(elem: string | HTMLElement | JQuery, options?: ValidatorOptions): Validator {
let v = $(elem).data("validator");
if (!v) {
v = new Validator(elem, options);
$(elem).data("validator", v);
}
return v;
}
/**
* 验证表单
*
* @returns {ValidationResult[]}
*/
validate(): ValidationResult[] {
let results: ValidationResult[] = [];
this.form.find(Validator.selector).each((i, el) => {
let $input = $(el);
let result = this.validateInput($input);
if (result != null) {
results.push(result);
}
Validator.mark($input, result);
});
return results;
}
/**
* 清除验证标识
*/
reset(): void {
this.form.find(Validator.selector).each((i, el) => {
let $input = $(el);
Validator.marker.reset($input);
});
}
/**
* 注册验证器
*
* @static
* @param {string} name 验证器名称
* @param {ValidationRule} rule 验证方法
* @param {string} msg 验证消息
*
* @memberOf Validator
*/
static register(name: string, rule: ValidationRule, msg: string) {
this.rules[name] = rule;
this.messages[name] = msg;
}
/**
* set error message
*/
static setMessage(name: string, msg: string) {
this.messages[name] = msg;
}
/**
* set error marker
*/
static setMarker(marker: ValidationMarker) {
this.marker = marker;
}
private validateInput($input: JQuery): ValidationResult {
let errors: string[] = [];
let rules: string[]= ($input.data('v-rule') || 'native').split(';');
rules.forEach(name => {
let rule = Validator.rules[name];
if (rule) {
let arg = $input.data("v-arg-" + name);
let r = rule.validate(this.form, $input, arg);
if (!r.ok) {
errors.push(r.error || Validator.getMessge($input, name));
}
}
});
return (errors.length == 0) ? null : {
input: $input,
errors: errors,
};
}
private static mark($input: JQuery, result: ValidationResult) {
if (Validator.marker != null) {
if (result) {
Validator.marker.setError($input, result.errors);
}
else {
Validator.marker.clearError($input);
}
}
}
private static getMessge($input: JQuery, rule: string) {
// $input[0].validationMessage
// if (!success) $input[0].setCustomValidity("错误信息");
if (rule == 'native') return (<InputElement>$input[0]).validationMessage;
else {
let msg = $input.data('v-msg-' + rule);
if (!msg) msg = this.messages[rule];
return msg;
}
}
}
}

View File

@@ -0,0 +1,26 @@
///<reference path="../core/core.ts" />
namespace Swirl.Network {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class DetailPage {
constructor() {
let dispatcher = Dispatcher.bind("#table-containers");
dispatcher.on("disconnect", this.disconnect.bind(this));
}
private disconnect(e: JQueryEventObject) {
let $btn = $(e.target);
let $tr = $btn.closest("tr");
let id = $btn.val();
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to disconnect container: <strong>${name}</strong>?`, "Disconnect container", (dlg, e) => {
$ajax.post("disconnect", {container: id}).trigger($btn).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,24 @@
///<reference path="../core/core.ts" />
namespace Swirl.Network {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
let dispatcher = Dispatcher.bind("#table-items");
dispatcher.on("delete-network", this.deleteNetwork.bind(this));
}
private deleteNetwork(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to remove network: <strong>${name}</strong>?`, "Delete network", (dlg, e) => {
$ajax.post("delete", {name: name}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,19 @@
///<reference path="../core/core.ts" />
namespace Swirl.Network {
import OptionTable = Swirl.Core.OptionTable;
export class NewPage {
constructor() {
new OptionTable("#table-options");
new OptionTable("#table-labels");
$("#drivers :radio[name=driver]").change(e => {
$("#txt-custom-driver").prop("disabled", $(e.target).val() != "other");
});
$("#ipv6_enabled").change(e => {
let enabled = $(e.target).prop("checked");
$("#ipv6_subnet,#ipv6_gateway").prop("disabled", !enabled);
});
}
}
}

View File

@@ -0,0 +1,10 @@
///<reference path="../core/core.ts" />
namespace Swirl.Node {
import OptionTable = Swirl.Core.OptionTable;
export class EditPage {
constructor() {
new OptionTable("#table-labels");
}
}
}

View File

@@ -0,0 +1,26 @@
///<reference path="../core/core.ts" />
namespace Swirl.Node {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
let dispatcher = Dispatcher.bind("#table-items");
dispatcher.on("delete-node", this.deleteNode.bind(this));
}
private deleteNode(e: JQueryEventObject) {
let $btn = $(e.target);
let $tr = $btn.closest("tr");
let id =$btn.val();
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to remove node: <strong>${name}</strong>?`, "Delete node", (dlg, e) => {
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,36 @@
///<reference path="../core/core.ts" />
namespace Swirl.Registry {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
let dispatcher = Dispatcher.bind("#table-items");
dispatcher.on("delete-registry", this.deleteRegistry.bind(this));
dispatcher.on("edit-registry", this.editRegistry.bind(this));
}
private deleteRegistry(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to remove registry: <strong>${name}</strong>?`, "Delete registry", (dlg, e) => {
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
private editRegistry(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let dlg = new Modal("#dlg-edit");
dlg.find("input[name=id]").val($tr.data("id"));
dlg.find("input[name=name]").val($tr.find("td:first").text().trim());
dlg.find("input[name=url]").val($tr.find("td:eq(1)").text().trim());
dlg.find("input[name=username]").val($tr.find("td:eq(2)").text().trim());
dlg.show();
}
}
}

View File

@@ -0,0 +1,32 @@
///<reference path="../core/core.ts" />
namespace Swirl.Role {
import Dispatcher = Swirl.Core.Dispatcher;
export class EditPage {
constructor() {
$("#table-perms").find("tr").each((i, elem) => {
let $tr = $(elem);
let $cbs = $tr.find("td :checkbox");
$tr.find("th>:checkbox").prop("checked", $cbs.length == $cbs.filter(":checked").length);
});
// bind events
Dispatcher.bind("#table-perms", "change")
.on("check-row", this.checkRow.bind(this))
.on("check", this.check.bind(this))
}
private checkRow(e: JQueryEventObject) {
let $cb = $(e.target);
let checked = $cb.prop("checked");
$cb.closest("th").next("td").find(":checkbox").prop("checked", checked);
}
private check(e: JQueryEventObject) {
let $cb = $(e.target);
let $cbs = $cb.closest("td").find(":checkbox");
let checked = $cbs.length == $cbs.filter(":checked").length;
$cb.closest("td").prev("th").find(":checkbox").prop("checked", checked);
}
}
}

View File

@@ -0,0 +1,26 @@
///<reference path="../core/core.ts" />
namespace Swirl.Role {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
// bind events
Dispatcher.bind("#table-items")
.on("delete-role", this.deleteUser.bind(this))
}
private deleteUser(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to remove role: <strong>${name}</strong>?`, "Delete role", (dlg, e) => {
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,26 @@
///<reference path="../core/core.ts" />
namespace Swirl.Role {
import Dispatcher = Swirl.Core.Dispatcher;
export class NewPage {
constructor() {
// bind events
Dispatcher.bind("#table-perms", "change")
.on("check-row", this.checkRow.bind(this))
.on("check", this.check.bind(this))
}
private checkRow(e: JQueryEventObject) {
let $cb = $(e.target);
let checked = $cb.prop("checked");
$cb.closest("th").next("td").find(":checkbox").prop("checked", checked);
}
private check(e: JQueryEventObject) {
let $cb = $(e.target);
let $cbs = $cb.closest("td").find(":checkbox");
let checked = $cbs.length == $cbs.filter(":checked").length;
$cb.closest("td").prev("th").find(":checkbox").prop("checked", checked);
}
}
}

View File

@@ -0,0 +1,45 @@
///<reference path="../core/core.ts" />
namespace Swirl.Secret {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Table = Swirl.Core.ListTable;
export class ListPage {
private table: Table;
constructor() {
this.table = new Table("#table-items");
// bind events
this.table.on("delete-secret", this.deleteSecret.bind(this));
$("#btn-delete").click(this.deleteSecrets.bind(this));
}
private deleteSecret(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:eq(1)").text().trim();
let id = $tr.find(":checkbox:first").val();
Modal.confirm(`Are you sure to remove secret: <strong>${name}</strong>?`, "Delete secret", (dlg, e) => {
$ajax.post("delete", { ids: id }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
private deleteSecrets(e: JQueryEventObject) {
let ids = this.table.selectedKeys();
if (ids.length == 0) {
Modal.alert("Please select one or more items.")
return;
}
Modal.confirm(`Are you sure to remove ${ids.length} secrets?`, "Delete secrets", (dlg, e) => {
$ajax.post("delete", { ids: ids.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
this.table.selectedRows().remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,10 @@
///<reference path="../core/core.ts" />
namespace Swirl.Secret {
import OptionTable = Swirl.Core.OptionTable;
export class NewPage {
constructor() {
new OptionTable("#table-labels");
}
}
}

View File

@@ -0,0 +1,258 @@
///<reference path="../core/core.ts" />
namespace Swirl.Service {
import Validator = Swirl.Core.Validator;
import OptionTable = Swirl.Core.OptionTable;
import EditTable = Swirl.Core.EditTable;
import Modal = Swirl.Core.Modal;
import Table = Swirl.Core.Table;
class ServiceModeRule implements Swirl.Core.ValidationRule {
private $mode: JQuery;
constructor($model: JQuery) {
this.$mode = $model;
}
validate($form: JQuery, $input: JQuery, arg?: string): { ok: boolean, error?: string } {
if (this.$mode.val() == "global") {
return { ok: true }
}
const regex = /^[1-9]\d*$/;
return { ok: regex.test($.trim($input.val())) };
}
}
class MountTable extends EditTable {
protected render(): string {
return `<tr>
<td>
<div class="select is-small">
<select name="mounts[${this.index}].type">
<option value="bind">Bind</option>
<option value="volume">Volume</option>
<option value="tmpfs">TempFS</option>
</select>
</div>
</td>
<td>
<input name="mounts[${this.index}].src" class="input is-small" placeholder="path in host">
</td>
<td><input name="mounts[${this.index}].dst" class="input is-small" placeholder="path in container"></td>
<td>
<div class="select is-small">
<select name="mounts[${this.index}].read_only" data-type="bool">
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</div>
</td>
<td>
<div class="select is-small">
<select name="mounts[${this.index}].propagation">
<option value="">--Select--</option>
<option value="rprivate">rprivate</option>
<option value="private">private</option>
<option value="rshared">rshared</option>
<option value="shared">shared</option>
<option value="rslave">rslave</option>
<option value="slave">slave</option>
</select>
</div>
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-mount">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>`;
}
}
class PortTable extends EditTable {
protected render(): string {
return `<tr>
<td><input name="endpoint.ports[${this.index}].published_port" class="input is-small" placeholder="port in host" data-type="integer"></td>
<td>
<input name="endpoint.ports[${this.index}].target_port" class="input is-small" placeholder="port in container" data-type="integer">
</td>
<td>
<div class="select is-small">
<select name="endpoint.ports[${this.index}].protocol">
<option value="false">TCP</option>
<option value="true">UDP</option>
</select>
</div>
</td>
<td>
<div class="select is-small">
<select name="endpoint.ports[${this.index}].publish_mode">
<option value="rprivate">ingress</option>
<option value="private">host</option>
</select>
</div>
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-endpoint-port">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>`;
}
}
class ConstraintTable extends EditTable {
protected render(): string {
return `<tr>
<td>
<input name="placement.constraints[${this.index}].name" class="input is-small" placeholder="e.g. node.role/node.hostname/node.id/node.labels.*/engine.labels.*/...">
</td>
<td>
<div class="select is-small">
<select name="placement.constraints[${this.index}].op">
<option value="==">==</option>
<option value="!=">!=</option>
</select>
</div>
</td>
<td>
<input name="placement.constraints[${this.index}].value" class="input is-small" placeholder="e.g. manager">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-constraint">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>`;
}
}
class PreferenceTable extends EditTable {
protected render(): string {
return `<tr>
<td>
<input name="placement.preferences[${this.index}].spread" class="input is-small" placeholder="e.g. engine.labels.az">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-preference">
<span class="icon is-small">
<i class="fa fa-trash"></i>
</span>
</a>
</td>
</tr>`;
}
}
class ConfigTable extends Table {
public readonly name: string;
private index: number;
private $body: JQuery;
constructor(elem: string | JQuery | Element) {
super(elem);
this.name = this.$table.data("name");
this.$body = this.$table.find("tbody");
this.index = this.$body.find("tr").length;
super.on("add-" + this.name, this.showAddDialog.bind(this)).on("delete-" + this.name, ConfigTable.deleteRow);
}
public addRow(id: string, name: string) {
let field = `${this.name}s[${this.index}]`;
this.$body.append(`<tr>
<td>${name}<input name="${field}.id" value="${id}" type="hidden"><input name="${field}.name" value="${name}" type="hidden"></td>
<td><input name="${field}.file_name" value="${name}" class="input is-small"></td>
<td><input name="${field}.uid" value="0" class="input is-small"></td>
<td><input name="${field}.gid" value="0" class="input is-small"></td>
<td><input name="${field}.mode" value="444" class="input is-small" data-type="integer"></td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-${this.name}">
<span class="icon is-small">
<i class="fa fa-remove"></i>
</span>
</a>
</td>
</tr>`);
this.index++;
}
private showAddDialog(e: JQueryEventObject) {
let dlg = new Modal("#dlg-add-"+this.name);
dlg.find(":checked").prop("checked", false);
dlg.error();
dlg.show();
}
private static deleteRow(e: JQueryEventObject) {
$(e.target).closest("tr").remove();
}
}
export class EditPage {
private $mode: JQuery;
private $replicas: JQuery;
private secret: ConfigTable;
private config: ConfigTable;
constructor() {
this.$mode = $("#cb-mode");
this.$replicas = $("#txt-replicas");
new OptionTable("#table-envs");
new OptionTable("#table-slabels");
new OptionTable("#table-clabels");
new OptionTable("#table-log_driver-options");
new PortTable("#table-endpoint-ports");
new MountTable("#table-mounts");
new ConstraintTable("#table-constraints");
new PreferenceTable("#table-preferences");
this.secret = new ConfigTable("#table-secrets");
this.config = new ConfigTable("#table-configs");
// register custom validators
Validator.register("service-mode", new ServiceModeRule(this.$mode), "Please input a positive integer.");
// bind events
this.$mode.change(e => this.$replicas.toggle(this.$mode.val() != "global"))
$("#btn-add-secret").click(() => EditPage.addConfig(this.secret));
$("#btn-add-config").click(() => EditPage.addConfig(this.config));
}
private static addConfig(t: ConfigTable) {
let dlg = Modal.current();
let $cbs = dlg.find(":checked");
if ($cbs.length == 0) {
dlg.error(`Please select the ${t.name} files.`)
} else {
dlg.close();
$cbs.each((i, cb) => {
let $cb = $(cb);
t.addRow($cb.val(), $cb.data("name"));
});
}
}
}
export class NewPage extends EditPage {
private $registry: JQuery;
private $registryUrl: JQuery;
constructor() {
super();
this.$registryUrl = $("#a-registry-url");
this.$registry = $("#cb-registry");
this.$registry.change(e => {
let url = this.$registry.find("option:selected").data("url") || "";
this.$registryUrl.text(url + "/").toggle(url != "");
})
}
}
}

View File

@@ -0,0 +1,59 @@
///<reference path="../core/core.ts" />
namespace Swirl.Service {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Table = Swirl.Core.ListTable;
export class ListPage {
private table: Table;
constructor() {
this.table = new Table("#table-items");
// bind events
this.table.on("delete-service", this.deleteService.bind(this)).on("scale-service", this.scaleService.bind(this))
$("#btn-delete").click(this.deleteServices.bind(this));
}
private deleteService(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:eq(1)").text().trim();
Modal.confirm(`Are you sure to remove service: <strong>${name}</strong>?`, "Delete service", (dlg, e) => {
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
private deleteServices(e: JQueryEventObject) {
let names = this.table.selectedKeys();
if (names.length == 0) {
Modal.alert("Please select one or more items.")
return;
}
Modal.confirm(`Are you sure to remove ${names.length} services?`, "Delete services", (dlg, e) => {
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
this.table.selectedRows().remove();
dlg.close();
})
});
}
private scaleService(e: JQueryEventObject) {
let $btn = $(e.target);
let $tr = $btn.closest("tr");
let data = {
name: $tr.find("td:eq(1)").text().trim(),
count: $btn.data("replicas"),
};
Modal.confirm(`<input name="count" value="${data.count}" class="input" placeholder="Replicas">`, "Scale service", (dlg, e) => {
data.count = dlg.find("input[name=count]").val();
$ajax.post("scale", data).trigger($btn).encoder("form").json<AjaxResult>(r => {
location.reload();
})
});
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Swirl.Setting {
export class IndexPage {
constructor() {
$("#ldap-enabled").change(e => {
let enabled = $(e.target).prop("checked");
$("#fs-ldap").find("input:not(:checkbox)").prop("readonly", !enabled);
});
}
}
}

View File

@@ -0,0 +1,37 @@
///<reference path="../../core/core.ts" />
namespace Swirl.Stack.Archive {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
let dispatcher = Dispatcher.bind("#table-items");
dispatcher.on("deploy-archive", this.deployArchive.bind(this));
dispatcher.on("delete-archive", this.deleteArchive.bind(this));
}
private deployArchive(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to deploy archive: <strong>${name}</strong>?`, "Deploy archive", (dlg, e) => {
$ajax.post("deploy", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
dlg.close();
})
});
}
private deleteArchive(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to remove archive: <strong>${name}</strong>?`, "Delete archive", (dlg, e) => {
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,24 @@
///<reference path="../../core/core.ts" />
namespace Swirl.Stack.Task {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
let dispatcher = Dispatcher.bind("#table-items");
dispatcher.on("delete-stack", this.deleteStack.bind(this));
}
private deleteStack(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to remove stack: <strong>${name}</strong>?`, "Delete stack", (dlg, e) => {
$ajax.post("delete", {name: name}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
}
}

View File

@@ -0,0 +1,50 @@
///<reference path="../core/core.ts" />
namespace Swirl.User {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
export class ListPage {
constructor() {
// bind events
Dispatcher.bind("#table-items")
.on("delete-user", this.deleteUser.bind(this))
.on("block-user", this.blockUser.bind(this))
.on("unblock-user", this.unblockUser.bind(this));
}
private deleteUser(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to remove user: <strong>${name}</strong>?`, "Delete user", (dlg, e) => {
$ajax.post("delete", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
private blockUser(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to block user: <strong>${name}</strong>?`, "Block user", (dlg, e) => {
$ajax.post("block", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
location.reload();
})
});
}
private unblockUser(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let id = $tr.data("id");
let name = $tr.find("td:first").text().trim();
Modal.confirm(`Are you sure to unblock user: <strong>${name}</strong>?`, "Unblock user", (dlg, e) => {
$ajax.post("unblock", {id: id}).trigger(e.target).encoder("form").json<AjaxResult>(r => {
location.reload();
})
});
}
}
}

View File

@@ -0,0 +1,53 @@
///<reference path="../core/core.ts" />
namespace Swirl.Volume {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Table = Swirl.Core.ListTable;
export class ListPage {
private table: Table;
constructor() {
this.table = new Table("#table-items");
// bind events
this.table.on("delete-volume", this.deleteVolume.bind(this));
$("#btn-delete").click(this.deleteVolumes.bind(this));
$("#btn-prune").click(this.pruneVolumes.bind(this));
}
private deleteVolume(e: JQueryEventObject) {
let $tr = $(e.target).closest("tr");
let name = $tr.find("td:eq(1)").text().trim();
Modal.confirm(`Are you sure to remove volume: <strong>${name}</strong>?`, "Delete volume", (dlg, e) => {
$ajax.post("delete", { names: name }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
$tr.remove();
dlg.close();
})
});
}
private deleteVolumes(e: JQueryEventObject) {
let names = this.table.selectedKeys();
if (names.length == 0) {
Modal.alert("Please select one or more items.")
return;
}
Modal.confirm(`Are you sure to remove ${names.length} volumes?`, "Delete volumes", (dlg, e) => {
$ajax.post("delete", { names: names.join(",") }).trigger(e.target).encoder("form").json<AjaxResult>(r => {
this.table.selectedRows().remove();
dlg.close();
})
});
}
private pruneVolumes(e: JQueryEventObject) {
Modal.confirm(`Are you sure to remove all unused volumes?`, "Prune volumes", (dlg, e) => {
$ajax.post("prune").trigger(e.target).json<AjaxResult>(r => {
location.reload();
})
});
}
}
}

View File

@@ -0,0 +1,15 @@
///<reference path="../core/core.ts" />
namespace Swirl.Volume {
import OptionTable = Swirl.Core.OptionTable;
export class NewPage {
constructor() {
new OptionTable("#table-options");
new OptionTable("#table-labels");
$("#drivers").find(":radio[name=driver]").change(e => {
$("#txt-custom-driver").prop("disabled", $(e.target).val() != "other");
});
}
}
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2015",
"noImplicitAny": true,
"removeComments": true,
"sourceMap": true,
"outFile": "js/swirl.js",
"lib": ["dom", "es2015.promise", "es5"]
}
}

3218
assets/swirl/typings/jquery.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

15
biz/biz.go Normal file
View File

@@ -0,0 +1,15 @@
package biz
import (
"github.com/cuigh/auxo/errors"
"github.com/cuigh/swirl/dao"
)
func do(fn func(d dao.Interface)) {
d, err := dao.Get()
if err != nil {
panic(errors.Wrap("failed to load storage engine", err))
}
fn(d)
}

23
biz/docker/compose.go Normal file
View File

@@ -0,0 +1,23 @@
package docker
//import (
// "io/ioutil"
//
// "github.com/docker/cli/cli/compose/loader"
// composetypes "github.com/docker/cli/cli/compose/types"
//)
//
//func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
// bytes, err := ioutil.ReadFile(filename)
// if err != nil {
// return nil, err
// }
// config, err := loader.ParseYAML(bytes)
// if err != nil {
// return nil, err
// }
// return &composetypes.ConfigFile{
// Filename: filename,
// Config: config,
// }, nil
//}

View File

@@ -0,0 +1 @@
see: https://github.com/docker/cli

View File

@@ -0,0 +1,141 @@
package compose
import (
"io/ioutil"
"strings"
"github.com/docker/docker/api/types"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
)
const (
// LabelNamespace is the label used to track stack resources
LabelNamespace = "com.docker.stack.namespace"
)
// Namespace mangles names by prepending the name
type Namespace struct {
name string
}
// Scope prepends the namespace to a name
func (n Namespace) Scope(name string) string {
return n.name + "_" + name
}
// Descope returns the name without the namespace prefix
func (n Namespace) Descope(name string) string {
return strings.TrimPrefix(name, n.name+"_")
}
// Name returns the name of the namespace
func (n Namespace) Name() string {
return n.name
}
// NewNamespace returns a new Namespace for scoping of names
func NewNamespace(name string) Namespace {
return Namespace{name: name}
}
// AddStackLabel returns labels with the namespace label added
func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string {
if labels == nil {
labels = make(map[string]string)
}
labels[LabelNamespace] = namespace.name
return labels
}
type networkMap map[string]NetworkConfig
// Networks from the compose-file type to the engine API type
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) {
if networks == nil {
networks = make(map[string]NetworkConfig)
}
externalNetworks := []string{}
result := make(map[string]types.NetworkCreate)
for internalName := range servicesNetworks {
network := networks[internalName]
if network.External.External {
externalNetworks = append(externalNetworks, network.External.Name)
continue
}
createOpts := types.NetworkCreate{
Labels: AddStackLabel(namespace, network.Labels),
Driver: network.Driver,
Options: network.DriverOpts,
Internal: network.Internal,
Attachable: network.Attachable,
}
if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 {
createOpts.IPAM = &networktypes.IPAM{}
}
if network.Ipam.Driver != "" {
createOpts.IPAM.Driver = network.Ipam.Driver
}
for _, ipamConfig := range network.Ipam.Config {
config := networktypes.IPAMConfig{
Subnet: ipamConfig.Subnet,
}
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
}
result[internalName] = createOpts
}
return result, externalNetworks
}
// Secrets converts secrets from the Compose type to the engine API type
func Secrets(namespace Namespace, secrets map[string]SecretConfig) ([]swarm.SecretSpec, error) {
result := []swarm.SecretSpec{}
for name, secret := range secrets {
if secret.External.External {
continue
}
data, err := ioutil.ReadFile(secret.File)
if err != nil {
return nil, err
}
result = append(result, swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: namespace.Scope(name),
Labels: AddStackLabel(namespace, secret.Labels),
},
Data: data,
})
}
return result, nil
}
// Configs converts config objects from the Compose type to the engine API type
func Configs(namespace Namespace, configs map[string]ConfigObjConfig) ([]swarm.ConfigSpec, error) {
result := []swarm.ConfigSpec{}
for name, config := range configs {
if config.External.External {
continue
}
data, err := ioutil.ReadFile(config.File)
if err != nil {
return nil, err
}
result = append(result, swarm.ConfigSpec{
Annotations: swarm.Annotations{
Name: namespace.Scope(name),
Labels: AddStackLabel(namespace, config.Labels),
},
Data: data,
})
}
return result, nil
}

View File

@@ -0,0 +1,680 @@
package compose
import (
"fmt"
"os"
"sort"
"strings"
"time"
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client"
"github.com/pkg/errors"
)
const (
defaultNetwork = "default"
// LabelImage is the label used to store image name provided in the compose file
LabelImage = "com.docker.stack.image"
)
// Services from compose-file types to engine API types
func Services(
namespace Namespace,
config *Config,
client *client.Client,
) (map[string]swarm.ServiceSpec, error) {
result := make(map[string]swarm.ServiceSpec)
services := config.Services
volumes := config.Volumes
networks := config.Networks
for _, service := range services {
secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
configs, err := convertServiceConfigObjs(client, namespace, service.Configs, config.Configs)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
serviceSpec, err := Service(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs)
if err != nil {
return nil, errors.Wrapf(err, "service %s", service.Name)
}
result[service.Name] = serviceSpec
}
return result, nil
}
// Service converts a ServiceConfig into a swarm ServiceSpec
func Service(
apiVersion string,
namespace Namespace,
service ServiceConfig,
networkConfigs map[string]NetworkConfig,
volumes map[string]VolumeConfig,
secrets []*swarm.SecretReference,
configs []*swarm.ConfigReference,
) (swarm.ServiceSpec, error) {
name := namespace.Scope(service.Name)
endpoint, err := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
if err != nil {
return swarm.ServiceSpec{}, err
}
mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
if err != nil {
return swarm.ServiceSpec{}, err
}
mounts, err := Volumes(service.Volumes, volumes, namespace)
if err != nil {
return swarm.ServiceSpec{}, err
}
resources, err := convertResources(service.Deploy.Resources)
if err != nil {
return swarm.ServiceSpec{}, err
}
restartPolicy, err := convertRestartPolicy(
service.Restart, service.Deploy.RestartPolicy)
if err != nil {
return swarm.ServiceSpec{}, err
}
healthcheck, err := convertHealthcheck(service.HealthCheck)
if err != nil {
return swarm.ServiceSpec{}, err
}
networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
if err != nil {
return swarm.ServiceSpec{}, err
}
dnsConfig, err := convertDNSConfig(service.DNS, service.DNSSearch)
if err != nil {
return swarm.ServiceSpec{}, err
}
var privileges swarm.Privileges
privileges.CredentialSpec, err = convertCredentialSpec(service.CredentialSpec)
if err != nil {
return swarm.ServiceSpec{}, err
}
var logDriver *swarm.Driver
if service.Logging != nil {
logDriver = &swarm.Driver{
Name: service.Logging.Driver,
Options: service.Logging.Options,
}
}
serviceSpec := swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: name,
Labels: AddStackLabel(namespace, service.Deploy.Labels),
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: service.Image,
Command: service.Entrypoint,
Args: service.Command,
Hostname: service.Hostname,
Hosts: sortStrings(convertExtraHosts(service.ExtraHosts)),
DNSConfig: dnsConfig,
Healthcheck: healthcheck,
Env: sortStrings(convertEnvironment(service.Environment)),
Labels: AddStackLabel(namespace, service.Labels),
Dir: service.WorkingDir,
User: service.User,
Mounts: mounts,
StopGracePeriod: service.StopGracePeriod,
StopSignal: service.StopSignal,
TTY: service.Tty,
OpenStdin: service.StdinOpen,
Secrets: secrets,
Configs: configs,
ReadOnly: service.ReadOnly,
Privileges: &privileges,
},
LogDriver: logDriver,
Resources: resources,
RestartPolicy: restartPolicy,
Placement: &swarm.Placement{
Constraints: service.Deploy.Placement.Constraints,
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
},
},
EndpointSpec: endpoint,
Mode: mode,
UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
}
// add an image label to serviceSpec
serviceSpec.Labels[LabelImage] = service.Image
// ServiceSpec.Networks is deprecated and should not have been used by
// this package. It is possible to update TaskTemplate.Networks, but it
// is not possible to update ServiceSpec.Networks. Unfortunately, we
// can't unconditionally start using TaskTemplate.Networks, because that
// will break with older daemons that don't support migrating from
// ServiceSpec.Networks to TaskTemplate.Networks. So which field to use
// is conditional on daemon version.
if versions.LessThan(apiVersion, "1.29") {
serviceSpec.Networks = networks
} else {
serviceSpec.TaskTemplate.Networks = networks
}
return serviceSpec, nil
}
func getPlacementPreference(preferences []PlacementPreferences) []swarm.PlacementPreference {
result := []swarm.PlacementPreference{}
for _, preference := range preferences {
spreadDescriptor := preference.Spread
result = append(result, swarm.PlacementPreference{
Spread: &swarm.SpreadOver{
SpreadDescriptor: spreadDescriptor,
},
})
}
return result
}
func sortStrings(strs []string) []string {
sort.Strings(strs)
return strs
}
type byNetworkTarget []swarm.NetworkAttachmentConfig
func (a byNetworkTarget) Len() int { return len(a) }
func (a byNetworkTarget) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byNetworkTarget) Less(i, j int) bool { return a[i].Target < a[j].Target }
func convertServiceNetworks(
networks map[string]*ServiceNetworkConfig,
networkConfigs networkMap,
namespace Namespace,
name string,
) ([]swarm.NetworkAttachmentConfig, error) {
if len(networks) == 0 {
networks = map[string]*ServiceNetworkConfig{
defaultNetwork: {},
}
}
nets := []swarm.NetworkAttachmentConfig{}
for networkName, network := range networks {
networkConfig, ok := networkConfigs[networkName]
if !ok && networkName != defaultNetwork {
return nil, errors.Errorf("undefined network %q", networkName)
}
var aliases []string
if network != nil {
aliases = network.Aliases
}
target := namespace.Scope(networkName)
if networkConfig.External.External {
target = networkConfig.External.Name
}
netAttachConfig := swarm.NetworkAttachmentConfig{
Target: target,
Aliases: aliases,
}
// Only add default aliases to user defined networks. Other networks do
// not support aliases.
if container.NetworkMode(target).IsUserDefined() {
netAttachConfig.Aliases = append(netAttachConfig.Aliases, name)
}
nets = append(nets, netAttachConfig)
}
sort.Sort(byNetworkTarget(nets))
return nets, nil
}
// TODO: fix secrets API so that SecretAPIClient is not required here
func convertServiceSecrets(
client *client.Client,
namespace Namespace,
secrets []ServiceSecretConfig,
secretSpecs map[string]SecretConfig,
) ([]*swarm.SecretReference, error) {
refs := []*swarm.SecretReference{}
for _, secret := range secrets {
target := secret.Target
if target == "" {
target = secret.Source
}
secretSpec, exists := secretSpecs[secret.Source]
if !exists {
return nil, errors.Errorf("undefined secret %q", secret.Source)
}
source := namespace.Scope(secret.Source)
if secretSpec.External.External {
source = secretSpec.External.Name
}
uid := secret.UID
gid := secret.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := secret.Mode
if mode == nil {
mode = uint32Ptr(0444)
}
refs = append(refs, &swarm.SecretReference{
File: &swarm.SecretReferenceFileTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
SecretName: source,
})
}
secrs, err := ParseSecrets(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
return secrs, err
}
// TODO: fix configs API so that ConfigsAPIClient is not required here
func convertServiceConfigObjs(
client *client.Client,
namespace Namespace,
configs []ServiceConfigObjConfig,
configSpecs map[string]ConfigObjConfig,
) ([]*swarm.ConfigReference, error) {
refs := []*swarm.ConfigReference{}
for _, config := range configs {
target := config.Target
if target == "" {
target = config.Source
}
configSpec, exists := configSpecs[config.Source]
if !exists {
return nil, errors.Errorf("undefined config %q", config.Source)
}
source := namespace.Scope(config.Source)
if configSpec.External.External {
source = configSpec.External.Name
}
uid := config.UID
gid := config.GID
if uid == "" {
uid = "0"
}
if gid == "" {
gid = "0"
}
mode := config.Mode
if mode == nil {
mode = uint32Ptr(0444)
}
refs = append(refs, &swarm.ConfigReference{
File: &swarm.ConfigReferenceFileTarget{
Name: target,
UID: uid,
GID: gid,
Mode: os.FileMode(*mode),
},
ConfigName: source,
})
}
confs, err := ParseConfigs(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
return confs, err
}
func uint32Ptr(value uint32) *uint32 {
return &value
}
func convertExtraHosts(extraHosts map[string]string) []string {
hosts := []string{}
for host, ip := range extraHosts {
hosts = append(hosts, fmt.Sprintf("%s %s", ip, host))
}
return hosts
}
func convertHealthcheck(healthcheck *HealthCheckConfig) (*container.HealthConfig, error) {
if healthcheck == nil {
return nil, nil
}
var (
timeout, interval, startPeriod time.Duration
retries int
)
if healthcheck.Disable {
if len(healthcheck.Test) != 0 {
return nil, errors.Errorf("test and disable can't be set at the same time")
}
return &container.HealthConfig{
Test: []string{"NONE"},
}, nil
}
if healthcheck.Timeout != nil {
timeout = *healthcheck.Timeout
}
if healthcheck.Interval != nil {
interval = *healthcheck.Interval
}
if healthcheck.StartPeriod != nil {
startPeriod = *healthcheck.StartPeriod
}
if healthcheck.Retries != nil {
retries = int(*healthcheck.Retries)
}
return &container.HealthConfig{
Test: healthcheck.Test,
Timeout: timeout,
Interval: interval,
Retries: retries,
StartPeriod: startPeriod,
}, nil
}
func convertRestartPolicy(restart string, source *RestartPolicy) (*swarm.RestartPolicy, error) {
// TODO: log if restart is being ignored
if source == nil {
policy, err := ParseRestartPolicy(restart)
if err != nil {
return nil, err
}
switch {
case policy.IsNone():
return nil, nil
case policy.IsAlways(), policy.IsUnlessStopped():
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionAny,
}, nil
case policy.IsOnFailure():
attempts := uint64(policy.MaximumRetryCount)
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyConditionOnFailure,
MaxAttempts: &attempts,
}, nil
default:
return nil, errors.Errorf("unknown restart policy: %s", restart)
}
}
return &swarm.RestartPolicy{
Condition: swarm.RestartPolicyCondition(source.Condition),
Delay: source.Delay,
MaxAttempts: source.MaxAttempts,
Window: source.Window,
}, nil
}
func convertUpdateConfig(source *UpdateConfig) *swarm.UpdateConfig {
if source == nil {
return nil
}
parallel := uint64(1)
if source.Parallelism != nil {
parallel = *source.Parallelism
}
return &swarm.UpdateConfig{
Parallelism: parallel,
Delay: source.Delay,
FailureAction: source.FailureAction,
Monitor: source.Monitor,
MaxFailureRatio: source.MaxFailureRatio,
Order: source.Order,
}
}
func convertResources(source Resources) (*swarm.ResourceRequirements, error) {
resources := &swarm.ResourceRequirements{}
var err error
if source.Limits != nil {
var cpus int64
if source.Limits.NanoCPUs != "" {
cpus, err = ParseCPUs(source.Limits.NanoCPUs)
if err != nil {
return nil, err
}
}
resources.Limits = &swarm.Resources{
NanoCPUs: cpus,
MemoryBytes: int64(source.Limits.MemoryBytes),
}
}
if source.Reservations != nil {
var cpus int64
if source.Reservations.NanoCPUs != "" {
cpus, err = ParseCPUs(source.Reservations.NanoCPUs)
if err != nil {
return nil, err
}
}
resources.Reservations = &swarm.Resources{
NanoCPUs: cpus,
MemoryBytes: int64(source.Reservations.MemoryBytes),
}
}
return resources, nil
}
type byPublishedPort []swarm.PortConfig
func (a byPublishedPort) Len() int { return len(a) }
func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort }
func convertEndpointSpec(endpointMode string, source []ServicePortConfig) (*swarm.EndpointSpec, error) {
portConfigs := []swarm.PortConfig{}
for _, port := range source {
portConfig := swarm.PortConfig{
Protocol: swarm.PortConfigProtocol(port.Protocol),
TargetPort: port.Target,
PublishedPort: port.Published,
PublishMode: swarm.PortConfigPublishMode(port.Mode),
}
portConfigs = append(portConfigs, portConfig)
}
sort.Sort(byPublishedPort(portConfigs))
return &swarm.EndpointSpec{
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
Ports: portConfigs,
}, nil
}
func convertEnvironment(source map[string]*string) []string {
var output []string
for name, value := range source {
switch value {
case nil:
output = append(output, name)
default:
output = append(output, fmt.Sprintf("%s=%s", name, *value))
}
}
return output
}
func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
serviceMode := swarm.ServiceMode{}
switch mode {
case "global":
if replicas != nil {
return serviceMode, errors.Errorf("replicas can only be used with replicated mode")
}
serviceMode.Global = &swarm.GlobalService{}
case "replicated", "":
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
default:
return serviceMode, errors.Errorf("Unknown mode: %s", mode)
}
return serviceMode, nil
}
func convertDNSConfig(DNS []string, DNSSearch []string) (*swarm.DNSConfig, error) {
if DNS != nil || DNSSearch != nil {
return &swarm.DNSConfig{
Nameservers: DNS,
Search: DNSSearch,
}, nil
}
return nil, nil
}
func convertCredentialSpec(spec CredentialSpecConfig) (*swarm.CredentialSpec, error) {
if spec.File == "" && spec.Registry == "" {
return nil, nil
}
if spec.File != "" && spec.Registry != "" {
return nil, errors.New("Invalid credential spec - must provide one of `File` or `Registry`")
}
swarmCredSpec := swarm.CredentialSpec(spec)
return &swarmCredSpec, nil
}
// ParseSecrets retrieves the secrets with the requested names and fills
// secret IDs into the secret references.
func ParseSecrets(client *client.Client, requestedSecrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) {
if len(requestedSecrets) == 0 {
return []*swarm.SecretReference{}, nil
}
secretRefs := make(map[string]*swarm.SecretReference)
ctx := context.Background()
for _, secret := range requestedSecrets {
if _, exists := secretRefs[secret.File.Name]; exists {
return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName)
}
secretRef := new(swarm.SecretReference)
*secretRef = *secret
secretRefs[secret.File.Name] = secretRef
}
args := filters.NewArgs()
for _, s := range secretRefs {
args.Add("name", s.SecretName)
}
secrets, err := client.SecretList(ctx, types.SecretListOptions{
Filters: args,
})
if err != nil {
return nil, err
}
foundSecrets := make(map[string]string)
for _, secret := range secrets {
foundSecrets[secret.Spec.Annotations.Name] = secret.ID
}
addedSecrets := []*swarm.SecretReference{}
for _, ref := range secretRefs {
id, ok := foundSecrets[ref.SecretName]
if !ok {
return nil, errors.Errorf("secret not found: %s", ref.SecretName)
}
// set the id for the ref to properly assign in swarm
// since swarm needs the ID instead of the name
ref.SecretID = id
addedSecrets = append(addedSecrets, ref)
}
return addedSecrets, nil
}
// ParseConfigs retrieves the configs from the requested names and converts
// them to config references to use with the spec
func ParseConfigs(client client.ConfigAPIClient, requestedConfigs []*swarm.ConfigReference) ([]*swarm.ConfigReference, error) {
if len(requestedConfigs) == 0 {
return []*swarm.ConfigReference{}, nil
}
configRefs := make(map[string]*swarm.ConfigReference)
ctx := context.Background()
for _, config := range requestedConfigs {
if _, exists := configRefs[config.File.Name]; exists {
return nil, errors.Errorf("duplicate config target for %s not allowed", config.ConfigName)
}
configRef := new(swarm.ConfigReference)
*configRef = *config
configRefs[config.File.Name] = configRef
}
args := filters.NewArgs()
for _, s := range configRefs {
args.Add("name", s.ConfigName)
}
configs, err := client.ConfigList(ctx, types.ConfigListOptions{
Filters: args,
})
if err != nil {
return nil, err
}
foundConfigs := make(map[string]string)
for _, config := range configs {
foundConfigs[config.Spec.Annotations.Name] = config.ID
}
addedConfigs := []*swarm.ConfigReference{}
for _, ref := range configRefs {
id, ok := foundConfigs[ref.ConfigName]
if !ok {
return nil, errors.Errorf("config not found: %s", ref.ConfigName)
}
// set the id for the ref to properly assign in swarm
// since swarm needs the ID instead of the name
ref.ConfigID = id
addedConfigs = append(addedConfigs, ref)
}
return addedConfigs, nil
}

View File

@@ -0,0 +1,92 @@
package compose
import (
"errors"
"fmt"
"github.com/docker/docker/api/types/mount"
)
type volumes map[string]VolumeConfig
// Volumes from compose-file types to engine api types
func Volumes(serviceVolumes []ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
var mounts []mount.Mount
for _, volumeConfig := range serviceVolumes {
mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace)
if err != nil {
return nil, err
}
mounts = append(mounts, mount)
}
return mounts, nil
}
func convertVolumeToMount(
volume ServiceVolumeConfig,
stackVolumes volumes,
namespace Namespace,
) (mount.Mount, error) {
result := mount.Mount{
Type: mount.Type(volume.Type),
Source: volume.Source,
Target: volume.Target,
ReadOnly: volume.ReadOnly,
Consistency: mount.Consistency(volume.Consistency),
}
// Anonymous volumes
if volume.Source == "" {
return result, nil
}
if volume.Type == "volume" && volume.Bind != nil {
return result, errors.New("bind options are incompatible with type volume")
}
if volume.Type == "bind" && volume.Volume != nil {
return result, errors.New("volume options are incompatible with type bind")
}
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
// Binds volumes
if volume.Type == "bind" {
return result, nil
}
stackVolume, exists := stackVolumes[volume.Source]
if !exists {
return result, fmt.Errorf("undefined volume %q", volume.Source)
}
result.Source = namespace.Scope(volume.Source)
result.VolumeOptions = &mount.VolumeOptions{}
if volume.Volume != nil {
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
}
// External named volumes
if stackVolume.External.External {
result.Source = stackVolume.External.Name
return result, nil
}
if stackVolume.Name != "" {
result.Source = stackVolume.Name
}
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
result.VolumeOptions.DriverConfig = &mount.Driver{
Name: stackVolume.Driver,
Options: stackVolume.DriverOpts,
}
}
// Named volumes
return result, nil
}

View File

@@ -0,0 +1,81 @@
package compose
import (
"bufio"
"bytes"
"fmt"
"os"
"strings"
"unicode"
"unicode/utf8"
)
// ParseEnvFile reads a file with environment variables enumerated by lines
//
// ``Environment variable names used by the utilities in the Shell and
// Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase
// letters, digits, and the '_' (underscore) from the characters defined in
// Portable Character Set and do not begin with a digit. *But*, other
// characters may be permitted by an implementation; applications shall
// tolerate the presence of such names.''
// -- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html
//
// As of #16585, it's up to application inside docker to validate or not
// environment variables, that's why we just strip leading whitespace and
// nothing more.
func ParseEnvFile(filename string) ([]string, error) {
fh, err := os.Open(filename)
if err != nil {
return []string{}, err
}
defer fh.Close()
lines := []string{}
scanner := bufio.NewScanner(fh)
currentLine := 0
utf8bom := []byte{0xEF, 0xBB, 0xBF}
for scanner.Scan() {
scannedBytes := scanner.Bytes()
if !utf8.Valid(scannedBytes) {
return []string{}, fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v", filename, currentLine+1, scannedBytes)
}
// We trim UTF8 BOM
if currentLine == 0 {
scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom)
}
// trim the line from all leading whitespace first
line := strings.TrimLeftFunc(string(scannedBytes), unicode.IsSpace)
currentLine++
// line is not empty, and not starting with '#'
if len(line) > 0 && !strings.HasPrefix(line, "#") {
data := strings.SplitN(line, "=", 2)
// trim the front of a variable, but nothing else
variable := strings.TrimLeft(data[0], whiteSpaces)
if strings.ContainsAny(variable, whiteSpaces) {
return []string{}, ErrBadEnvVariable{fmt.Sprintf("variable '%s' has white spaces", variable)}
}
if len(data) > 1 {
// pass the value through, no trimming
lines = append(lines, fmt.Sprintf("%s=%s", variable, data[1]))
} else {
// if only a pass-through variable is given, clean it up.
lines = append(lines, fmt.Sprintf("%s=%s", strings.TrimSpace(line), os.Getenv(line)))
}
}
}
return lines, scanner.Err()
}
var whiteSpaces = " \t"
// ErrBadEnvVariable typed error for bad environment variable
type ErrBadEnvVariable struct {
msg string
}
func (e ErrBadEnvVariable) Error() string {
return fmt.Sprintf("poorly formatted environment: %s", e.msg)
}

View File

@@ -0,0 +1,93 @@
package compose
import "github.com/pkg/errors"
// Interpolate replaces variables in a string with the values from a mapping
func Interpolate(config map[string]interface{}, section string, mapping Mapping) (map[string]interface{}, error) {
out := map[string]interface{}{}
for name, item := range config {
if item == nil {
out[name] = nil
continue
}
mapItem, ok := item.(map[string]interface{})
if !ok {
return nil, errors.Errorf("Invalid type for %s : %T instead of %T", name, item, out)
}
interpolatedItem, err := interpolateSectionItem(name, mapItem, section, mapping)
if err != nil {
return nil, err
}
out[name] = interpolatedItem
}
return out, nil
}
func interpolateSectionItem(
name string,
item map[string]interface{},
section string,
mapping Mapping,
) (map[string]interface{}, error) {
out := map[string]interface{}{}
for key, value := range item {
interpolatedValue, err := recursiveInterpolate(value, mapping)
switch err := err.(type) {
case nil:
case *InvalidTemplateError:
return nil, errors.Errorf(
"Invalid interpolation format for %#v option in %s %#v: %#v. You may need to escape any $ with another $.",
key, section, name, err.Template,
)
default:
return nil, errors.Wrapf(err, "error while interpolating %s in %s %s", key, section, name)
}
out[key] = interpolatedValue
}
return out, nil
}
func recursiveInterpolate(
value interface{},
mapping Mapping,
) (interface{}, error) {
switch value := value.(type) {
case string:
return Substitute(value, mapping)
case map[string]interface{}:
out := map[string]interface{}{}
for key, elem := range value {
interpolatedElem, err := recursiveInterpolate(elem, mapping)
if err != nil {
return nil, err
}
out[key] = interpolatedElem
}
return out, nil
case []interface{}:
out := make([]interface{}, len(value))
for i, elem := range value {
interpolatedElem, err := recursiveInterpolate(elem, mapping)
if err != nil {
return nil, err
}
out[i] = interpolatedElem
}
return out, nil
default:
return value, nil
}
}

View File

@@ -0,0 +1,723 @@
package compose
import (
"errors"
"fmt"
"path"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/cuigh/auxo/log"
"github.com/docker/go-connections/nat"
units "github.com/docker/go-units"
shellwords "github.com/mattn/go-shellwords"
"github.com/mitchellh/mapstructure"
"gopkg.in/yaml.v2"
)
// ParseYAML reads the bytes from a file, parses the bytes into a mapping
// structure, and returns it.
func ParseYAML(source []byte) (map[string]interface{}, error) {
var cfg interface{}
if err := yaml.Unmarshal(source, &cfg); err != nil {
return nil, err
}
cfgMap, ok := cfg.(map[interface{}]interface{})
if !ok {
return nil, errors.New("Top-level object must be a mapping")
}
converted, err := convertToStringKeysRecursive(cfgMap, "")
if err != nil {
return nil, err
}
return converted.(map[string]interface{}), nil
}
// Load reads a ConfigDetails and returns a fully loaded configuration
func Load(configDetails ConfigDetails) (*Config, error) {
if len(configDetails.ConfigFiles) < 1 {
return nil, errors.New("No files specified")
}
if len(configDetails.ConfigFiles) > 1 {
return nil, errors.New("Multiple files are not yet supported")
}
configDict := getConfigDict(configDetails)
if services, ok := configDict["services"]; ok {
if servicesDict, ok := services.(map[string]interface{}); ok {
forbidden := getProperties(servicesDict, ForbiddenProperties)
if len(forbidden) > 0 {
return nil, &ForbiddenPropertiesError{Properties: forbidden}
}
}
}
// todo: Add validation
//if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
// return nil, err
//}
cfg := Config{}
config, err := interpolateConfig(configDict, configDetails.LookupEnv)
if err != nil {
return nil, err
}
cfg.Services, err = LoadServices(config["services"], configDetails.WorkingDir, configDetails.LookupEnv)
if err != nil {
return nil, err
}
cfg.Networks, err = LoadNetworks(config["networks"])
if err != nil {
return nil, err
}
cfg.Volumes, err = LoadVolumes(config["volumes"])
if err != nil {
return nil, err
}
cfg.Secrets, err = LoadSecrets(config["secrets"], configDetails.WorkingDir)
if err != nil {
return nil, err
}
cfg.Configs, err = LoadConfigObjs(config["configs"], configDetails.WorkingDir)
return &cfg, err
}
func interpolateConfig(configDict map[string]interface{}, lookupEnv Mapping) (map[string]map[string]interface{}, error) {
config := make(map[string]map[string]interface{})
for _, key := range []string{"services", "networks", "volumes", "secrets", "configs"} {
section, ok := configDict[key]
if !ok {
config[key] = make(map[string]interface{})
continue
}
var err error
config[key], err = Interpolate(section.(map[string]interface{}), key, lookupEnv)
if err != nil {
return nil, err
}
}
return config, nil
}
// GetUnsupportedProperties returns the list of any unsupported properties that are
// used in the Compose files.
func GetUnsupportedProperties(configDetails ConfigDetails) []string {
unsupported := map[string]bool{}
for _, service := range getServices(getConfigDict(configDetails)) {
serviceDict := service.(map[string]interface{})
for _, property := range UnsupportedProperties {
if _, isSet := serviceDict[property]; isSet {
unsupported[property] = true
}
}
}
return sortedKeys(unsupported)
}
func sortedKeys(set map[string]bool) []string {
var keys []string
for key := range set {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
// GetDeprecatedProperties returns the list of any deprecated properties that
// are used in the compose files.
func GetDeprecatedProperties(configDetails ConfigDetails) map[string]string {
return getProperties(getServices(getConfigDict(configDetails)), DeprecatedProperties)
}
func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
output := map[string]string{}
for _, service := range services {
if serviceDict, ok := service.(map[string]interface{}); ok {
for property, description := range propertyMap {
if _, isSet := serviceDict[property]; isSet {
output[property] = description
}
}
}
}
return output
}
// ForbiddenPropertiesError is returned when there are properties in the Compose
// file that are forbidden.
type ForbiddenPropertiesError struct {
Properties map[string]string
}
func (e *ForbiddenPropertiesError) Error() string {
return "Configuration contains forbidden properties"
}
// TODO: resolve multiple files into a single config
func getConfigDict(configDetails ConfigDetails) map[string]interface{} {
return configDetails.ConfigFiles[0].Config
}
func getServices(configDict map[string]interface{}) map[string]interface{} {
if services, ok := configDict["services"]; ok {
if servicesDict, ok := services.(map[string]interface{}); ok {
return servicesDict
}
}
return map[string]interface{}{}
}
func transform(source map[string]interface{}, target interface{}) error {
data := mapstructure.Metadata{}
config := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
createTransformHook(),
mapstructure.StringToTimeDurationHookFunc()),
Result: target,
Metadata: &data,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return err
}
return decoder.Decode(source)
}
func createTransformHook() mapstructure.DecodeHookFuncType {
transforms := map[reflect.Type]func(interface{}) (interface{}, error){
reflect.TypeOf(External{}): transformExternal,
reflect.TypeOf(HealthCheckTest{}): transformHealthCheckTest,
reflect.TypeOf(ShellCommand{}): transformShellCommand,
reflect.TypeOf(StringList{}): transformStringList,
reflect.TypeOf(map[string]string{}): transformMapStringString,
reflect.TypeOf(UlimitsConfig{}): transformUlimits,
reflect.TypeOf(UnitBytes(0)): transformSize,
reflect.TypeOf([]ServicePortConfig{}): transformServicePort,
reflect.TypeOf(ServiceSecretConfig{}): transformStringSourceMap,
reflect.TypeOf(ServiceConfigObjConfig{}): transformStringSourceMap,
reflect.TypeOf(StringOrNumberList{}): transformStringOrNumberList,
reflect.TypeOf(map[string]*ServiceNetworkConfig{}): transformServiceNetworkMap,
reflect.TypeOf(MappingWithEquals{}): transformMappingOrListFunc("=", true),
reflect.TypeOf(Labels{}): transformMappingOrListFunc("=", false),
reflect.TypeOf(MappingWithColon{}): transformMappingOrListFunc(":", false),
reflect.TypeOf(ServiceVolumeConfig{}): transformServiceVolumeConfig,
reflect.TypeOf(BuildConfig{}): transformBuildConfig,
}
return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) {
transform, ok := transforms[target]
if !ok {
return data, nil
}
return transform(data)
}
}
// keys needs to be converted to strings for jsonschema
func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
if mapping, ok := value.(map[interface{}]interface{}); ok {
dict := make(map[string]interface{})
for key, entry := range mapping {
str, ok := key.(string)
if !ok {
return nil, formatInvalidKeyError(keyPrefix, key)
}
var newKeyPrefix string
if keyPrefix == "" {
newKeyPrefix = str
} else {
newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
}
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
if err != nil {
return nil, err
}
dict[str] = convertedEntry
}
return dict, nil
}
if list, ok := value.([]interface{}); ok {
var convertedList []interface{}
for index, entry := range list {
newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
if err != nil {
return nil, err
}
convertedList = append(convertedList, convertedEntry)
}
return convertedList, nil
}
return value, nil
}
func formatInvalidKeyError(keyPrefix string, key interface{}) error {
var location string
if keyPrefix == "" {
location = "at top level"
} else {
location = fmt.Sprintf("in %s", keyPrefix)
}
return fmt.Errorf("Non-string key %s: %#v", location, key)
}
// LoadServices produces a ServiceConfig map from a compose file Dict
// the servicesDict is not validated if directly used. Use Load() to enable validation
func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv Mapping) ([]ServiceConfig, error) {
var services []ServiceConfig
for name, serviceDef := range servicesDict {
serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
if err != nil {
return nil, err
}
services = append(services, *serviceConfig)
}
return services, nil
}
// LoadService produces a single ServiceConfig from a compose file Dict
// the serviceDict is not validated if directly used. Use Load() to enable validation
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv Mapping) (*ServiceConfig, error) {
serviceConfig := &ServiceConfig{}
if err := transform(serviceDict, serviceConfig); err != nil {
return nil, err
}
serviceConfig.Name = name
if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
return nil, err
}
resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv)
return serviceConfig, nil
}
func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv Mapping) {
for k, v := range vars {
interpolatedV, ok := lookupEnv(k)
if (v == nil || *v == "") && ok {
// lookupEnv is prioritized over vars
environment[k] = &interpolatedV
} else {
environment[k] = v
}
}
}
func resolveEnvironment(serviceConfig *ServiceConfig, workingDir string, lookupEnv Mapping) error {
environment := make(map[string]*string)
if len(serviceConfig.EnvFile) > 0 {
var envVars []string
for _, file := range serviceConfig.EnvFile {
filePath := absPath(workingDir, file)
fileVars, err := ParseEnvFile(filePath)
if err != nil {
return err
}
envVars = append(envVars, fileVars...)
}
updateEnvironment(environment,
ConvertKVStringsToMapWithNil(envVars), lookupEnv)
}
updateEnvironment(environment, serviceConfig.Environment, lookupEnv)
serviceConfig.Environment = environment
return nil
}
func resolveVolumePaths(volumes []ServiceVolumeConfig, workingDir string, lookupEnv Mapping) {
for i, volume := range volumes {
if volume.Type != "bind" {
continue
}
filePath := expandUser(volume.Source, lookupEnv)
// Check for a Unix absolute path first, to handle a Windows client
// with a Unix daemon. This handles a Windows client connecting to a
// Unix daemon. Note that this is not required for Docker for Windows
// when specifying a local Windows path, because Docker for Windows
// translates the Windows path into a valid path within the VM.
if !path.IsAbs(filePath) {
filePath = absPath(workingDir, filePath)
}
volume.Source = filePath
volumes[i] = volume
}
}
// TODO: make this more robust
func expandUser(path string, lookupEnv Mapping) string {
if strings.HasPrefix(path, "~") {
home, ok := lookupEnv("HOME")
if !ok {
log.Get("compose").Warn("cannot expand '~', because the environment lacks HOME")
return path
}
return strings.Replace(path, "~", home, 1)
}
return path
}
func transformUlimits(data interface{}) (interface{}, error) {
switch value := data.(type) {
case int:
return UlimitsConfig{Single: value}, nil
case map[string]interface{}:
ulimit := UlimitsConfig{}
ulimit.Soft = value["soft"].(int)
ulimit.Hard = value["hard"].(int)
return ulimit, nil
default:
return data, fmt.Errorf("invalid type %T for ulimits", value)
}
}
// LoadNetworks produces a NetworkConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadNetworks(source map[string]interface{}) (map[string]NetworkConfig, error) {
networks := make(map[string]NetworkConfig)
err := transform(source, &networks)
if err != nil {
return networks, err
}
for name, network := range networks {
if network.External.External && network.External.Name == "" {
network.External.Name = name
networks[name] = network
}
}
return networks, nil
}
func externalVolumeError(volume, key string) error {
return fmt.Errorf(
"conflicting parameters \"external\" and %q specified for volume %q",
key, volume)
}
// LoadVolumes produces a VolumeConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadVolumes(source map[string]interface{}) (map[string]VolumeConfig, error) {
volumes := make(map[string]VolumeConfig)
err := transform(source, &volumes)
if err != nil {
return volumes, err
}
for name, volume := range volumes {
if volume.External.External {
if volume.Driver != "" {
return nil, externalVolumeError(name, "driver")
}
if len(volume.DriverOpts) > 0 {
return nil, externalVolumeError(name, "driver_opts")
}
if len(volume.Labels) > 0 {
return nil, externalVolumeError(name, "labels")
}
if volume.External.Name == "" {
volume.External.Name = name
volumes[name] = volume
} else {
log.Get("compose").Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
if volume.Name != "" {
return nil, fmt.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
}
}
}
}
return volumes, nil
}
// LoadSecrets produces a SecretConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadSecrets(source map[string]interface{}, workingDir string) (map[string]SecretConfig, error) {
secrets := make(map[string]SecretConfig)
if err := transform(source, &secrets); err != nil {
return secrets, err
}
for name, secret := range secrets {
if secret.External.External && secret.External.Name == "" {
secret.External.Name = name
secrets[name] = secret
}
if secret.File != "" {
secret.File = absPath(workingDir, secret.File)
}
}
return secrets, nil
}
// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadConfigObjs(source map[string]interface{}, workingDir string) (map[string]ConfigObjConfig, error) {
configs := make(map[string]ConfigObjConfig)
if err := transform(source, &configs); err != nil {
return configs, err
}
for name, config := range configs {
if config.External.External && config.External.Name == "" {
config.External.Name = name
configs[name] = config
}
if config.File != "" {
config.File = absPath(workingDir, config.File)
}
}
return configs, nil
}
func absPath(workingDir string, filePath string) string {
if filepath.IsAbs(filePath) {
return filePath
}
return filepath.Join(workingDir, filePath)
}
func transformMapStringString(data interface{}) (interface{}, error) {
switch value := data.(type) {
case map[string]interface{}:
return toMapStringString(value, false), nil
case map[string]string:
return value, nil
default:
return data, fmt.Errorf("invalid type %T for map[string]string", value)
}
}
func transformExternal(data interface{}) (interface{}, error) {
switch value := data.(type) {
case bool:
return map[string]interface{}{"external": value}, nil
case map[string]interface{}:
return map[string]interface{}{"external": true, "name": value["name"]}, nil
default:
return data, fmt.Errorf("invalid type %T for external", value)
}
}
func transformServicePort(data interface{}) (interface{}, error) {
switch entries := data.(type) {
case []interface{}:
// We process the list instead of individual items here.
// The reason is that one entry might be mapped to multiple ServicePortConfig.
// Therefore we take an input of a list and return an output of a list.
ports := []interface{}{}
for _, entry := range entries {
switch value := entry.(type) {
case int:
v, err := toServicePortConfigs(fmt.Sprint(value))
if err != nil {
return data, err
}
ports = append(ports, v...)
case string:
v, err := toServicePortConfigs(value)
if err != nil {
return data, err
}
ports = append(ports, v...)
case map[string]interface{}:
ports = append(ports, value)
default:
return data, fmt.Errorf("invalid type %T for port", value)
}
}
return ports, nil
default:
return data, fmt.Errorf("invalid type %T for port", entries)
}
}
func transformStringSourceMap(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:
return map[string]interface{}{"source": value}, nil
case map[string]interface{}:
return data, nil
default:
return data, fmt.Errorf("invalid type %T for secret", value)
}
}
func transformBuildConfig(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:
return map[string]interface{}{"context": value}, nil
case map[string]interface{}:
return data, nil
default:
return data, fmt.Errorf("invalid type %T for service build", value)
}
}
func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:
return ParseVolume(value)
case map[string]interface{}:
return data, nil
default:
return data, fmt.Errorf("invalid type %T for service volume", value)
}
}
func transformServiceNetworkMap(value interface{}) (interface{}, error) {
if list, ok := value.([]interface{}); ok {
mapValue := map[interface{}]interface{}{}
for _, name := range list {
mapValue[name] = nil
}
return mapValue, nil
}
return value, nil
}
func transformStringOrNumberList(value interface{}) (interface{}, error) {
list := value.([]interface{})
result := make([]string, len(list))
for i, item := range list {
result[i] = fmt.Sprint(item)
}
return result, nil
}
func transformStringList(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:
return []string{value}, nil
case []interface{}:
return value, nil
default:
return data, fmt.Errorf("invalid type %T for string list", value)
}
}
func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
return func(data interface{}) (interface{}, error) {
return transformMappingOrList(data, sep, allowNil), nil
}
}
func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
switch value := mappingOrList.(type) {
case map[string]interface{}:
return toMapStringString(value, allowNil)
case []interface{}:
result := make(map[string]interface{})
for _, value := range value {
parts := strings.SplitN(value.(string), sep, 2)
key := parts[0]
switch {
case len(parts) == 1 && allowNil:
result[key] = nil
case len(parts) == 1 && !allowNil:
result[key] = ""
default:
result[key] = parts[1]
}
}
return result
}
panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
}
func transformShellCommand(value interface{}) (interface{}, error) {
if str, ok := value.(string); ok {
return shellwords.Parse(str)
}
return value, nil
}
func transformHealthCheckTest(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:
return append([]string{"CMD-SHELL"}, value), nil
case []interface{}:
return value, nil
default:
return value, fmt.Errorf("invalid type %T for healthcheck.test", value)
}
}
func transformSize(value interface{}) (interface{}, error) {
switch value := value.(type) {
case int:
return int64(value), nil
case string:
return units.RAMInBytes(value)
}
panic(fmt.Errorf("invalid type for size %T", value))
}
func toServicePortConfigs(value string) ([]interface{}, error) {
var portConfigs []interface{}
ports, portBindings, err := nat.ParsePortSpecs([]string{value})
if err != nil {
return nil, err
}
// We need to sort the key of the ports to make sure it is consistent
keys := []string{}
for port := range ports {
keys = append(keys, string(port))
}
sort.Strings(keys)
for _, key := range keys {
// Reuse ConvertPortToPortConfig so that it is consistent
portConfig, err := ConvertPortToPortConfig(nat.Port(key), portBindings)
if err != nil {
return nil, err
}
for _, p := range portConfig {
portConfigs = append(portConfigs, ServicePortConfig{
Protocol: string(p.Protocol),
Target: p.TargetPort,
Published: p.PublishedPort,
Mode: string(p.PublishMode),
})
}
}
return portConfigs, nil
}
func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} {
output := make(map[string]interface{})
for key, value := range value {
output[key] = toString(value, allowNil)
}
return output
}
func toString(value interface{}, allowNil bool) interface{} {
switch {
case value != nil:
return fmt.Sprint(value)
case allowNil:
return nil
default:
return ""
}
}

View File

@@ -0,0 +1,69 @@
package compose
import (
"fmt"
"math/big"
"strconv"
"strings"
"github.com/docker/docker/api/types/container"
)
// ConvertKVStringsToMapWithNil converts ["key=value"] to {"key":"value"}
// but set unset keys to nil - meaning the ones with no "=" in them.
// We use this in cases where we need to distinguish between
// FOO= and FOO
// where the latter case just means FOO was mentioned but not given a value
func ConvertKVStringsToMapWithNil(values []string) map[string]*string {
result := make(map[string]*string, len(values))
for _, value := range values {
kv := strings.SplitN(value, "=", 2)
if len(kv) == 1 {
result[kv[0]] = nil
} else {
result[kv[0]] = &kv[1]
}
}
return result
}
// ParseRestartPolicy returns the parsed policy or an error indicating what is incorrect
func ParseRestartPolicy(policy string) (container.RestartPolicy, error) {
p := container.RestartPolicy{}
if policy == "" {
return p, nil
}
parts := strings.Split(policy, ":")
if len(parts) > 2 {
return p, fmt.Errorf("invalid restart policy format")
}
if len(parts) == 2 {
count, err := strconv.Atoi(parts[1])
if err != nil {
return p, fmt.Errorf("maximum retry count must be an integer")
}
p.MaximumRetryCount = count
}
p.Name = parts[0]
return p, nil
}
// ParseCPUs takes a string ratio and returns an integer value of nano cpus
func ParseCPUs(value string) (int64, error) {
cpu, ok := new(big.Rat).SetString(value)
if !ok {
return 0, fmt.Errorf("failed to parse %v as a rational number", value)
}
nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
if !nano.IsInt() {
return 0, fmt.Errorf("value is too precise")
}
return nano.Num().Int64(), nil
}

View File

@@ -0,0 +1,86 @@
package compose
import (
"fmt"
"os"
"sort"
"strings"
)
func Parse(name, content string) (*Config, error) {
//absPath, err := filepath.Abs(composefile)
//if err != nil {
// return details, err
//}
//details.WorkingDir = filepath.Dir(absPath)
configFile, err := getConfigFile(name, content)
if err != nil {
return nil, err
}
env, err := buildEnvironment(os.Environ())
if err != nil {
return nil, err
}
details := ConfigDetails{
ConfigFiles: []ConfigFile{*configFile},
Environment: env,
}
cfg, err := Load(details)
if err != nil {
if fpe, ok := err.(*ForbiddenPropertiesError); ok {
err = fmt.Errorf("Compose file contains unsupported options:\n\n%s\n",
propertyWarnings(fpe.Properties))
}
}
return cfg, err
}
func propertyWarnings(properties map[string]string) string {
var msgs []string
for name, description := range properties {
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
}
sort.Strings(msgs)
return strings.Join(msgs, "\n\n")
}
func getConfigFile(name, content string) (*ConfigFile, error) {
config, err := ParseYAML([]byte(content))
if err != nil {
return nil, err
}
return &ConfigFile{
Filename: name,
Config: config,
}, nil
}
func buildEnvironment(env []string) (map[string]string, error) {
result := make(map[string]string, len(env))
for _, s := range env {
// if value is empty, s is like "K=", not "K".
if !strings.Contains(s, "=") {
return result, fmt.Errorf("unexpected environment %q", s)
}
kv := strings.SplitN(s, "=", 2)
result[kv[0]] = kv[1]
}
return result, nil
}
func GetServicesDeclaredNetworks(serviceConfigs []ServiceConfig) map[string]struct{} {
serviceNetworks := map[string]struct{}{}
for _, serviceConfig := range serviceConfigs {
if len(serviceConfig.Networks) == 0 {
serviceNetworks["default"] = struct{}{}
continue
}
for network := range serviceConfig.Networks {
serviceNetworks[network] = struct{}{}
}
}
return serviceNetworks
}

163
biz/docker/compose/port.go Normal file
View File

@@ -0,0 +1,163 @@
package compose
import (
"encoding/csv"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/go-connections/nat"
)
const (
portOptTargetPort = "target"
portOptPublishedPort = "published"
portOptProtocol = "protocol"
portOptMode = "mode"
)
// PortOpt represents a port config in swarm mode.
type PortOpt struct {
ports []swarm.PortConfig
}
// Set a new port value
// nolint: gocyclo
func (p *PortOpt) Set(value string) error {
longSyntax, err := regexp.MatchString(`\w+=\w+(,\w+=\w+)*`, value)
if err != nil {
return err
}
if longSyntax {
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return err
}
pConfig := swarm.PortConfig{}
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid field %s", field)
}
key := strings.ToLower(parts[0])
value := strings.ToLower(parts[1])
switch key {
case portOptProtocol:
if value != string(swarm.PortConfigProtocolTCP) && value != string(swarm.PortConfigProtocolUDP) {
return fmt.Errorf("invalid protocol value %s", value)
}
pConfig.Protocol = swarm.PortConfigProtocol(value)
case portOptMode:
if value != string(swarm.PortConfigPublishModeIngress) && value != string(swarm.PortConfigPublishModeHost) {
return fmt.Errorf("invalid publish mode value %s", value)
}
pConfig.PublishMode = swarm.PortConfigPublishMode(value)
case portOptTargetPort:
tPort, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return err
}
pConfig.TargetPort = uint32(tPort)
case portOptPublishedPort:
pPort, err := strconv.ParseUint(value, 10, 16)
if err != nil {
return err
}
pConfig.PublishedPort = uint32(pPort)
default:
return fmt.Errorf("invalid field key %s", key)
}
}
if pConfig.TargetPort == 0 {
return fmt.Errorf("missing mandatory field %q", portOptTargetPort)
}
if pConfig.PublishMode == "" {
pConfig.PublishMode = swarm.PortConfigPublishModeIngress
}
if pConfig.Protocol == "" {
pConfig.Protocol = swarm.PortConfigProtocolTCP
}
p.ports = append(p.ports, pConfig)
} else {
// short syntax
portConfigs := []swarm.PortConfig{}
ports, portBindingMap, err := nat.ParsePortSpecs([]string{value})
if err != nil {
return err
}
for _, portBindings := range portBindingMap {
for _, portBinding := range portBindings {
if portBinding.HostIP != "" {
return fmt.Errorf("hostip is not supported")
}
}
}
for port := range ports {
portConfig, err := ConvertPortToPortConfig(port, portBindingMap)
if err != nil {
return err
}
portConfigs = append(portConfigs, portConfig...)
}
p.ports = append(p.ports, portConfigs...)
}
return nil
}
// Type returns the type of this option
func (p *PortOpt) Type() string {
return "port"
}
// String returns a string repr of this option
func (p *PortOpt) String() string {
ports := []string{}
for _, port := range p.ports {
repr := fmt.Sprintf("%v:%v/%s/%s", port.PublishedPort, port.TargetPort, port.Protocol, port.PublishMode)
ports = append(ports, repr)
}
return strings.Join(ports, ", ")
}
// Value returns the ports
func (p *PortOpt) Value() []swarm.PortConfig {
return p.ports
}
// ConvertPortToPortConfig converts ports to the swarm type
func ConvertPortToPortConfig(
port nat.Port,
portBindings map[nat.Port][]nat.PortBinding,
) ([]swarm.PortConfig, error) {
ports := []swarm.PortConfig{}
for _, binding := range portBindings[port] {
hostPort, err := strconv.ParseUint(binding.HostPort, 10, 16)
if err != nil && binding.HostPort != "" {
return nil, fmt.Errorf("invalid hostport binding (%s) for port (%s)", binding.HostPort, port.Port())
}
ports = append(ports, swarm.PortConfig{
//TODO Name: ?
Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
TargetPort: uint32(port.Int()),
PublishedPort: uint32(hostPort),
PublishMode: swarm.PortConfigPublishModeIngress,
})
}
return ports, nil
}

View File

@@ -0,0 +1,101 @@
package compose
import (
"fmt"
"regexp"
"strings"
)
var delimiter = "\\$"
var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?"
var patternString = fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
delimiter, delimiter, substitution, substitution,
)
var pattern = regexp.MustCompile(patternString)
// InvalidTemplateError is returned when a variable template is not in a valid
// format
type InvalidTemplateError struct {
Template string
}
func (e InvalidTemplateError) Error() string {
return fmt.Sprintf("Invalid template: %#v", e.Template)
}
// Mapping is a user-supplied function which maps from variable names to values.
// Returns the value as a string and a bool indicating whether
// the value is present, to distinguish between an empty string
// and the absence of a value.
type Mapping func(string) (string, bool)
// Substitute variables in the string with their values
func Substitute(template string, mapping Mapping) (string, error) {
var err error
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
matches := pattern.FindStringSubmatch(substring)
groups := make(map[string]string)
for i, name := range pattern.SubexpNames() {
if i != 0 {
groups[name] = matches[i]
}
}
substitution := groups["named"]
if substitution == "" {
substitution = groups["braced"]
}
if substitution != "" {
// Soft default (fall back if unset or empty)
if strings.Contains(substitution, ":-") {
name, defaultValue := partition(substitution, ":-")
value, ok := mapping(name)
if !ok || value == "" {
return defaultValue
}
return value
}
// Hard default (fall back if-and-only-if empty)
if strings.Contains(substitution, "-") {
name, defaultValue := partition(substitution, "-")
value, ok := mapping(name)
if !ok {
return defaultValue
}
return value
}
// No default (fall back to empty string)
value, ok := mapping(substitution)
if !ok {
return ""
}
return value
}
if escaped := groups["escaped"]; escaped != "" {
return escaped
}
err = &InvalidTemplateError{Template: template}
return ""
})
return result, err
}
// Split the string at the first occurrence of sep, and return the part before the separator,
// and the part after the separator.
//
// If the separator is not found, return the string itself, followed by an empty string.
func partition(s, sep string) (string, string) {
if strings.Contains(s, sep) {
parts := strings.SplitN(s, sep, 2)
return parts[0], parts[1]
}
return s, ""
}

353
biz/docker/compose/types.go Normal file
View File

@@ -0,0 +1,353 @@
package compose
import (
"time"
)
// UnsupportedProperties not yet supported by this implementation of the compose file
var UnsupportedProperties = []string{
"build",
"cap_add",
"cap_drop",
"cgroup_parent",
"devices",
"domainname",
"external_links",
"ipc",
"links",
"mac_address",
"network_mode",
"privileged",
"restart",
"security_opt",
"shm_size",
"sysctls",
"tmpfs",
"ulimits",
"userns_mode",
}
// DeprecatedProperties that were removed from the v3 format, but their
// use should not impact the behaviour of the application.
var DeprecatedProperties = map[string]string{
"container_name": "Setting the container name is not supported.",
"expose": "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.",
}
// ForbiddenProperties that are not supported in this implementation of the
// compose file.
var ForbiddenProperties = map[string]string{
"extends": "Support for `extends` is not implemented yet.",
"volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.",
"volumes_from": "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.",
"cpu_quota": "Set resource limits using deploy.resources",
"cpu_shares": "Set resource limits using deploy.resources",
"cpuset": "Set resource limits using deploy.resources",
"mem_limit": "Set resource limits using deploy.resources",
"memswap_limit": "Set resource limits using deploy.resources",
}
// ConfigFile is a filename and the contents of the file as a Dict
type ConfigFile struct {
Filename string
Config map[string]interface{}
}
// ConfigDetails are the details about a group of ConfigFiles
type ConfigDetails struct {
WorkingDir string
ConfigFiles []ConfigFile
Environment map[string]string
}
// LookupEnv provides a lookup function for environment variables
func (cd ConfigDetails) LookupEnv(key string) (string, bool) {
v, ok := cd.Environment[key]
return v, ok
}
// Config is a full compose file configuration
type Config struct {
Services []ServiceConfig
Networks map[string]NetworkConfig
Volumes map[string]VolumeConfig
Secrets map[string]SecretConfig
Configs map[string]ConfigObjConfig
}
// ServiceConfig is the configuration of one service
type ServiceConfig struct {
Name string
Build BuildConfig
CapAdd []string `mapstructure:"cap_add"`
CapDrop []string `mapstructure:"cap_drop"`
CgroupParent string `mapstructure:"cgroup_parent"`
Command ShellCommand
Configs []ServiceConfigObjConfig
ContainerName string `mapstructure:"container_name"`
CredentialSpec CredentialSpecConfig `mapstructure:"credential_spec"`
DependsOn []string `mapstructure:"depends_on"`
Deploy DeployConfig
Devices []string
DNS StringList
DNSSearch StringList `mapstructure:"dns_search"`
DomainName string `mapstructure:"domainname"`
Entrypoint ShellCommand
Environment MappingWithEquals
EnvFile StringList `mapstructure:"env_file"`
Expose StringOrNumberList
ExternalLinks []string `mapstructure:"external_links"`
ExtraHosts MappingWithColon `mapstructure:"extra_hosts"`
Hostname string
HealthCheck *HealthCheckConfig
Image string
Ipc string
Labels Labels
Links []string
Logging *LoggingConfig
MacAddress string `mapstructure:"mac_address"`
NetworkMode string `mapstructure:"network_mode"`
Networks map[string]*ServiceNetworkConfig
Pid string
Ports []ServicePortConfig
Privileged bool
ReadOnly bool `mapstructure:"read_only"`
Restart string
Secrets []ServiceSecretConfig
SecurityOpt []string `mapstructure:"security_opt"`
StdinOpen bool `mapstructure:"stdin_open"`
StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"`
StopSignal string `mapstructure:"stop_signal"`
Tmpfs StringList
Tty bool `mapstructure:"tty"`
Ulimits map[string]*UlimitsConfig
User string
Volumes []ServiceVolumeConfig
WorkingDir string `mapstructure:"working_dir"`
}
// BuildConfig is a type for build
// using the same format at libcompose: https://github.com/docker/libcompose/blob/master/yaml/build.go#L12
type BuildConfig struct {
Context string
Dockerfile string
Args MappingWithEquals
Labels Labels
CacheFrom StringList `mapstructure:"cache_from"`
Network string
Target string
}
// ShellCommand is a string or list of string args
type ShellCommand []string
// StringList is a type for fields that can be a string or list of strings
type StringList []string
// StringOrNumberList is a type for fields that can be a list of strings or
// numbers
type StringOrNumberList []string
// MappingWithEquals is a mapping type that can be converted from a list of
// key[=value] strings.
// For the key with an empty value (`key=`), the mapped value is set to a pointer to `""`.
// For the key without value (`key`), the mapped value is set to nil.
type MappingWithEquals map[string]*string
// Labels is a mapping type for labels
type Labels map[string]string
// MappingWithColon is a mapping type that can be converted from a list of
// 'key: value' strings
type MappingWithColon map[string]string
// LoggingConfig the logging configuration for a service
type LoggingConfig struct {
Driver string
Options map[string]string
}
// DeployConfig the deployment configuration for a service
type DeployConfig struct {
Mode string
Replicas *uint64
Labels Labels
UpdateConfig *UpdateConfig `mapstructure:"update_config"`
Resources Resources
RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
Placement Placement
EndpointMode string `mapstructure:"endpoint_mode"`
}
// HealthCheckConfig the healthcheck configuration for a service
type HealthCheckConfig struct {
Test HealthCheckTest
Timeout *time.Duration
Interval *time.Duration
Retries *uint64
StartPeriod *time.Duration `mapstructure:"start_period"`
Disable bool
}
// HealthCheckTest is the command run to test the health of a service
type HealthCheckTest []string
// UpdateConfig the service update configuration
type UpdateConfig struct {
Parallelism *uint64
Delay time.Duration
FailureAction string `mapstructure:"failure_action"`
Monitor time.Duration
MaxFailureRatio float32 `mapstructure:"max_failure_ratio"`
Order string
}
// Resources the resource limits and reservations
type Resources struct {
Limits *Resource
Reservations *Resource
}
// Resource is a resource to be limited or reserved
type Resource struct {
// TODO: types to convert from units and ratios
NanoCPUs string `mapstructure:"cpus"`
MemoryBytes UnitBytes `mapstructure:"memory"`
}
// UnitBytes is the bytes type
type UnitBytes int64
// RestartPolicy the service restart policy
type RestartPolicy struct {
Condition string
Delay *time.Duration
MaxAttempts *uint64 `mapstructure:"max_attempts"`
Window *time.Duration
}
// Placement constraints for the service
type Placement struct {
Constraints []string
Preferences []PlacementPreferences
}
// PlacementPreferences is the preferences for a service placement
type PlacementPreferences struct {
Spread string
}
// ServiceNetworkConfig is the network configuration for a service
type ServiceNetworkConfig struct {
Aliases []string
Ipv4Address string `mapstructure:"ipv4_address"`
Ipv6Address string `mapstructure:"ipv6_address"`
}
// ServicePortConfig is the port configuration for a service
type ServicePortConfig struct {
Mode string
Target uint32
Published uint32
Protocol string
}
// ServiceVolumeConfig are references to a volume used by a service
type ServiceVolumeConfig struct {
Type string
Source string
Target string
ReadOnly bool `mapstructure:"read_only"`
Consistency string
Bind *ServiceVolumeBind
Volume *ServiceVolumeVolume
}
// ServiceVolumeBind are options for a service volume of type bind
type ServiceVolumeBind struct {
Propagation string
}
// ServiceVolumeVolume are options for a service volume of type volume
type ServiceVolumeVolume struct {
NoCopy bool `mapstructure:"nocopy"`
}
type fileReferenceConfig struct {
Source string
Target string
UID string
GID string
Mode *uint32
}
// ServiceConfigObjConfig is the config obj configuration for a service
type ServiceConfigObjConfig fileReferenceConfig
// ServiceSecretConfig is the secret configuration for a service
type ServiceSecretConfig fileReferenceConfig
// UlimitsConfig the ulimit configuration
type UlimitsConfig struct {
Single int
Soft int
Hard int
}
// NetworkConfig for a network
type NetworkConfig struct {
Driver string
DriverOpts map[string]string `mapstructure:"driver_opts"`
Ipam IPAMConfig
External External
Internal bool
Attachable bool
Labels Labels
}
// IPAMConfig for a network
type IPAMConfig struct {
Driver string
Config []*IPAMPool
}
// IPAMPool for a network
type IPAMPool struct {
Subnet string
}
// VolumeConfig for a volume
type VolumeConfig struct {
Name string
Driver string
DriverOpts map[string]string `mapstructure:"driver_opts"`
External External
Labels Labels
}
// External identifies a Volume or Network as a reference to a resource that is
// not managed, and should already exist.
// External.name is deprecated and replaced by Volume.name
type External struct {
Name string
External bool
}
// CredentialSpecConfig for credential spec on Windows
type CredentialSpecConfig struct {
File string
Registry string
}
type fileObjectConfig struct {
File string
External External
Labels Labels
}
// SecretConfig for a secret
type SecretConfig fileObjectConfig
// ConfigObjConfig is the config for the swarm "Config" object
type ConfigObjConfig fileObjectConfig

View File

@@ -0,0 +1,116 @@
package compose
import (
"strings"
"unicode"
"unicode/utf8"
"github.com/docker/docker/api/types/mount"
"github.com/pkg/errors"
)
const endOfSpec = rune(0)
// ParseVolume parses a volume spec without any knowledge of the target platform
func ParseVolume(spec string) (ServiceVolumeConfig, error) {
volume := ServiceVolumeConfig{}
switch len(spec) {
case 0:
return volume, errors.New("invalid empty volume spec")
case 1, 2:
volume.Target = spec
volume.Type = string(mount.TypeVolume)
return volume, nil
}
buffer := []rune{}
for _, char := range spec + string(endOfSpec) {
switch {
case isWindowsDrive(buffer, char):
buffer = append(buffer, char)
case char == ':' || char == endOfSpec:
if err := populateFieldFromBuffer(char, buffer, &volume); err != nil {
populateType(&volume)
return volume, errors.Wrapf(err, "invalid spec: %s", spec)
}
buffer = []rune{}
default:
buffer = append(buffer, char)
}
}
populateType(&volume)
return volume, nil
}
func isWindowsDrive(buffer []rune, char rune) bool {
return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0])
}
func populateFieldFromBuffer(char rune, buffer []rune, volume *ServiceVolumeConfig) error {
strBuffer := string(buffer)
switch {
case len(buffer) == 0:
return errors.New("empty section between colons")
// Anonymous volume
case volume.Source == "" && char == endOfSpec:
volume.Target = strBuffer
return nil
case volume.Source == "":
volume.Source = strBuffer
return nil
case volume.Target == "":
volume.Target = strBuffer
return nil
case char == ':':
return errors.New("too many colons")
}
for _, option := range strings.Split(strBuffer, ",") {
switch option {
case "ro":
volume.ReadOnly = true
case "rw":
volume.ReadOnly = false
case "nocopy":
volume.Volume = &ServiceVolumeVolume{NoCopy: true}
default:
if isBindOption(option) {
volume.Bind = &ServiceVolumeBind{Propagation: option}
}
// ignore unknown options
}
}
return nil
}
func isBindOption(option string) bool {
for _, propagation := range mount.Propagations {
if mount.Propagation(option) == propagation {
return true
}
}
return false
}
func populateType(volume *ServiceVolumeConfig) {
switch {
// Anonymous volume
case volume.Source == "":
volume.Type = string(mount.TypeVolume)
case isFilePath(volume.Source):
volume.Type = string(mount.TypeBind)
default:
volume.Type = string(mount.TypeVolume)
}
}
func isFilePath(source string) bool {
switch source[0] {
case '.', '/', '~':
return true
}
first, nextIndex := utf8.DecodeRuneInString(source)
return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
}

59
biz/docker/config.go Normal file
View File

@@ -0,0 +1,59 @@
package docker
import (
"context"
"sort"
"github.com/cuigh/swirl/misc"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
// ConfigList return all configs.
func ConfigList(name string, pageIndex, pageSize int) (configs []swarm.Config, totalCount int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
opts := types.ConfigListOptions{}
if name != "" {
opts.Filters = filters.NewArgs()
opts.Filters.Add("name", name)
}
configs, err = cli.ConfigList(ctx, opts)
if err == nil {
sort.Slice(configs, func(i, j int) bool {
return configs[i].Spec.Name < configs[j].Spec.Name
})
totalCount = len(configs)
start, end := misc.Page(totalCount, pageIndex, pageSize)
configs = configs[start:end]
}
return
})
return
}
// ConfigCreate create a config.
func ConfigCreate(name string, data []byte, labels map[string]string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
// todo:
spec := swarm.ConfigSpec{}
spec.Name = name
spec.Data = data
spec.Labels = labels
_, err = cli.ConfigCreate(ctx, spec)
return
})
}
// ConfigRemove remove a config.
func ConfigRemove(ids []string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
for _, id := range ids {
if err = cli.ConfigRemove(ctx, id); err != nil {
return
}
}
return
})
}

61
biz/docker/container.go Normal file
View File

@@ -0,0 +1,61 @@
package docker
import (
"context"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
// ContainerList return containers on the host.
func ContainerList(name string, pageIndex, pageSize int) (infos []*model.ContainerListInfo, totalCount int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var (
containers []types.Container
opts = types.ContainerListOptions{}
)
if name != "" {
opts.Filters = filters.NewArgs()
opts.Filters.Add("name", name)
}
containers, err = cli.ContainerList(ctx, opts)
if err == nil {
//sort.Slice(containers, func(i, j int) bool {
// return containers[i] < containers[j].Description.Hostname
//})
totalCount = len(containers)
start, end := misc.Page(totalCount, pageIndex, pageSize)
containers = containers[start:end]
if length := len(containers); length > 0 {
infos = make([]*model.ContainerListInfo, length)
for i, c := range containers {
infos[i] = model.NewContainerListInfo(c)
}
}
}
return
})
return
}
// ContainerInspect return detail information of a container.
func ContainerInspect(id string) (container types.ContainerJSON, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
container, err = cli.ContainerInspect(ctx, id)
return
})
return
}
// ContainerInspectRaw return container raw information.
func ContainerInspectRaw(id string) (container types.ContainerJSON, raw []byte, err error) {
mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
container, raw, err = cli.ContainerInspectWithRaw(ctx, id, true)
return
})
return
}

64
biz/docker/docker.go Normal file
View File

@@ -0,0 +1,64 @@
package docker
import (
"context"
"sync"
"github.com/cuigh/auxo/log"
"github.com/cuigh/swirl/misc"
"github.com/docker/docker/client"
)
const (
apiVersion = "1.32"
)
var mgr = &manager{
host: misc.DockerHost,
}
type manager struct {
host string
client *client.Client
locker sync.Mutex
logger *log.Logger
}
func (m *manager) Do(fn func(ctx context.Context, cli *client.Client) error) (err error) {
ctx, cli, err := m.Client()
if err != nil {
return err
}
return fn(ctx, cli)
}
func (m *manager) Client() (ctx context.Context, cli *client.Client, err error) {
if m.client == nil {
m.locker.Lock()
defer m.locker.Unlock()
if m.client == nil {
if m.host == "" {
m.client, err = client.NewEnvClient()
} else {
m.client, err = client.NewClient(m.host, apiVersion, nil, nil)
}
if err != nil {
return
}
}
}
return context.TODO(), m.client, nil
}
func (m *manager) Logger() *log.Logger {
if m.logger == nil {
m.locker.Lock()
defer m.locker.Unlock()
if m.logger == nil {
m.logger = log.Get("docker")
}
}
return m.logger
}

40
biz/docker/image.go Normal file
View File

@@ -0,0 +1,40 @@
package docker
import (
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
)
// ImageList return images on the host.
func ImageList(name string, pageIndex, pageSize int) (images []*model.ImageListInfo, totalCount int, err error) {
ctx, cli, err := mgr.Client()
if err != nil {
return nil, 0, err
}
opts := types.ImageListOptions{}
if name != "" {
opts.Filters = filters.NewArgs()
opts.Filters.Add("reference", name)
}
summaries, err := cli.ImageList(ctx, opts)
if err != nil {
return nil, 0, err
}
//sort.Slice(images, func(i, j int) bool {
// return images[i].ID < images[j].ID
//})
totalCount = len(summaries)
start, end := misc.Page(totalCount, pageIndex, pageSize)
summaries = summaries[start:end]
if length := len(summaries); length > 0 {
images = make([]*model.ImageListInfo, length)
for i, summary := range summaries {
images[i] = model.NewImageListInfo(summary)
}
}
return
}

116
biz/docker/network.go Normal file
View File

@@ -0,0 +1,116 @@
package docker
import (
"context"
"sort"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)
// NetworkList return all networks.
func NetworkList() (networks []types.NetworkResource, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
networks, err = cli.NetworkList(ctx, types.NetworkListOptions{})
if err == nil {
sort.Slice(networks, func(i, j int) bool {
return networks[i].Name < networks[j].Name
})
}
return
})
return
}
// NetworkCount return number of networks.
func NetworkCount() (count int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var networks []types.NetworkResource
networks, err = cli.NetworkList(ctx, types.NetworkListOptions{})
if err == nil {
count = len(networks)
}
return
})
return
}
// NetworkCreate create a network.
func NetworkCreate(info *model.NetworkCreateInfo) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var (
resp types.NetworkCreateResponse
options = types.NetworkCreate{
Internal: info.Internal,
Attachable: info.Attachable,
IPAM: &network.IPAM{},
EnableIPv6: info.IPV6.Enabled,
Options: info.Options.ToMap(),
Labels: info.Labels.ToMap(),
}
)
if info.Driver == "other" {
options.Driver = info.CustomDriver
} else {
options.Driver = info.Driver
}
if info.IPV4.Subnet != "" || info.IPV4.Gateway != "" || info.IPV4.IPRange != "" {
cfg := network.IPAMConfig{
Subnet: info.IPV4.Subnet,
Gateway: info.IPV4.Gateway,
IPRange: info.IPV4.IPRange,
}
options.IPAM.Config = append(options.IPAM.Config, cfg)
}
if info.IPV6.Enabled && (info.IPV6.Subnet != "" || info.IPV6.Gateway != "") {
cfg := network.IPAMConfig{
Subnet: info.IPV6.Subnet,
Gateway: info.IPV6.Gateway,
}
options.IPAM.Config = append(options.IPAM.Config, cfg)
}
resp, err = cli.NetworkCreate(ctx, info.Name, options)
if err == nil && resp.Warning != "" {
mgr.Logger().Warnf("network %s was created but got warning: %s", info.Name, resp.Warning)
}
return
})
}
// NetworkRemove remove a network.
func NetworkRemove(name string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
return cli.NetworkRemove(ctx, name)
})
}
// NetworkDisconnect Disconnect a container from a network.
func NetworkDisconnect(name, container string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
return cli.NetworkDisconnect(ctx, name, container, false)
})
}
// NetworkInspect return network information.
func NetworkInspect(name string) (network types.NetworkResource, err error) {
mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
network, err = cli.NetworkInspect(ctx, name, types.NetworkInspectOptions{})
return
})
return
}
// NetworkInspectRaw return network raw information.
func NetworkInspectRaw(name string) (raw []byte, err error) {
mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
_, raw, err = cli.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{})
return
})
return
}

75
biz/docker/node.go Normal file
View File

@@ -0,0 +1,75 @@
package docker
import (
"context"
"sort"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
// NodeList return all swarm nodes.
func NodeList() (infos []*model.NodeListInfo, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var nodes []swarm.Node
nodes, err = cli.NodeList(ctx, types.NodeListOptions{})
if err == nil {
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Description.Hostname < nodes[j].Description.Hostname
})
infos = make([]*model.NodeListInfo, len(nodes))
for i, n := range nodes {
infos[i] = model.NewNodeListInfo(n)
}
}
return
})
return
}
// NodeCount return number of swarm nodes.
func NodeCount() (count int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var nodes []swarm.Node
nodes, err = cli.NodeList(ctx, types.NodeListOptions{})
if err == nil {
count = len(nodes)
}
return
})
return
}
// NodeRemove remove a swarm node from cluster.
func NodeRemove(id string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
return cli.NodeRemove(ctx, id, types.NodeRemoveOptions{})
})
}
// NodeInspect return node information.
func NodeInspect(id string) (node swarm.Node, raw []byte, err error) {
mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
node, raw, err = cli.NodeInspectWithRaw(ctx, id)
return
})
return
}
// NodeUpdate update a node.
func NodeUpdate(id string, info *model.NodeUpdateInfo) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
version := swarm.Version{
Index: info.Version,
}
spec := swarm.NodeSpec{
Role: info.Role,
Availability: info.Availability,
}
spec.Name = info.Name
spec.Labels = info.Labels.ToMap()
return cli.NodeUpdate(ctx, id, version, spec)
})
}

54
biz/docker/secret.go Normal file
View File

@@ -0,0 +1,54 @@
package docker
import (
"context"
"sort"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/cuigh/swirl/misc"
)
// SecretList return all secrets.
func SecretList(name string, pageIndex, pageSize int) (secrets []swarm.Secret, totalCount int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
opts := types.SecretListOptions{}
if name != "" {
opts.Filters = filters.NewArgs()
opts.Filters.Add("name", name)
}
secrets, err = cli.SecretList(ctx, opts)
if err == nil {
sort.Slice(secrets, func(i, j int) bool {
return secrets[i].Spec.Name < secrets[j].Spec.Name
})
totalCount = len(secrets)
start, end := misc.Page(totalCount, pageIndex, pageSize)
secrets = secrets[start:end]
}
return
})
return
}
// SecretCreate create a secret.
func SecretCreate(name string, data []byte, labels map[string]string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
// todo:
spec := swarm.SecretSpec{}
spec.Name = name
spec.Data = data
spec.Labels = labels
_, err = cli.SecretCreate(ctx, spec)
return
})
}
// SecretRemove remove a secret.
func SecretRemove(id string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
return cli.SecretRemove(ctx, id)
})
}

515
biz/docker/service.go Normal file
View File

@@ -0,0 +1,515 @@
package docker
import (
"context"
"errors"
"sort"
"strings"
"time"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
// ServiceList return service list.
func ServiceList(name string, pageIndex, pageSize int) (infos []*model.ServiceListInfo, totalCount int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var (
services []swarm.Service
nodes []swarm.Node
activeNodes map[string]struct{}
tasks []swarm.Task
)
// acquire services
opts := types.ServiceListOptions{}
if name != "" {
opts.Filters = filters.NewArgs()
opts.Filters.Add("name", name)
}
services, err = cli.ServiceList(ctx, opts)
if err != nil {
return
}
sort.Slice(services, func(i, j int) bool {
return services[i].Spec.Name < services[j].Spec.Name
})
totalCount = len(services)
start, end := misc.Page(totalCount, pageIndex, pageSize)
services = services[start:end]
// acquire all swarm nodes
nodes, err = cli.NodeList(ctx, types.NodeListOptions{})
if err != nil {
return
}
activeNodes = make(map[string]struct{})
for _, n := range nodes {
if n.Status.State != swarm.NodeStateDown {
activeNodes[n.ID] = struct{}{}
}
}
// acquire all related tasks
taskOpts := types.TaskListOptions{
Filters: filters.NewArgs(),
}
for _, service := range services {
taskOpts.Filters.Add("service", service.ID)
}
tasks, err = cli.TaskList(ctx, taskOpts)
if err != nil {
return
}
// count active tasks
running, tasksNoShutdown := map[string]uint64{}, map[string]uint64{}
for _, task := range tasks {
if task.DesiredState != swarm.TaskStateShutdown {
tasksNoShutdown[task.ServiceID]++
}
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
running[task.ServiceID]++
}
}
infos = make([]*model.ServiceListInfo, len(services))
for i, service := range services {
infos[i] = model.NewServiceListInfo(service, running[service.ID])
if service.Spec.Mode.Global != nil {
infos[i].Replicas = tasksNoShutdown[service.ID]
}
}
return
})
return
}
// ServiceCount return number of services.
func ServiceCount() (count int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var services []swarm.Service
if services, err = cli.ServiceList(ctx, types.ServiceListOptions{}); err == nil {
count = len(services)
}
return
})
return
}
// ServiceInspect return service raw information.
func ServiceInspect(name string) (service swarm.Service, raw []byte, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
service, raw, err = cli.ServiceInspectWithRaw(ctx, name, types.ServiceInspectOptions{})
return
})
return
}
// ServiceUpdate update a service.
func ServiceUpdate(info *model.ServiceInfo) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
service, _, err := cli.ServiceInspectWithRaw(ctx, info.Name, types.ServiceInspectOptions{})
if err != nil {
return err
}
spec := service.Spec
// Annotations
spec.Annotations.Labels = info.ServiceLabels.ToMap()
// ContainerSpec
spec.TaskTemplate.ContainerSpec.Image = info.Image
spec.TaskTemplate.ContainerSpec.Dir = info.Dir
spec.TaskTemplate.ContainerSpec.User = info.User
spec.TaskTemplate.ContainerSpec.Labels = info.ContainerLabels.ToMap()
spec.TaskTemplate.ContainerSpec.Command = nil
spec.TaskTemplate.ContainerSpec.Args = nil
spec.TaskTemplate.ContainerSpec.Env = nil
if info.Command != "" {
spec.TaskTemplate.ContainerSpec.Command = strings.Split(info.Command, " ")
}
if info.Args != "" {
spec.TaskTemplate.ContainerSpec.Args = strings.Split(info.Args, " ")
}
if envs := info.Environments.ToMap(); len(envs) > 0 {
for n, v := range envs {
spec.TaskTemplate.ContainerSpec.Env = append(spec.TaskTemplate.ContainerSpec.Env, n+"="+v)
}
}
// Mode
if info.Mode == "replicated" {
if spec.Mode.Replicated == nil {
spec.Mode.Replicated = &swarm.ReplicatedService{Replicas: &info.Replicas}
} else {
spec.Mode.Replicated.Replicas = &info.Replicas
}
} else if info.Mode == "global" && spec.Mode.Global == nil {
spec.Mode.Global = &swarm.GlobalService{}
}
// Network
// todo: only process updated networks
spec.TaskTemplate.Networks = nil
for _, n := range info.Networks {
spec.TaskTemplate.Networks = append(spec.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{Target: n})
}
// Endpoint
spec.EndpointSpec = &swarm.EndpointSpec{Mode: swarm.ResolutionMode(info.Endpoint.Mode)}
for _, p := range info.Endpoint.Ports {
port := swarm.PortConfig{
Protocol: p.Protocol,
TargetPort: p.TargetPort,
PublishedPort: p.PublishedPort,
PublishMode: p.PublishMode,
}
spec.EndpointSpec.Ports = append(spec.EndpointSpec.Ports, port)
}
spec.TaskTemplate.ContainerSpec.Secrets = nil
for _, s := range info.Secrets {
spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, s.ToSecret())
}
spec.TaskTemplate.ContainerSpec.Configs = nil
for _, c := range info.Configs {
spec.TaskTemplate.ContainerSpec.Configs = append(spec.TaskTemplate.ContainerSpec.Configs, c.ToConfig())
}
// Mounts
// todo: fix > original options are not reserved.
spec.TaskTemplate.ContainerSpec.Mounts = nil
for _, m := range info.Mounts {
if m.Target != "" {
mnt := mount.Mount{
Type: mount.Type(m.Type),
Source: m.Source,
Target: m.Target,
ReadOnly: m.ReadOnly,
}
if m.Propagation != "" {
mnt.BindOptions = &mount.BindOptions{Propagation: m.Propagation}
}
spec.TaskTemplate.ContainerSpec.Mounts = append(spec.TaskTemplate.ContainerSpec.Mounts, mnt)
}
}
// Placement
if spec.TaskTemplate.Placement == nil {
spec.TaskTemplate.Placement = &swarm.Placement{}
}
spec.TaskTemplate.Placement.Constraints = nil
for _, c := range info.Placement.Constraints {
if cons := c.ToConstraint(); cons != "" {
spec.TaskTemplate.Placement.Constraints = append(spec.TaskTemplate.Placement.Constraints, cons)
}
}
spec.TaskTemplate.Placement.Preferences = nil
for _, p := range info.Placement.Preferences {
if p.Spread != "" {
pref := swarm.PlacementPreference{
Spread: &swarm.SpreadOver{SpreadDescriptor: p.Spread},
}
spec.TaskTemplate.Placement.Preferences = append(spec.TaskTemplate.Placement.Preferences, pref)
}
}
// update policy
spec.UpdateConfig = &swarm.UpdateConfig{
Parallelism: info.UpdatePolicy.Parallelism,
FailureAction: info.UpdatePolicy.FailureAction,
Order: info.UpdatePolicy.Order,
}
if info.UpdatePolicy.Delay != "" {
spec.UpdateConfig.Delay, err = time.ParseDuration(info.UpdatePolicy.Delay)
if err != nil {
return err
}
}
// rollback policy
spec.RollbackConfig = &swarm.UpdateConfig{
Parallelism: info.RollbackPolicy.Parallelism,
FailureAction: info.RollbackPolicy.FailureAction,
Order: info.RollbackPolicy.Order,
}
if info.RollbackPolicy.Delay != "" {
spec.RollbackConfig.Delay, err = time.ParseDuration(info.RollbackPolicy.Delay)
if err != nil {
return err
}
}
// restart policy
var d time.Duration
spec.TaskTemplate.RestartPolicy = &swarm.RestartPolicy{
Condition: info.RestartPolicy.Condition,
MaxAttempts: &info.RestartPolicy.MaxAttempts,
}
if info.RestartPolicy.Delay != "" {
d, err = time.ParseDuration(info.RestartPolicy.Delay)
if err != nil {
return err
}
spec.TaskTemplate.RestartPolicy.Delay = &d
}
if info.RestartPolicy.Window != "" {
d, err = time.ParseDuration(info.RestartPolicy.Window)
if err != nil {
return err
}
spec.TaskTemplate.RestartPolicy.Window = &d
}
// resources
if info.Resource.Limit.IsSet() || info.Resource.Reserve.IsSet() {
spec.TaskTemplate.Resources = &swarm.ResourceRequirements{}
if info.Resource.Limit.IsSet() {
spec.TaskTemplate.Resources.Limits, err = info.Resource.Limit.ToResources()
if err != nil {
return err
}
}
if info.Resource.Limit.IsSet() {
spec.TaskTemplate.Resources.Reservations, err = info.Resource.Reserve.ToResources()
if err != nil {
return err
}
}
}
// log driver
if info.LogDriver.Name != "" {
spec.TaskTemplate.LogDriver = &swarm.Driver{
Name: info.LogDriver.Name,
Options: info.LogDriver.Options.ToMap(),
}
}
options := types.ServiceUpdateOptions{
RegistryAuthFrom: types.RegistryAuthFromSpec,
QueryRegistry: false,
}
resp, err := cli.ServiceUpdate(context.Background(), info.Name, service.Version, spec, options)
if err == nil && len(resp.Warnings) > 0 {
mgr.Logger().Warnf("service %s was updated but got warnings: %v", info.Name, resp.Warnings)
}
return err
})
}
// ServiceScale adjust replicas of a service.
func ServiceScale(name string, count uint64) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
service, _, err := cli.ServiceInspectWithRaw(ctx, name, types.ServiceInspectOptions{})
if err != nil {
return err
}
spec := service.Spec
if spec.Mode.Replicated == nil {
return errors.New("the mode of service isn't replicated")
}
spec.Mode.Replicated.Replicas = &count
options := types.ServiceUpdateOptions{
RegistryAuthFrom: types.RegistryAuthFromSpec,
QueryRegistry: false,
}
resp, err := cli.ServiceUpdate(context.Background(), name, service.Version, spec, options)
if err == nil && len(resp.Warnings) > 0 {
mgr.Logger().Warnf("service %s was scaled but got warnings: %v", name, resp.Warnings)
}
return err
})
}
// ServiceCreate create a service.
func ServiceCreate(info *model.ServiceInfo) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
service := swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: info.Name,
Labels: info.ServiceLabels.ToMap(),
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: info.Image,
Dir: info.Dir,
User: info.User,
Labels: info.ContainerLabels.ToMap(),
},
},
EndpointSpec: &swarm.EndpointSpec{Mode: swarm.ResolutionModeVIP},
}
if info.Command != "" {
service.TaskTemplate.ContainerSpec.Command = strings.Split(info.Command, " ")
}
if info.Args != "" {
service.TaskTemplate.ContainerSpec.Args = strings.Split(info.Args, " ")
}
if info.Mode == "replicated" {
service.Mode.Replicated = &swarm.ReplicatedService{Replicas: &info.Replicas}
} else if info.Mode == "global" {
service.Mode.Global = &swarm.GlobalService{}
}
if envs := info.Environments.ToMap(); len(envs) > 0 {
for n, v := range envs {
service.TaskTemplate.ContainerSpec.Env = append(service.TaskTemplate.ContainerSpec.Env, n+"="+v)
}
}
for _, n := range info.Networks {
service.TaskTemplate.Networks = append(service.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{Target: n})
}
if info.Endpoint.Mode != "" && len(info.Endpoint.Ports) > 0 {
service.EndpointSpec = &swarm.EndpointSpec{Mode: swarm.ResolutionMode(info.Endpoint.Mode)}
for _, p := range info.Endpoint.Ports {
port := swarm.PortConfig{
Protocol: p.Protocol,
TargetPort: p.TargetPort,
PublishedPort: p.PublishedPort,
PublishMode: p.PublishMode,
}
service.EndpointSpec.Ports = append(service.EndpointSpec.Ports, port)
}
}
for _, s := range info.Secrets {
service.TaskTemplate.ContainerSpec.Secrets = append(service.TaskTemplate.ContainerSpec.Secrets, s.ToSecret())
}
for _, c := range info.Configs {
service.TaskTemplate.ContainerSpec.Configs = append(service.TaskTemplate.ContainerSpec.Configs, c.ToConfig())
}
for _, m := range info.Mounts {
if m.Target != "" {
mnt := mount.Mount{
Type: mount.Type(m.Type),
Source: m.Source,
Target: m.Target,
ReadOnly: m.ReadOnly,
}
if m.Propagation != "" {
mnt.BindOptions = &mount.BindOptions{Propagation: m.Propagation}
}
service.TaskTemplate.ContainerSpec.Mounts = append(service.TaskTemplate.ContainerSpec.Mounts, mnt)
}
}
service.TaskTemplate.Placement = &swarm.Placement{}
for _, c := range info.Placement.Constraints {
if cons := c.ToConstraint(); cons != "" {
service.TaskTemplate.Placement.Constraints = append(service.TaskTemplate.Placement.Constraints, cons)
}
}
for _, p := range info.Placement.Preferences {
if p.Spread != "" {
pref := swarm.PlacementPreference{
Spread: &swarm.SpreadOver{SpreadDescriptor: p.Spread},
}
service.TaskTemplate.Placement.Preferences = append(service.TaskTemplate.Placement.Preferences, pref)
}
}
// update policy
service.UpdateConfig = &swarm.UpdateConfig{
Parallelism: info.UpdatePolicy.Parallelism,
FailureAction: info.UpdatePolicy.FailureAction,
Order: info.UpdatePolicy.Order,
}
if info.UpdatePolicy.Delay != "" {
service.UpdateConfig.Delay, err = time.ParseDuration(info.UpdatePolicy.Delay)
if err != nil {
return err
}
}
// rollback policy
service.RollbackConfig = &swarm.UpdateConfig{
Parallelism: info.RollbackPolicy.Parallelism,
FailureAction: info.RollbackPolicy.FailureAction,
Order: info.RollbackPolicy.Order,
}
if info.RollbackPolicy.Delay != "" {
service.RollbackConfig.Delay, err = time.ParseDuration(info.RollbackPolicy.Delay)
if err != nil {
return err
}
}
// restart policy
var d time.Duration
service.TaskTemplate.RestartPolicy = &swarm.RestartPolicy{
Condition: info.RestartPolicy.Condition,
MaxAttempts: &info.RestartPolicy.MaxAttempts,
}
if info.RestartPolicy.Delay != "" {
d, err = time.ParseDuration(info.RestartPolicy.Delay)
if err != nil {
return err
}
service.TaskTemplate.RestartPolicy.Delay = &d
}
if info.RestartPolicy.Window != "" {
d, err = time.ParseDuration(info.RestartPolicy.Window)
if err != nil {
return err
}
service.TaskTemplate.RestartPolicy.Window = &d
}
// resources
if info.Resource.Limit.IsSet() || info.Resource.Reserve.IsSet() {
service.TaskTemplate.Resources = &swarm.ResourceRequirements{}
if info.Resource.Limit.IsSet() {
service.TaskTemplate.Resources.Limits, err = info.Resource.Limit.ToResources()
if err != nil {
return err
}
}
if info.Resource.Limit.IsSet() {
service.TaskTemplate.Resources.Reservations, err = info.Resource.Reserve.ToResources()
if err != nil {
return err
}
}
}
// log driver
if info.LogDriver.Name != "" {
service.TaskTemplate.LogDriver = &swarm.Driver{
Name: info.LogDriver.Name,
Options: info.LogDriver.Options.ToMap(),
}
}
opts := types.ServiceCreateOptions{EncodedRegistryAuth: info.RegistryAuth}
var resp types.ServiceCreateResponse
resp, err = cli.ServiceCreate(ctx, service, opts)
if err == nil && len(resp.Warnings) > 0 {
mgr.Logger().Warnf("service %s was created but got warnings: %v", info.Name, resp.Warnings)
}
return
})
}
// ServiceRemove remove a service.
func ServiceRemove(name string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
return cli.ServiceRemove(ctx, name)
})
}

396
biz/docker/stack.go Normal file
View File

@@ -0,0 +1,396 @@
package docker
import (
"context"
"fmt"
"sort"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/swirl/biz/docker/compose"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
const stackLabel = "com.docker.stack.namespace"
// StackList return all stacks.
func StackList() (stacks []*model.StackListInfo, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var services []swarm.Service
opts := types.ServiceListOptions{
Filters: filters.NewArgs(),
}
opts.Filters.Add("label", stackLabel)
services, err = cli.ServiceList(ctx, opts)
if err != nil {
return
}
m := make(map[string]*model.StackListInfo)
for _, service := range services {
labels := service.Spec.Labels
name, ok := labels[stackLabel]
if !ok {
err = fmt.Errorf("cannot get label %s for service %s(%s)", stackLabel, service.Spec.Name, service.ID)
return
}
if stack, ok := m[name]; ok {
stack.Services = append(stack.Services, service.Spec.Name)
} else {
m[name] = &model.StackListInfo{
Name: name,
Services: []string{service.Spec.Name},
}
}
}
for _, stack := range m {
stacks = append(stacks, stack)
}
sort.Slice(stacks, func(i, j int) bool {
return stacks[i].Name < stacks[j].Name
})
return
})
return
}
// StackCount return number of stacks.
func StackCount() (count int, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var services []swarm.Service
opts := types.ServiceListOptions{
Filters: filters.NewArgs(),
}
opts.Filters.Add("label", stackLabel)
services, err = cli.ServiceList(ctx, opts)
if err != nil {
return
}
m := make(map[string]struct{})
for _, service := range services {
labels := service.Spec.Labels
if name, ok := labels[stackLabel]; ok {
m[name] = struct{}{}
} else {
mgr.Logger().Warnf("cannot get label %s for service %s(%s)", stackLabel, service.Spec.Name, service.ID)
}
}
count = len(m)
return
})
return
}
// StackRemove remove a stack.
func StackRemove(name string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var (
services []swarm.Service
networks []types.NetworkResource
secrets []swarm.Secret
configs []swarm.Config
errs []error
)
args := filters.NewArgs()
args.Add("label", stackLabel+"="+name)
services, err = cli.ServiceList(ctx, types.ServiceListOptions{Filters: args})
if err != nil {
return
}
networks, err = cli.NetworkList(ctx, types.NetworkListOptions{Filters: args})
if err != nil {
return
}
// API version >= 1.25
secrets, err = cli.SecretList(ctx, types.SecretListOptions{Filters: args})
if err != nil {
return
}
// API version >= 1.30
configs, err = cli.ConfigList(ctx, types.ConfigListOptions{Filters: args})
if err != nil {
return
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
return fmt.Errorf("nothing found in stack: %s", name)
}
// Remove services
for _, service := range services {
if err = cli.ServiceRemove(ctx, service.ID); err != nil {
e := errors.Format("Failed to remove service %s: %s", service.Spec.Name, err)
errs = append(errs, e)
mgr.Logger().Warn(e)
}
}
// Remove secrets
for _, secret := range secrets {
if err = cli.SecretRemove(ctx, secret.ID); err != nil {
e := errors.Format("Failed to remove secret %s: %s", secret.Spec.Name, err)
errs = append(errs, e)
mgr.Logger().Warn(e)
}
}
// Remove configs
for _, config := range configs {
if err = cli.ConfigRemove(ctx, config.ID); err != nil {
e := errors.Format("Failed to remove config %s: %s", config.Spec.Name, err)
errs = append(errs, e)
mgr.Logger().Warn(e)
}
}
// Remove networks
for _, network := range networks {
if err = cli.NetworkRemove(ctx, network.ID); err != nil {
e := errors.Format("Failed to remove network %s: %s", network.Name, err)
errs = append(errs, e)
mgr.Logger().Warn(e)
}
}
if len(errs) > 0 {
return errors.Multi(errs...)
}
return nil
})
}
// StackDeploy deploy a stack.
func StackDeploy(name, content string, authes map[string]string) error {
ctx, cli, err := mgr.Client()
if err != nil {
return err
}
cfg, err := compose.Parse(name, content)
if err != nil {
return err
}
namespace := compose.NewNamespace(name)
serviceNetworks := compose.GetServicesDeclaredNetworks(cfg.Services)
networks, externalNetworks := compose.Networks(namespace, cfg.Networks, serviceNetworks)
if err = validateExternalNetworks(ctx, cli, externalNetworks); err != nil {
return err
}
if err = createNetworks(ctx, cli, namespace, networks); err != nil {
return err
}
secrets, err := compose.Secrets(namespace, cfg.Secrets)
if err != nil {
return err
}
if err = createSecrets(ctx, cli, secrets); err != nil {
return err
}
configs, err := compose.Configs(namespace, cfg.Configs)
if err != nil {
return err
}
if err = createConfigs(ctx, cli, configs); err != nil {
return err
}
services, err := compose.Services(namespace, cfg, cli)
if err != nil {
return err
}
return deployServices(ctx, cli, services, namespace, authes)
}
func validateExternalNetworks(ctx context.Context, cli *client.Client, externalNetworks []string) error {
for _, networkName := range externalNetworks {
network, err := cli.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
switch {
case client.IsErrNotFound(err):
return errors.Format("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
case err != nil:
return err
case network.Scope != "swarm":
return errors.Format("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
}
}
return nil
}
func createNetworks(ctx context.Context, cli *client.Client, namespace compose.Namespace, networks map[string]types.NetworkCreate) error {
opts := types.NetworkListOptions{
Filters: filters.NewArgs(),
}
opts.Filters.Add("label", stackLabel+"="+namespace.Name())
existingNetworks, err := cli.NetworkList(ctx, opts)
if err != nil {
return err
}
existingNetworkMap := make(map[string]types.NetworkResource)
for _, network := range existingNetworks {
existingNetworkMap[network.Name] = network
}
for internalName, createOpts := range networks {
name := namespace.Scope(internalName)
if _, exists := existingNetworkMap[name]; exists {
continue
}
if createOpts.Driver == "" {
createOpts.Driver = "overlay"
}
mgr.Logger().Infof("Creating network %s", name)
if _, err = cli.NetworkCreate(ctx, name, createOpts); err != nil {
return errors.Wrap("failed to create network "+internalName, err)
}
}
return nil
}
func createSecrets(ctx context.Context, cli *client.Client, secrets []swarm.SecretSpec) error {
for _, secretSpec := range secrets {
secret, _, err := cli.SecretInspectWithRaw(ctx, secretSpec.Name)
switch {
case err == nil:
// secret already exists, then we update that
if err = cli.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
return errors.Wrap("failed to update secret "+secretSpec.Name, err)
}
case client.IsErrSecretNotFound(err):
// secret does not exist, then we create a new one.
if _, err = cli.SecretCreate(ctx, secretSpec); err != nil {
return errors.Wrap("failed to create secret "+secretSpec.Name, err)
}
default:
return err
}
}
return nil
}
func createConfigs(ctx context.Context, cli *client.Client, configs []swarm.ConfigSpec) error {
for _, configSpec := range configs {
config, _, err := cli.ConfigInspectWithRaw(ctx, configSpec.Name)
switch {
case err == nil:
// config already exists, then we update that
if err = cli.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
errors.Wrap("failed to update config "+configSpec.Name, err)
}
case client.IsErrConfigNotFound(err):
// config does not exist, then we create a new one.
if _, err = cli.ConfigCreate(ctx, configSpec); err != nil {
errors.Wrap("failed to create config "+configSpec.Name, err)
}
default:
return err
}
}
return nil
}
func getServices(
ctx context.Context,
cli *client.Client,
namespace string,
) ([]swarm.Service, error) {
opts := types.ServiceListOptions{
Filters: filters.NewArgs(),
}
opts.Filters.Add("label", stackLabel+"="+namespace)
return cli.ServiceList(ctx, opts)
}
func deployServices(
ctx context.Context,
cli *client.Client,
services map[string]swarm.ServiceSpec,
namespace compose.Namespace,
authes map[string]string,
//sendAuth bool,
//resolveImage string,
) error {
existingServices, err := getServices(ctx, cli, namespace.Name())
if err != nil {
return err
}
existingServiceMap := make(map[string]swarm.Service)
for _, service := range existingServices {
existingServiceMap[service.Spec.Name] = service
}
for internalName, serviceSpec := range services {
name := namespace.Scope(internalName)
// TODO: Add auth
encodedAuth := authes[serviceSpec.TaskTemplate.ContainerSpec.Image]
//image := serviceSpec.TaskTemplate.ContainerSpec.Image
//if sendAuth {
// // Retrieve encoded auth token from the image reference
// encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
// if err != nil {
// return err
// }
//}
if service, exists := existingServiceMap[name]; exists {
mgr.Logger().Infof("Updating service %s (id: %s)", name, service.ID)
updateOpts := types.ServiceUpdateOptions{
RegistryAuthFrom: types.RegistryAuthFromSpec,
EncodedRegistryAuth: encodedAuth,
}
//if resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[compose.LabelImage]) {
// updateOpts.QueryRegistry = true
//}
response, err := cli.ServiceUpdate(
ctx,
service.ID,
service.Version,
serviceSpec,
updateOpts,
)
if err != nil {
return errors.Wrap("failed to update service "+name, err)
}
for _, warning := range response.Warnings {
mgr.Logger().Warn(warning)
}
} else {
mgr.Logger().Infof("Creating service %s", name)
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
// query registry if flag disabling it was not set
//if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged {
// createOpts.QueryRegistry = true
//}
if _, err = cli.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
return errors.Wrap("failed to create service "+name, err)
}
}
}
return nil
}

78
biz/docker/task.go Normal file
View File

@@ -0,0 +1,78 @@
package docker
import (
"context"
"sort"
"io"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
// TaskList return all running tasks of a service or a node.
func TaskList(service, node string) (infos []*model.TaskInfo, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
var (
tasks []swarm.Task
)
opts := types.TaskListOptions{
Filters: filters.NewArgs(),
}
if service != "" {
opts.Filters.Add("service", service)
}
if node != "" {
opts.Filters.Add("node", node)
}
tasks, err = cli.TaskList(ctx, opts)
if err == nil && len(tasks) > 0 {
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].UpdatedAt.After(tasks[j].UpdatedAt)
})
nodes := make(map[string]string)
for _, t := range tasks {
if _, ok := nodes[t.NodeID]; !ok {
if n, _, e := cli.NodeInspectWithRaw(ctx, t.NodeID); e == nil {
nodes[t.NodeID] = n.Description.Hostname
} else {
nodes[t.NodeID] = ""
//mgr.Logger().Warnf("Node %s of task %s can't be load: %s", t.NodeID, t.ID, e)
}
}
}
infos = make([]*model.TaskInfo, len(tasks))
for i, t := range tasks {
infos[i] = model.NewTaskInfo(t, nodes[t.NodeID])
}
}
return
})
return
}
// TaskInspect return detail information of a task.
func TaskInspect(id string) (task swarm.Task, raw []byte, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
task, raw, err = cli.TaskInspectWithRaw(ctx, id)
return
})
return
}
// TaskLogs returns the logs generated by a task in an io.ReadCloser.
// It's up to the caller to close the stream.
func TaskLogs(id string) (rc io.ReadCloser, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
opts := types.ContainerLogsOptions{}
rc, err = cli.TaskLogs(ctx, id, opts)
return
})
return
}

89
biz/docker/volume.go Normal file
View File

@@ -0,0 +1,89 @@
package docker
import (
"context"
"sort"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
)
// VolumeList return volumes on the host.
func VolumeList(pageIndex, pageSize int) (volumes []*types.Volume, totalCount int, err error) {
var (
ctx context.Context
cli *client.Client
resp volume.VolumesListOKBody
)
ctx, cli, err = mgr.Client()
if err != nil {
return
}
f := filters.NewArgs()
f.Add("dangling", "true")
//f.Add("driver", "xx")
//f.Add("name", "xx")
resp, err = cli.VolumeList(ctx, f)
if err != nil {
return
}
sort.Slice(resp.Volumes, func(i, j int) bool {
return resp.Volumes[i].Name < resp.Volumes[j].Name
})
totalCount = len(resp.Volumes)
start, end := misc.Page(totalCount, pageIndex, pageSize)
volumes = resp.Volumes[start:end]
return
}
// VolumeCreate create a volume.
func VolumeCreate(info *model.VolumeCreateInfo) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
options := volume.VolumesCreateBody{
Name: info.Name,
DriverOpts: info.Options.ToMap(),
Labels: info.Labels.ToMap(),
}
if info.Driver == "other" {
options.Driver = info.CustomDriver
} else {
options.Driver = info.Driver
}
_, err = cli.VolumeCreate(ctx, options)
return
})
}
// VolumeRemove remove a volume.
func VolumeRemove(name string) error {
return mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
return cli.VolumeRemove(ctx, name, false)
})
}
// VolumePrune remove all unused volumes.
func VolumePrune() (report types.VolumesPruneReport, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) (err error) {
f := filters.NewArgs()
report, err = cli.VolumesPrune(ctx, f)
return
})
return
}
// VolumeInspectRaw return volume raw information.
func VolumeInspectRaw(name string) (vol types.Volume, raw []byte, err error) {
err = mgr.Do(func(ctx context.Context, cli *client.Client) error {
vol, raw, err = cli.VolumeInspectWithRaw(ctx, name)
return err
})
return
}

179
biz/event.go Normal file
View File

@@ -0,0 +1,179 @@
package biz
import (
"time"
"github.com/cuigh/auxo/data/guid"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
)
// Event return a event biz instance.
var Event = &eventBiz{}
type eventBiz struct {
}
func (b *eventBiz) Create(event *model.Event) {
event.ID = guid.New()
event.Time = time.Now()
do(func(d dao.Interface) {
err := d.EventCreate(event)
if err != nil {
log.Get("event").Errorf("Create event `%+v` failed: %v", event, err)
}
})
return
}
func (b *eventBiz) CreateRegistry(action model.EventAction, id, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeRegistry,
Action: action,
Code: id,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateService(action model.EventAction, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeService,
Action: action,
Code: name,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateNetwork(action model.EventAction, id, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeNetwork,
Action: action,
Code: id,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateVolume(action model.EventAction, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeVolume,
Action: action,
Code: name,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateStackTask(action model.EventAction, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeStackTask,
Action: action,
Code: name,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateStackArchive(action model.EventAction, id, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeStackArchive,
Action: action,
Code: id,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateSecret(action model.EventAction, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeSecret,
Action: action,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateConfig(action model.EventAction, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeConfig,
Action: action,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateRole(action model.EventAction, id, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeRole,
Action: action,
Code: id,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateUser(action model.EventAction, loginName, name string, user web.User) {
event := &model.Event{
Type: model.EventTypeUser,
Action: action,
Code: loginName,
Name: name,
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateSetting(action model.EventAction, user web.User) {
event := &model.Event{
Type: model.EventTypeSetting,
Action: action,
Code: "",
Name: "Setting",
UserID: user.ID(),
Username: user.Name(),
}
b.Create(event)
}
func (b *eventBiz) CreateAuthentication(action model.EventAction, userID, loginName, username string) {
event := &model.Event{
Type: model.EventTypeAuthentication,
Action: action,
Code: loginName,
Name: username,
UserID: userID,
Username: username,
}
b.Create(event)
}
func (b *eventBiz) List(args *model.EventListArgs) (events []*model.Event, count int, err error) {
do(func(d dao.Interface) {
events, count, err = d.EventList(args)
})
return
}

69
biz/registry.go Normal file
View File

@@ -0,0 +1,69 @@
package biz
import (
"time"
"github.com/cuigh/auxo/data/guid"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
)
var Registry = &registryBiz{}
type registryBiz struct {
}
func (b *registryBiz) Create(registry *model.Registry, user web.User) (err error) {
registry.ID = guid.New()
registry.CreatedAt = time.Now()
registry.UpdatedAt = registry.CreatedAt
do(func(d dao.Interface) {
err = d.RegistryCreate(registry)
if err == nil {
Event.CreateRegistry(model.EventActionCreate, registry.ID, registry.Name, user)
}
})
return
}
func (b *registryBiz) Update(registry *model.Registry, user web.User) (err error) {
do(func(d dao.Interface) {
err = d.RegistryUpdate(registry)
if err == nil {
Event.CreateRegistry(model.EventActionUpdate, registry.ID, registry.Name, user)
}
})
return
}
func (b *registryBiz) List() (registries []*model.Registry, err error) {
do(func(d dao.Interface) {
registries, err = d.RegistryList()
})
return
}
func (b *registryBiz) Get(id string) (registry *model.Registry, err error) {
do(func(d dao.Interface) {
registry, err = d.RegistryGet(id)
})
return
}
func (b *registryBiz) Delete(id string, user web.User) (err error) {
do(func(d dao.Interface) {
var registry *model.Registry
registry, err = d.RegistryGet(id)
if err != nil {
return
}
err = d.RegistryDelete(id)
if err == nil {
Event.CreateRegistry(model.EventActionDelete, id, registry.Name, user)
}
})
return
}

70
biz/role.go Normal file
View File

@@ -0,0 +1,70 @@
package biz
import (
"time"
"github.com/cuigh/auxo/data/guid"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
)
// Role return a role biz instance.
var Role = &roleBiz{}
type roleBiz struct {
}
func (b *roleBiz) List() (roles []*model.Role, err error) {
do(func(d dao.Interface) {
roles, err = d.RoleList()
})
return
}
func (b *roleBiz) Create(role *model.Role, user web.User) (err error) {
do(func(d dao.Interface) {
role.ID = guid.New()
role.CreatedAt = time.Now()
role.UpdatedAt = role.CreatedAt
err = d.RoleCreate(role)
if err == nil {
Event.CreateRole(model.EventActionCreate, role.ID, role.Name, user)
}
})
return
}
func (b *roleBiz) Delete(id string, user web.User) (err error) {
do(func(d dao.Interface) {
var role *model.Role
role, err = d.RoleGet(id)
if err != nil {
return
}
err = d.RoleDelete(id)
if err == nil {
Event.CreateRole(model.EventActionDelete, id, role.Name, user)
}
})
return
}
func (b *roleBiz) Get(id string) (role *model.Role, err error) {
do(func(d dao.Interface) {
role, err = d.RoleGet(id)
})
return
}
func (b *roleBiz) Update(role *model.Role, user web.User) (err error) {
do(func(d dao.Interface) {
role.UpdatedAt = time.Now()
err = d.RoleUpdate(role)
if err == nil {
Event.CreateRole(model.EventActionUpdate, role.ID, role.Name, user)
}
})
return
}

47
biz/setting.go Normal file
View File

@@ -0,0 +1,47 @@
package biz
import (
"time"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
)
// Setting return a setting biz instance.
var Setting = &settingBiz{}
type settingBiz struct {
loc *time.Location
}
func (b *settingBiz) Get() (setting *model.Setting, err error) {
do(func(d dao.Interface) {
setting, err = d.SettingGet()
})
return
}
func (b *settingBiz) Update(setting *model.Setting, user web.User) (err error) {
do(func(d dao.Interface) {
setting.UpdatedBy = user.ID()
setting.UpdatedAt = time.Now()
err = d.SettingUpdate(setting)
if err == nil {
Event.CreateSetting(model.EventActionUpdate, user)
}
})
return
}
func (b *settingBiz) Time(t time.Time) string {
if b.loc == nil {
// todo: auto refresh settings after update
if s, err := b.Get(); err == nil && s != nil {
b.loc = time.FixedZone(s.TimeZone.Name, int(s.TimeZone.Offset))
} else {
b.loc = time.Local
}
}
return t.In(b.loc).Format("2006-01-02 15:04:05")
}

60
biz/stack.go Normal file
View File

@@ -0,0 +1,60 @@
package biz
import (
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
)
// Stack return a stack biz instance.
var Archive = &archiveBiz{}
type archiveBiz struct {
}
func (b *archiveBiz) List(args *model.ArchiveListArgs) (archives []*model.Archive, count int, err error) {
do(func(d dao.Interface) {
archives, count, err = d.ArchiveList(args)
})
return
}
func (b *archiveBiz) Create(archive *model.Archive) (err error) {
do(func(d dao.Interface) {
err = d.ArchiveCreate(archive)
//if err == nil {
// Event.CreateStackArchive(model.EventActionCreate, archive.ID, archive.Name, ctx.User())
//}
})
return
}
func (b *archiveBiz) Delete(id string, user web.User) (err error) {
do(func(d dao.Interface) {
var archive *model.Archive
archive, err = d.ArchiveGet(id)
if err != nil {
return
}
err = d.ArchiveDelete(id)
if err == nil {
Event.CreateStackArchive(model.EventActionDelete, id, archive.Name, user)
}
})
return
}
func (b *archiveBiz) Get(id string) (archives *model.Archive, err error) {
do(func(d dao.Interface) {
archives, err = d.ArchiveGet(id)
})
return
}
func (b *archiveBiz) Update(archive *model.Archive) (err error) {
do(func(d dao.Interface) {
err = d.ArchiveUpdate(archive)
})
return
}

313
biz/user.go Normal file
View File

@@ -0,0 +1,313 @@
package biz
import (
"fmt"
"time"
"github.com/cuigh/auxo/data/guid"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/security/password"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
"github.com/go-ldap/ldap"
)
var ErrIncorrectAuth = errors.New("login name or password is incorrect")
var User = &userBiz{}
type userBiz struct {
}
func (b *userBiz) GetByID(id string) (user *model.User, err error) {
do(func(d dao.Interface) {
user, err = d.UserGetByID(id)
})
return
}
func (b *userBiz) GetByName(loginName string) (user *model.User, err error) {
do(func(d dao.Interface) {
user, err = d.UserGetByName(loginName)
})
return
}
func (b *userBiz) Create(user *model.User, ctxUser web.User) (err error) {
user.ID = guid.New()
user.Status = model.UserStatusActive
user.CreatedAt = time.Now()
user.UpdatedAt = user.CreatedAt
if user.Type == model.UserTypeInternal {
user.Password, user.Salt, err = password.Get(user.Password)
if err != nil {
return
}
}
do(func(d dao.Interface) {
if err = d.UserCreate(user); err == nil && ctxUser != nil {
Event.CreateUser(model.EventActionCreate, user.LoginName, user.Name, ctxUser)
}
})
return
}
func (b *userBiz) Update(user *model.User, ctxUser web.User) (err error) {
do(func(d dao.Interface) {
user.UpdatedAt = time.Now()
if err = d.UserUpdate(user); err == nil {
Event.CreateUser(model.EventActionUpdate, user.LoginName, user.Name, ctxUser)
}
})
return
}
func (b *userBiz) Block(id string) (err error) {
do(func(d dao.Interface) {
err = d.UserBlock(id, true)
})
return
}
func (b *userBiz) Unblock(id string) (err error) {
do(func(d dao.Interface) {
err = d.UserBlock(id, false)
})
return
}
func (b *userBiz) Delete(id string) (err error) {
do(func(d dao.Interface) {
err = d.UserDelete(id)
})
return
}
func (b *userBiz) UpdateInfo(user *model.User) (err error) {
do(func(d dao.Interface) {
err = d.ProfileUpdateInfo(user)
})
return
}
func (b *userBiz) UpdatePassword(id, old_pwd, new_pwd string) (err error) {
do(func(d dao.Interface) {
var (
user *model.User
pwd, salt string
)
user, err = d.UserGetByID(id)
if err != nil {
return
}
if !password.Validate(user.Password, old_pwd, user.Salt) {
err = errors.New("Current password is incorrect")
return
}
pwd, salt, err = password.Get(new_pwd)
if err != nil {
return
}
err = d.ProfileUpdatePassword(id, pwd, salt)
})
return
}
func (b *userBiz) List(args *model.UserListArgs) (users []*model.User, count int, err error) {
do(func(d dao.Interface) {
users, count, err = d.UserList(args)
})
return
}
func (b *userBiz) Count() (count int, err error) {
do(func(d dao.Interface) {
count, err = d.UserCount()
})
return
}
func (b *userBiz) Login(name, pwd string) (token string, err error) {
do(func(d dao.Interface) {
var (
user *model.User
)
user, err = d.UserGetByName(name)
if err != nil {
return
}
if user == nil {
user = &model.User{
Type: model.UserTypeLDAP,
LoginName: name,
}
err = b.loginLDAP(user, pwd)
} else {
if user.Status == model.UserStatusBlocked {
err = fmt.Errorf("user %s is blocked", name)
return
}
if user.Type == model.UserTypeInternal {
err = b.loginInternal(user, pwd)
} else {
err = b.loginLDAP(user, pwd)
}
}
if err != nil {
return
}
session := &model.Session{
UserID: user.ID,
Token: guid.New(),
UpdatedAt: time.Now(),
}
session.Expires = session.UpdatedAt.Add(time.Hour * 24)
err = d.SessionUpdate(session)
if err != nil {
return
}
token = session.Token
// create event
Event.CreateAuthentication(model.EventActionLogin, user.ID, user.LoginName, user.Name)
})
return
}
func (b *userBiz) loginInternal(user *model.User, pwd string) error {
if !password.Validate(user.Password, pwd, user.Salt) {
return ErrIncorrectAuth
}
return nil
}
func (b *userBiz) loginLDAP(user *model.User, pwd string) error {
setting, err := Setting.Get()
if err != nil {
return err
}
if !setting.LDAP.Enabled {
return ErrIncorrectAuth
}
l, err := ldap.Dial("tcp", setting.LDAP.Address)
if err != nil {
return err
}
defer l.Close()
// bind
err = l.Bind(user.LoginName, pwd)
if err != nil {
log.Get("user").Error("Login by LDAP failed: ", err)
return ErrIncorrectAuth
}
// Stop here for an exist user because we only need validate password.
if user.ID != "" {
return nil
}
// If user wasn't exist, we need create it
req := ldap.NewSearchRequest(
setting.LDAP.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=organizationalPerson)(%s=%s))", setting.LDAP.LoginAttr, user.LoginName),
[]string{"dn", setting.LDAP.EmailAttr, setting.LDAP.LoginAttr, setting.LDAP.NameAttr},
nil,
)
searchResult, err := l.Search(req)
if err != nil {
return err
}
if len(searchResult.Entries) == 0 {
return ErrIncorrectAuth
}
entry := searchResult.Entries[0]
user.Email = entry.GetAttributeValue(setting.LDAP.EmailAttr)
user.Name = entry.GetAttributeValue(setting.LDAP.NameAttr)
if user.ID == "" {
return b.Create(user, nil)
}
return nil
}
// Identify authenticate user
func (b *userBiz) Identify(token string) (user web.User) {
do(func(d dao.Interface) {
var (
roles []*model.Role
role *model.Role
)
session, err := d.SessionGet(token)
if err != nil {
log.Get("user").Errorf("Load session failed: %v", err)
return
}
if session == nil || session.Expires.Before(time.Now()) {
return
}
u, err := d.UserGetByID(session.UserID)
if err != nil {
log.Get("user").Errorf("Load user failed: %v", err)
return
}
if u == nil {
return
}
if len(u.Roles) > 0 {
roles = make([]*model.Role, len(u.Roles))
for i, id := range u.Roles {
role, err = d.RoleGet(id)
if err != nil {
return
} else if role != nil {
roles[i] = role
}
}
}
user = model.NewAuthUser(u, roles)
})
return
}
// Authorize check permission of user
func (b *userBiz) Authorize(user web.User, handler string) bool {
if au, ok := user.(*model.AuthUser); ok {
return au.IsAllowed(handler)
}
return false
}
//
//func (b *userBiz) Find(key string) string {
// b.locker.Lock()
// defer b.locker.Unlock()
//
// return b.tickets[key]
//}
//
//func (b *userBiz) setTicket(name string) (key string) {
// b.locker.Lock()
// defer b.locker.Unlock()
//
// key = guid.New()
// b.tickets[key] = name
// return
//}

14
config/app.xml Normal file
View File

@@ -0,0 +1,14 @@
<config>
<app>
<add key="name" value="swirl"/>
<add key="debug" value="true"/>
</app>
<web>
<add key="address" value=":8001"/>
<add key="authorize_mode" value="?"/>
</web>
<!-- <swirl>
<add key="db_type" value="mongo"/>
<add key="db_address" value="localhost:27017/swirl"/>
</swirl> -->
</config>

8
config/profile.xml Normal file
View File

@@ -0,0 +1,8 @@
<profiles>
<profile>
<option key="debug" value="false"/>
</profile>
<profile name="dev">
<option key="debug" value="true"/>
</profile>
</profiles>

34
controller/common.go Normal file
View File

@@ -0,0 +1,34 @@
package controller
import (
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/model"
)
func newModel(ctx web.Context) web.Map {
return web.Map{
"ContextUser": ctx.User(),
}
}
func newPagerModel(ctx web.Context, totalCount, size, page int) web.Map {
pager := model.NewPager(ctx.Request().RequestURI, totalCount, size, page)
return newModel(ctx).Add("Pager", pager)
}
func ajaxResult(ctx web.Context, err error) error {
if err != nil {
return err
}
return ctx.JSON(web.Map{
"success": err == nil,
})
}
func ajaxSuccess(ctx web.Context, data interface{}) error {
return ctx.JSON(web.Map{
"success": true,
"data": data,
})
}

70
controller/config.go Normal file
View File

@@ -0,0 +1,70 @@
package controller
import (
"strings"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/util/cast"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/model"
)
type ConfigController struct {
List web.HandlerFunc `path:"/" name:"config.list" authorize:"!" desc:"config list page"`
Delete web.HandlerFunc `path:"/delete" method:"post" name:"config.delete" authorize:"!" desc:"delete config"`
New web.HandlerFunc `path:"/new" name:"config.new" authorize:"!" desc:"new config page"`
Create web.HandlerFunc `path:"/new" method:"post" name:"config.create" authorize:"!" desc:"create config"`
}
func Config() (c *ConfigController) {
c = &ConfigController{}
c.List = func(ctx web.Context) error {
name := ctx.Q("name")
page := cast.ToIntD(ctx.Q("page"), 1)
configs, totalCount, err := docker.ConfigList(name, page, model.PageSize)
if err != nil {
return err
}
m := newPagerModel(ctx, totalCount, model.PageSize, page).
Add("Name", name).
Add("Configs", configs)
return ctx.Render("config/list", m)
}
c.Delete = func(ctx web.Context) error {
ids := strings.Split(ctx.F("ids"), ",")
err := docker.ConfigRemove(ids)
return ajaxResult(ctx, err)
}
c.New = func(ctx web.Context) error {
m := newModel(ctx)
return ctx.Render("config/new", m)
}
c.Create = func(ctx web.Context) error {
v := struct {
Name string `json:"name"`
Data string `json:"data"`
Labels []struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"labels"`
}{}
err := ctx.Bind(&v)
if err == nil {
labels := make(map[string]string)
for _, l := range v.Labels {
if l.Name != "" && l.Value != "" {
labels[l.Name] = l.Value
}
}
err = docker.ConfigCreate(v.Name, []byte(v.Data), labels)
}
return ajaxResult(ctx, err)
}
return
}

65
controller/container.go Normal file
View File

@@ -0,0 +1,65 @@
package controller
import (
"bytes"
"encoding/json"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/util/cast"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/model"
)
type ContainerController struct {
List web.HandlerFunc `path:"/" name:"container.list" authorize:"!" desc:"container list page"`
Detail web.HandlerFunc `path:"/:id/detail" name:"container.detail" authorize:"!" desc:"container detail page"`
Raw web.HandlerFunc `path:"/:id/raw" name:"container.raw" authorize:"!" desc:"container raw page"`
}
func Container() (c *ContainerController) {
c = &ContainerController{}
c.List = func(ctx web.Context) error {
name := ctx.Q("name")
page := cast.ToIntD(ctx.Q("page"), 1)
containers, totalCount, err := docker.ContainerList(name, page, model.PageSize)
if err != nil {
return err
}
m := newPagerModel(ctx, totalCount, model.PageSize, page).
Add("Name", name).
Add("Containers", containers)
return ctx.Render("container/list", m)
}
c.Detail = func(ctx web.Context) error {
id := ctx.P("id")
container, err := docker.ContainerInspect(id)
if err != nil {
return err
}
m := newModel(ctx).Add("Container", container)
return ctx.Render("container/detail", m)
}
c.Raw = func(ctx web.Context) error {
id := ctx.P("id")
container, raw, err := docker.ContainerInspectRaw(id)
if err != nil {
return err
}
buf := &bytes.Buffer{}
err = json.Indent(buf, raw, "", " ")
if err != nil {
return err
}
m := newModel(ctx).Add("Container", container).Add("Raw", string(buf.Bytes()))
return ctx.Render("container/raw", m)
}
return
}

38
controller/event.go Normal file
View File

@@ -0,0 +1,38 @@
package controller
import (
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/model"
)
type EventController struct {
List web.HandlerFunc `path:"/" name:"event.list" authorize:"!" desc:"event list page"`
}
func Event() (c *EventController) {
c = &EventController{}
c.List = func(ctx web.Context) error {
args := &model.EventListArgs{}
err := ctx.Bind(args)
if err != nil {
return err
}
args.PageSize = model.PageSize
if args.PageIndex == 0 {
args.PageIndex = 1
}
events, totalCount, err := biz.Event.List(args)
if err != nil {
return err
}
m := newPagerModel(ctx, totalCount, model.PageSize, args.PageIndex).
Add("Events", events).Add("Args", args)
return ctx.Render("system/event/list", m)
}
return
}

102
controller/home.go Normal file
View File

@@ -0,0 +1,102 @@
package controller
import (
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/model"
)
type HomeController struct {
Index web.HandlerFunc `path:"/" name:"index" authorize:"?" desc:"index page"`
Error404 web.HandlerFunc `path:"/404" name:"404" authorize:"*" desc:"404 page"`
Login web.HandlerFunc `path:"/login" name:"login" authorize:"*" desc:"sign in page"`
InitGet web.HandlerFunc `path:"/init" name:"init" authorize:"*" desc:"initialize page"`
InitPost web.HandlerFunc `path:"/init" method:"post" name:"init" authorize:"*" desc:"initialize system"`
}
func Home() (c *HomeController) {
c = &HomeController{}
c.Index = func(ctx web.Context) (err error) {
var (
count int
m = newModel(ctx)
)
if count, err = docker.NodeCount(); err != nil {
return
}
m.Add("NodeCount", count)
if count, err = docker.NetworkCount(); err != nil {
return
}
m.Add("NetworkCount", count)
if count, err = docker.ServiceCount(); err != nil {
return
}
m.Add("ServiceCount", count)
if count, err = docker.StackCount(); err != nil {
return
}
m.Add("StackCount", count)
return ctx.Render("index", m)
}
c.Login = func(ctx web.Context) error {
count, err := biz.User.Count()
if err != nil {
return err
} else if count == 0 {
return ctx.Redirect("init")
}
if ctx.User() != nil {
u := ctx.Q("from")
if u == "" {
u = "/"
}
return ctx.Redirect(u)
}
return ctx.Render("login", nil)
}
c.InitGet = func(ctx web.Context) error {
count, err := biz.User.Count()
if err != nil {
return err
} else if count > 0 {
return ctx.Redirect("login")
}
return ctx.Render("init", nil)
}
c.InitPost = func(ctx web.Context) error {
count, err := biz.User.Count()
if err != nil {
return err
} else if count > 0 {
return errors.New("Swirl was already initialized")
}
user := &model.User{}
err = ctx.Bind(user)
if err != nil {
return err
}
user.Admin = true
user.Type = model.UserTypeInternal
err = biz.User.Create(user, nil)
return ajaxResult(ctx, err)
}
c.Error404 = func(ctx web.Context) error {
return ctx.Render("404", nil)
}
return
}

34
controller/image.go Normal file
View File

@@ -0,0 +1,34 @@
package controller
import (
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/util/cast"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/model"
)
type ImageController struct {
List web.HandlerFunc `path:"/" name:"image.list" authorize:"!" desc:"image list page"`
//Detail web.HandlerFunc `path:"/:id/detail" name:"image.detail" authorize:"!" desc:"image detail page"`
//Raw web.HandlerFunc `path:"/:id/raw" name:"image.raw" authorize:"!" desc:"image raw page"`
}
func Image() (c *ImageController) {
c = &ImageController{}
c.List = func(ctx web.Context) error {
name := ctx.Q("name")
page := cast.ToIntD(ctx.Q("page"), 1)
images, totalCount, err := docker.ImageList(name, page, model.PageSize)
if err != nil {
return err
}
m := newPagerModel(ctx, totalCount, model.PageSize, page).
Add("Name", name).
Add("Images", images)
return ctx.Render("image/list", m)
}
return
}

102
controller/network.go Normal file
View File

@@ -0,0 +1,102 @@
package controller
import (
"bytes"
"encoding/json"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/model"
)
type NetworkController struct {
List web.HandlerFunc `path:"/" name:"network.list" authorize:"!" desc:"network list page"`
New web.HandlerFunc `path:"/new" name:"network.new" authorize:"!" desc:"new network page"`
Create web.HandlerFunc `path:"/create" method:"post" name:"network.create" authorize:"!" desc:"create network"`
Delete web.HandlerFunc `path:"/delete" method:"post" name:"network.delete" authorize:"!" desc:"delete network"`
Disconnect web.HandlerFunc `path:"/:name/disconnect" method:"post" name:"network.disconnect" authorize:"!" desc:"disconnect network"`
Detail web.HandlerFunc `path:"/:name/detail" name:"network.detail" authorize:"!" desc:"network detail page"`
Raw web.HandlerFunc `path:"/:name/raw" name:"network.raw" authorize:"!" desc:"network raw page"`
}
// Network create a NetworkController instance.
func Network() (c *NetworkController) {
c = &NetworkController{}
c.List = func(ctx web.Context) error {
networks, err := docker.NetworkList()
if err != nil {
return err
}
m := newModel(ctx).Add("Networks", networks)
return ctx.Render("network/list", m)
}
c.New = func(ctx web.Context) error {
m := newModel(ctx)
return ctx.Render("/network/new", m)
}
c.Create = func(ctx web.Context) error {
info := &model.NetworkCreateInfo{}
err := ctx.Bind(info)
if err != nil {
return err
}
err = docker.NetworkCreate(info)
if err == nil {
biz.Event.CreateNetwork(model.EventActionCreate, info.Name, info.Name, ctx.User())
}
return ajaxResult(ctx, err)
}
c.Delete = func(ctx web.Context) error {
name := ctx.F("name")
err := docker.NetworkRemove(name)
if err == nil {
biz.Event.CreateNetwork(model.EventActionDelete, name, name, ctx.User())
}
return ajaxResult(ctx, err)
}
c.Disconnect = func(ctx web.Context) error {
name := ctx.P("name")
container := ctx.F("container")
err := docker.NetworkDisconnect(name, container)
if err == nil {
biz.Event.CreateNetwork(model.EventActionDisconnect, name, name+" <-> "+container, ctx.User())
}
return ajaxResult(ctx, err)
}
c.Detail = func(ctx web.Context) error {
name := ctx.P("name")
network, err := docker.NetworkInspect(name)
if err != nil {
return err
}
m := newModel(ctx).Add("Network", network)
return ctx.Render("network/detail", m)
}
c.Raw = func(ctx web.Context) error {
name := ctx.P("name")
raw, err := docker.NetworkInspectRaw(name)
if err != nil {
return err
}
buf := &bytes.Buffer{}
err = json.Indent(buf, raw, "", " ")
if err != nil {
return err
}
m := newModel(ctx).Add("Network", name).Add("Raw", string(buf.Bytes()))
return ctx.Render("network/raw", m)
}
return
}

95
controller/node.go Normal file
View File

@@ -0,0 +1,95 @@
package controller
import (
"bytes"
"encoding/json"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/model"
)
type NodeController struct {
List web.HandlerFunc `path:"/" name:"node.list" authorize:"!" desc:"node list page"`
Detail web.HandlerFunc `path:"/:id/detail" name:"node.detail" authorize:"!" desc:"node detail page"`
Raw web.HandlerFunc `path:"/:id/raw" name:"node.raw" authorize:"!" desc:"node raw page"`
Delete web.HandlerFunc `path:"/delete" method:"post" name:"node.delete" authorize:"!" desc:"delete node"`
Edit web.HandlerFunc `path:"/:id/edit" name:"node.edit" authorize:"!" desc:"node edit page"`
Update web.HandlerFunc `path:"/:id/update" method:"post" name:"node.update" authorize:"!" desc:"update node"`
}
func Node() (c *NodeController) {
c = &NodeController{}
c.List = func(ctx web.Context) error {
nodes, err := docker.NodeList()
if err != nil {
return err
}
m := newModel(ctx).Add("Nodes", nodes)
return ctx.Render("node/list", m)
}
c.Delete = func(ctx web.Context) error {
id := ctx.F("id")
err := docker.NodeRemove(id)
return ajaxResult(ctx, err)
}
c.Detail = func(ctx web.Context) error {
id := ctx.P("id")
node, _, err := docker.NodeInspect(id)
if err != nil {
return err
}
tasks, err := docker.TaskList("", id)
if err != nil {
return err
}
m := newModel(ctx).Add("Node", node).Add("Tasks", tasks)
return ctx.Render("node/detail", m)
}
c.Raw = func(ctx web.Context) error {
id := ctx.P("id")
node, raw, err := docker.NodeInspect(id)
if err != nil {
return err
}
buf := &bytes.Buffer{}
err = json.Indent(buf, raw, "", " ")
if err != nil {
return err
}
m := newModel(ctx).Add("ID", id).Add("Node", node).Add("Raw", string(buf.Bytes()))
return ctx.Render("node/raw", m)
}
c.Edit = func(ctx web.Context) error {
id := ctx.P("id")
node, _, err := docker.NodeInspect(id)
if err != nil {
return err
}
m := newModel(ctx).Add("Node", node)
return ctx.Render("node/edit", m)
}
c.Update = func(ctx web.Context) error {
id := ctx.P("id")
info := &model.NodeUpdateInfo{}
err := ctx.Bind(info)
if err == nil {
err = docker.NodeUpdate(id, info)
}
return ajaxResult(ctx, err)
}
return
}

52
controller/profile.go Normal file
View File

@@ -0,0 +1,52 @@
package controller
import (
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/model"
)
type ProfileController struct {
Index web.HandlerFunc `path:"/" name:"profile.info" authorize:"?" desc:"profile info page"`
ModifyInfo web.HandlerFunc `path:"/" method:"post" name:"profile.info.modify" authorize:"?" desc:"modify info"`
Password web.HandlerFunc `path:"/password" name:"profile.password" authorize:"?" desc:"profile password page"`
ModifyPassword web.HandlerFunc `path:"/password" method:"post" name:"profile.password.modify" authorize:"?" desc:"modify password"`
}
func Profile() (c *ProfileController) {
c = &ProfileController{}
c.Index = func(ctx web.Context) error {
user, err := biz.User.GetByID(ctx.User().ID())
if err != nil {
return err
}
m := newModel(ctx).Add("User", user)
return ctx.Render("profile/index", m)
}
c.ModifyInfo = func(ctx web.Context) error {
user := &model.User{}
err := ctx.Bind(user)
if err == nil {
user.ID = ctx.User().ID()
err = biz.User.UpdateInfo(user)
}
return ajaxResult(ctx, err)
}
c.Password = func(ctx web.Context) error {
m := newModel(ctx)
return ctx.Render("profile/password", m)
}
c.ModifyPassword = func(ctx web.Context) error {
old_pwd := ctx.F("password_old")
new_pwd := ctx.F("password")
err := biz.User.UpdatePassword(ctx.User().ID(), old_pwd, new_pwd)
return ajaxResult(ctx, err)
}
return
}

Some files were not shown because too many files have changed in this diff Show More