mirror of
https://github.com/cuigh/swirl
synced 2025-06-26 18:16:50 +00:00
Add codes
This commit is contained in:
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.js linguist-language=Go
|
||||
*.css linguist-language=Go
|
||||
*.jet linguist-language=Go
|
||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal 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
15
Dockerfile
Normal 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
12
Gopkg.toml
Normal 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
111
README.md
Normal 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
|
||||
|
||||

|
||||
|
||||
### Service list
|
||||
|
||||

|
||||
|
||||
### Compose list
|
||||
|
||||

|
||||
|
||||
### Role editing
|
||||
|
||||

|
||||
|
||||
### Settings
|
||||
|
||||

|
||||
|
||||
## 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
13071
assets/bulma/bulma.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/bulma/bulma.css.map
Normal file
1
assets/bulma/bulma.css.map
Normal file
File diff suppressed because one or more lines are too long
213
assets/codemirror/addon/comment.js
vendored
Normal file
213
assets/codemirror/addon/comment.js
vendored
Normal 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;
|
||||
});
|
||||
});
|
||||
341
assets/codemirror/codemirror.css
Normal file
341
assets/codemirror/codemirror.css
Normal 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; }
|
||||
9622
assets/codemirror/codemirror.js
Normal file
9622
assets/codemirror/codemirror.js
Normal file
File diff suppressed because it is too large
Load Diff
613
assets/codemirror/keymap/sublime.js
vendored
Normal file
613
assets/codemirror/keymap/sublime.js
vendored
Normal 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
118
assets/codemirror/mode/yaml.js
vendored
Normal 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");
|
||||
|
||||
});
|
||||
99
assets/highlight/highlight.css
Normal file
99
assets/highlight/highlight.css
Normal 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;
|
||||
}
|
||||
2
assets/highlight/highlight.pack.js
Normal file
2
assets/highlight/highlight.pack.js
Normal file
File diff suppressed because one or more lines are too long
10253
assets/jquery/jquery-3.2.1.js
vendored
Normal file
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
4
assets/jquery/jquery-3.2.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/jquery/jquery-3.2.1.min.map
Normal file
1
assets/jquery/jquery-3.2.1.min.map
Normal file
File diff suppressed because one or more lines are too long
2550
assets/lyicon/css/lyicon.css
Executable file
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
BIN
assets/lyicon/fonts/lyicon.eot
Executable file
Binary file not shown.
796
assets/lyicon/fonts/lyicon.svg
Executable file
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
BIN
assets/lyicon/fonts/lyicon.ttf
Executable file
Binary file not shown.
BIN
assets/lyicon/fonts/lyicon.woff
Executable file
BIN
assets/lyicon/fonts/lyicon.woff
Executable file
Binary file not shown.
252
assets/swirl/css/swirl.css
Normal file
252
assets/swirl/css/swirl.css
Normal 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
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
1918
assets/swirl/js/swirl.js
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/swirl/js/swirl.js.map
Normal file
1
assets/swirl/js/swirl.js.map
Normal file
File diff suppressed because one or more lines are too long
45
assets/swirl/ts/config/list.ts
Normal file
45
assets/swirl/ts/config/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
10
assets/swirl/ts/config/new.ts
Normal file
10
assets/swirl/ts/config/new.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
387
assets/swirl/ts/core/ajax.ts
Normal file
387
assets/swirl/ts/core/ajax.ts
Normal 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;
|
||||
211
assets/swirl/ts/core/bulma.ts
Normal file
211
assets/swirl/ts/core/bulma.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
assets/swirl/ts/core/core.ts
Normal file
82
assets/swirl/ts/core/core.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
71
assets/swirl/ts/core/dispatcher.ts
Normal file
71
assets/swirl/ts/core/dispatcher.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
420
assets/swirl/ts/core/form.ts
Normal file
420
assets/swirl/ts/core/form.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
assets/swirl/ts/core/page.ts
Normal file
16
assets/swirl/ts/core/page.ts
Normal 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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
117
assets/swirl/ts/core/table.ts
Normal file
117
assets/swirl/ts/core/table.ts
Normal 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>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
410
assets/swirl/ts/core/validator.ts
Normal file
410
assets/swirl/ts/core/validator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
assets/swirl/ts/network/detail.ts
Normal file
26
assets/swirl/ts/network/detail.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
24
assets/swirl/ts/network/list.ts
Normal file
24
assets/swirl/ts/network/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
19
assets/swirl/ts/network/new.ts
Normal file
19
assets/swirl/ts/network/new.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
10
assets/swirl/ts/node/edit.ts
Normal file
10
assets/swirl/ts/node/edit.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
26
assets/swirl/ts/node/list.ts
Normal file
26
assets/swirl/ts/node/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
36
assets/swirl/ts/registry/list.ts
Normal file
36
assets/swirl/ts/registry/list.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
assets/swirl/ts/role/edit.ts
Normal file
32
assets/swirl/ts/role/edit.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
assets/swirl/ts/role/list.ts
Normal file
26
assets/swirl/ts/role/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
26
assets/swirl/ts/role/new.ts
Normal file
26
assets/swirl/ts/role/new.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
assets/swirl/ts/secret/list.ts
Normal file
45
assets/swirl/ts/secret/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
10
assets/swirl/ts/secret/new.ts
Normal file
10
assets/swirl/ts/secret/new.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
258
assets/swirl/ts/service/edit.ts
Normal file
258
assets/swirl/ts/service/edit.ts
Normal 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 != "");
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
59
assets/swirl/ts/service/list.ts
Normal file
59
assets/swirl/ts/service/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
10
assets/swirl/ts/setting/index.ts
Normal file
10
assets/swirl/ts/setting/index.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
37
assets/swirl/ts/stack/archive/list.ts
Normal file
37
assets/swirl/ts/stack/archive/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
24
assets/swirl/ts/stack/task/list.ts
Normal file
24
assets/swirl/ts/stack/task/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
50
assets/swirl/ts/user/list.ts
Normal file
50
assets/swirl/ts/user/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
53
assets/swirl/ts/volume/list.ts
Normal file
53
assets/swirl/ts/volume/list.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
15
assets/swirl/ts/volume/new.ts
Normal file
15
assets/swirl/ts/volume/new.ts
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
10
assets/swirl/tsconfig.json
Normal file
10
assets/swirl/tsconfig.json
Normal 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
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
15
biz/biz.go
Normal 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
23
biz/docker/compose.go
Normal 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
|
||||
//}
|
||||
1
biz/docker/compose/README.md
Normal file
1
biz/docker/compose/README.md
Normal file
@@ -0,0 +1 @@
|
||||
see: https://github.com/docker/cli
|
||||
141
biz/docker/compose/convert.go
Normal file
141
biz/docker/compose/convert.go
Normal 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
|
||||
}
|
||||
680
biz/docker/compose/convert_service.go
Normal file
680
biz/docker/compose/convert_service.go
Normal 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
|
||||
}
|
||||
92
biz/docker/compose/convert_volume.go
Normal file
92
biz/docker/compose/convert_volume.go
Normal 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
|
||||
}
|
||||
81
biz/docker/compose/envfile.go
Normal file
81
biz/docker/compose/envfile.go
Normal 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)
|
||||
}
|
||||
93
biz/docker/compose/interpolation.go
Normal file
93
biz/docker/compose/interpolation.go
Normal 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
723
biz/docker/compose/loader.go
Normal file
723
biz/docker/compose/loader.go
Normal 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 ""
|
||||
}
|
||||
}
|
||||
69
biz/docker/compose/opts.go
Normal file
69
biz/docker/compose/opts.go
Normal 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
|
||||
}
|
||||
86
biz/docker/compose/parse.go
Normal file
86
biz/docker/compose/parse.go
Normal 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
163
biz/docker/compose/port.go
Normal 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
|
||||
}
|
||||
101
biz/docker/compose/template.go
Normal file
101
biz/docker/compose/template.go
Normal 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
353
biz/docker/compose/types.go
Normal 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
|
||||
116
biz/docker/compose/volume.go
Normal file
116
biz/docker/compose/volume.go
Normal 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
59
biz/docker/config.go
Normal 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
61
biz/docker/container.go
Normal 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
64
biz/docker/docker.go
Normal 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
40
biz/docker/image.go
Normal 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
116
biz/docker/network.go
Normal 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
75
biz/docker/node.go
Normal 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
54
biz/docker/secret.go
Normal 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
515
biz/docker/service.go
Normal 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
396
biz/docker/stack.go
Normal 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
78
biz/docker/task.go
Normal 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
89
biz/docker/volume.go
Normal 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
179
biz/event.go
Normal 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
69
biz/registry.go
Normal 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 = ®istryBiz{}
|
||||
|
||||
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
70
biz/role.go
Normal 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
47
biz/setting.go
Normal 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
60
biz/stack.go
Normal 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
313
biz/user.go
Normal 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
14
config/app.xml
Normal 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
8
config/profile.xml
Normal 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
34
controller/common.go
Normal 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
70
controller/config.go
Normal 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
65
controller/container.go
Normal 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
38
controller/event.go
Normal 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
102
controller/home.go
Normal 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
34
controller/image.go
Normal 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
102
controller/network.go
Normal 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
95
controller/node.go
Normal 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
52
controller/profile.go
Normal 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
Reference in New Issue
Block a user