Commit d3e7fe3f authored by Dmytro Bogatov's avatar Dmytro Bogatov 💕

Merge branch '47-use-docker-compose-3-0' into 'master'

Resolve "Use docker compose 3.0"

Closes #47

See merge request dbogatov/status-site!22
parents ea342164 c0f03513
Pipeline #2486 failed with stages
in 9 minutes and 29 seconds
POSTGRES_DB=statussite
POSTGRES_USER=statususer
POSTGRES_PASSWORD=SomethingWeird15
# BRANCH TAG (DO NOT MODIFY)
......@@ -60,3 +60,5 @@ debian/build
test/appsettings.*
*lock.json
docker-compose-*.yml
stages:
- lint
- build
- test
- release
......@@ -6,6 +7,16 @@ stages:
before_script:
- export DOTNET_TAG="$CI_BUILD_REF_NAME"
## LINT
cspell-docs:
image: dbogatov/docker-images:cspell-latest
stage: lint
script:
- ./build.sh -f test-docs-spell-ci
tags:
- docker
## BUILD
build-docs:
......@@ -24,8 +35,7 @@ build-ping:
image: golang:alpine
stage: build
script:
- apk update
- apk add bash
- apk --update add bash
- ./build.sh -f build-ping-server
artifacts:
expire_in: 90 min
......@@ -54,13 +64,13 @@ build-app:
unit-test-app:
stage: test
image: microsoft/dotnet:2.0.3-sdk
dependencies: []
script:
- printf "{\"Version\":{\"GitHash\":\"%s\"}}" $CI_BUILD_REF > src/version.json
- cd test
- dotnet restore
- dotnet build
- ./test.sh | tee tests.out
- "cat tests.out | grep 'Test Run Successful.'"
- ./test.sh | tee /dev/stderr | grep 'Test Run Successful.'
tags:
- docker
......@@ -75,8 +85,8 @@ tidy-app:
- dotnet web.dll > /dev/null &
- sleep 15
script:
- curl -Ls http://localhost:5555/ | tidy -e
- curl -Ls http://localhost:5555/home/metric/CpuLoad/the-source | tidy -e
- curl -Ls http://localhost | tidy -e
- curl -Ls http://localhost/home/metric/CpuLoad/the-source | tidy -e
tags:
- docker
......@@ -91,7 +101,7 @@ blc-app:
- dotnet web.dll > /dev/null &
- sleep 15
script:
- blc --filter-level 3 --input http://localhost:5555 -rog --exclude "*linkedin.*" --exclude "*authenticate*"
- blc --filter-level 3 --input http://localhost -rog --exclude "*linkedin.*" --exclude "*authenticate*" | tee /dev/stderr | grep 'Finished!' | tail -1 | grep '0 broken.'
tags:
- docker
......@@ -101,10 +111,10 @@ blc-docs:
dependencies:
- build-docs
before_script:
- http-server documentation/out/ -p 8080 > /dev/null &
- http-server documentation/out/ -p 80 > /dev/null &
- sleep 5
script:
- blc --filter-level 3 --input http://localhost:8080 -rog --exclude "*linkedin.*" --exclude "*doxygen.*" --exclude "*status.dbogatov.org*"
- blc --filter-level 3 --input http://localhost -rog --exclude "*linkedin.*" --exclude "*doxygen.*" --exclude "*status.dbogatov.org*" --exclude "*github.com*" --exclude "*git.dbogatov.org*" | tee /dev/stderr | grep 'Finished!' | tail -1 | grep '0 broken.'
tags:
- docker
......@@ -120,12 +130,11 @@ release-app-docs:
- ./build.sh -f build-docker-images
- docker login -u $DOCKER_USER -p $DOCKER_PASS
- ./build.sh -f push-docker-images
- cp src/appsettings.production.yml appsettings.yml.example
- ./build.sh -f build-compose-files
artifacts:
paths:
- docker-compose.yml
- .env.example
- appsettings.yml.example
- appsettings.production.yml
tags:
- shell
......
......@@ -15,7 +15,7 @@
"preLaunchTask": "build-web",
"justMyCode": false,
"program": "${workspaceRoot}/src/web/bin/Debug/netcoreapp2.0/web.dll",
"args": [],
"args": ["5555"],
"cwd": "${workspaceRoot}/src/web",
"externalConsole": false,
"stopAtEntry": false,
......
// Place your settings in this file to overwrite default and user settings.
{
"cSpell.words": [
"ASPNETCORE",
"Doxyfile",
"FQDN",
"Intelli",
"Notificator",
"Postgre",
"SMTP",
"SPAMing",
"appsettings",
"captcha",
"doxygen",
"gitlab",
"makerchip",
"mattermost",
"minified",
"mkdir",
"mkdocs",
"nginx",
"nuget",
"stylesheet",
"timestamps",
"transpiler",
"transpilers",
"typedoc",
"wwwroot"
]
}
<!-- cSpell:ignore cpuload -->
# Badges
You may put badges on your websites or in your markdown documents.
......
<!-- cSpell:ignore SDKJF 5432 SMDFKL sdahjdjhd _678 ajsdvbja asgfdk _876 ajhsvdjh yourdomain logmessage -->
# Configuration
## Developer perspective
......@@ -26,9 +28,10 @@ Configuration is built in the `Startup` method and is available for [Dependency
## User perspective
It is required to supply `appsettings.yml` file when launching the application with *docker-compose*.
When [deploying with script](deployment/) it is possible to supply *example configuration* to get app up and running.
Then user is free to change the configuration and restart the app.
It is required to supply `appsettings.yml` file as a *docker secret* launching the application with `docker stack deploy`.
Please, refer to [Deployment section](/deployment/) to download example config and supply it as docker secret.
<!-- When [deploying with script](deployment/) it is possible to supply *example configuration* to get app up and running. -->
<!-- Then user is free to change the configuration and restart the app. -->
## Configuration specs
......@@ -49,7 +52,7 @@ Configuration spec:
SecretKey: "asgfdk_876ajhsvdjh" # Secret key provided by Google
GoogleAnalytics:
TrackingId: "UA-XXXXXXX-X" # Google Analytics tracking number (https://analytics.google.com)
ConnectionString: "connection-strings" # Connection string to PostgreSQL database
ConnectionString: "connection-strings" # Connection string to PostgreSQL database NOT INTENDED TO BE MODIFIED
Email: # Email settings
Enabled: true # If false, all messages sent to email service will be logged to STDOUT instead
ToEmail: "recipient@domain.com" # Email of the recipient
......@@ -62,7 +65,7 @@ Configuration spec:
Security: "StartTls" # SMTP Security option (one of Auto, None, SslOnConnect, StartTls, StartTlsWhenAvailable)
Slack: # Slack settings
Enabled: true # If false, all messages sent to slack service will be logged to STDOUT instead
WebHook: "https://slack.com/webhooks/a78" # Slack webhook URL which uniquely defines chanel (https://api.slack.com/incoming-webhooks)
WebHook: "https://slack.com/webhooks/a78" # Slack web hook URL which uniquely defines chanel (https://api.slack.com/incoming-webhooks)
Data: # Static data put into DB during app initialization. Rarely override this section
PingSettings: # Array of servers to ping
- ServerUrl: "https://google.com" # URL to ping (FQDN required)
......@@ -76,8 +79,8 @@ Configuration spec:
- "Microsoft." # Example of such string. All messages from "Microsoft" are ignored
Guard: # This section defines protection settings for some API endpoints
Logging: # Protection of /api/logmessage endpoint from SPAMing
Requests: 10 # Number of requests to allow from a single source and category per timeframe
PerSeconds: 10 # Timeframe in seconds
Requests: 10 # Number of requests to allow from a single source and category per time frame
PerSeconds: 10 # Time frame in seconds
ServiceManager: # Settings for daemon part of the app
CacheService: # Settings for cache service of the app
Enabled: true # Whether to use the service
......@@ -99,9 +102,9 @@ Configuration spec:
Enabled: true # If true, then the gaps in data will be periodically generated
Frequency: 10 # N, where once in N runs a gap is generated
DiscrepancyService: # Settings for discrepancy service of the app
Enabled: ture # Whether to use the service
Enabled: true # Whether to use the service
Interval: 60 # How many seconds to wait between re-runs of the service
DataTimeframe: 800 # Number of seconds of data to consider counting from the time of running the service when looking for discrepancies
DataTimeFrame: 800 # Number of seconds of data to consider counting from the time of running the service when looking for discrepancies
Gaps: # Settings for type of discrepancy "Gap in data"
MaxDifference: 60 # Number of seconds multiplied by 1.5 to consider as gap
Load: # Settings for type of discrepancy "High load"
......
# Deployment
## The new way
## Deploy to swarm (preferred way)
Install status site official debian package - control tool.
Status site is designed with swarm in mind.
The preferred way to deploy the system is using `docker stack deploy` command.
* Make sure you have `docker` [installed](https://docs.docker.com/engine/installation/) and `docker-compose` [installed](https://docs.docker.com/compose/install/).
* Add `apt.dbogatov.org`'s key. Run `sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 7BAD7958`.
* Add `apt.dbogatov.org` repository. Run `sudo add-apt-repository "deb http://apt.dbogatov.org/ trusty main"`.
* Update package listings. Run `sudo apt-get update`.
* Install `status-ctl`. Run `sudo apt-get install status-ctl`.
### TL;DR
#!bash
This will install `status-ctl` in your `/usr/bin/` directory and will create config files in `/etc/status-site/` directory.
# init swarm if necessary
docker swarm init
# download and set configuration
curl -L -o appsettings.production.yml https://git.dbogatov.org/dbogatov/status-site/-/jobs/artifacts/master/raw/appsettings.production.yml?job=release-app-docs
docker secret create appsettings.production.yml appsettings.production.yml
* Launch the app. Run `status-ctl start`.
# download compose file
curl -L -o docker-compose.yml https://git.dbogatov.org/dbogatov/status-site/-/jobs/artifacts/master/raw/docker-compose.yml?job=release-app-docs
Great! The app is served on port 5555!
# deploy stack
docker stack deploy --compose-file docker-compose.yml status
### To update
# if you want to bind to port 80
docker service update status_nginx --publish-add 80:80
Update control tool the way you would update any other debian package.
`sudo apt-get update` and `sudo apt-get upgrade`.
# if you want to join existing docker network
docker service update status_nginx --network-add my-overlay
### What the tool can do
# verify your deployment
docker stack services status
Run `status-ctl help` or `man status-ctl` to view the available options and commands.
## The old way
### Prerequisites
Make sure you have [Docker](https://www.docker.com) and [Docker compose](https://docs.docker.com/compose/) installed.
Run the following command in a directory where you want your configuration files to be.
You need the following before you can deploy a stack into swarm.
#!bash
curl -Ls https://status.dbogatov.org/docs/deploy.sh | bash -s -- -e
* Your docker node has to operate in [*swarm* mode](https://docs.docker.com/engine/swarm/).
* You have to have one *secret* in your swarm - [app config](/configuration/).
* You have to have `docker-compose-yml` file which defines the stack.
* By default, the stack does not open up ports (eq. 80) because it is designed to be a part of an existing infrastructure.
You need to manually either open a port, or hook existing reverse proxy to the stack.
!!! warning
This script does not require `sudo` privileges.
Nevertheless, it is recommended that you examine the script before running it.
Here is the explanation of each of these prerequisites.
!!! tip
You may use `-b feature-branch` to deploy a specific branch.
In general, it does not hurt to convert a regular node to a swarm node (size 1 cluster).
General command is `docker swarm init`.
If you want a truly **highly available** multi-node cluster, you might want to setup a number of nodes.
Please, refer to [docker swarm documentation](https://docs.docker.com/engine/swarm/) for instructions.
#!bash
curl -Ls https://status.dbogatov.org/docs/deploy.sh | bash -s -- -b feature-branch
The stack requires one secret - [app config](/configuration/).
You may download up-to-date example config file [here](https://git.dbogatov.org/dbogatov/status-site/-/jobs/artifacts/master/raw/appsettings.production.yml?job=release-app-docs).
Please, refer to [Configuration section](/configuration/) for config explanation.
Once you have the config (eq. `appsettings.production.yml`), run this command `docker secret create appsettings.production.yml appsettings.production.yml`
!!! warning
PostgreSQL database connection string is hardwired into the application.
It may be changed, though, by manually editing `appsettings.yml` and `docker-compose.yml`.
The security relies on internal docker network created for the stack, so nobody can even access database from the outside.
See up-to-date connection string in [Configuration section](/configuration/).
!!! note
The `e` parameter in `bash -s -- -e` specifies that you want to use example configuration.
Example configuration is conservative - most of the features are disabled, but is still enough for a basic operation of the app.
If you want to change configuration, modify `appsettings.yml` and re-run the command without `-e` argument, otherwise it will override your changes to default example configuration.
`docker-compose.yml` is not intended to be modified.
Download latest version [here](https://git.dbogatov.org/dbogatov/status-site/-/jobs/artifacts/master/raw/docker-compose.yml?job=release-app-docs)
## appsettings.yml and .env
At this point, you are ready to deploy the stack!
There are 4 mandatory files that need to be in the directory alongside with `docker-compose.yml`, so that the app can start.
`appsettings.yml` is the main configuration file, see more in [Configuration](/configuration/).
`.env` file is simply a collection of environmental variables for composition.
Its content is self-explanatory, except for `DOTNET_TAG` which needs to point to the branch you want to use (*master* by default).
#!bash
docker stack deploy --compose-file docker-compose.yml status
POSTGRES_DB=statussite
POSTGRES_USER=statususer
POSTGRES_PASSWORD=SomethingWeird15
If you want to serve the website on the node where you are deploying the stack, open up ports for **nginx** service of the stack **after** you deploy the stack.
DOTNET_TAG=master
#!bash
docker service update status_nginx --publish-add 80:80
!!! warning
Environmental variables define database connection settings which you will use in `appsettings.yml`.
For example, for the above env variables, this would be an appropriate database connection string.
If you want to add stack to an existing docker network, run the following
#!yml hl_lines="2"
Secrets:
ConnectionString: "User ID=statususer;Password=SomethingWeird15;Host=database;Port=5432;Database=statussite;Pooling=false;CommandTimeout=300;"
#!bash
docker service update status_nginx --network-add my-overlay
## Manual deployment
You are all set!
Run `docker stack services status` to verify your deployment.
Application is packaged as a collection of docker images with the `docker-compose.yml` file, which knows how to orchestrate those images, and a couple of config files.
!!! tip
Debian package is under construction, which will automate these tasks for you.
Manual deployment procedure is as follows:
## Other deployment strategies (on you own risk)
* Download artifacts archive from [GitLab](https://git.dbogatov.org/dbogatov/status-site).
* Extract its contents.
* Create `appsettings.yml` and `.env`, or use example files (renaming $1.example to $1).
* Stop app if it is running - `docker-compose -p statussite stop`.
* Pull app images - `docker-compose -p statussite pull`.
* Start app - `docker-compose -p statussite up -d --remove-orphans`.
It is possible to run stack as a *docker composition* (using the same `docker-compose.yml` file).
You might need to modify composition file a little.
Now, the app is served on `http://localhost:5555`.
It is also possible to run composition containers manually.
!!! summary
Here are the helpful links:
* [ASP.Core: Hosting and Deployment](https://docs.microsoft.com/en-us/aspnet/core/publishing/)
Finally, it is possible to build the app from source and serve it from the bare metal.
{% extends "base.html" %}
{% block content %}
<h1>404 - Not found</h1>
{% endblock %}
<svg xmlns="http://www.w3.org/2000/svg" width="352" height="448" viewBox="0 0 352 448" id="bitbucket"><path fill="currentColor" d="M203.75 214.75q2 15.75-12.625 25.25t-27.875 1.5q-9.75-4.25-13.375-14.5t-.125-20.5 13-14.5q9-4.5 18.125-3t16 8.875 6.875 16.875zm27.75-5.25q-3.5-26.75-28.25-41T154 165.25q-15.75 7-25.125 22.125t-8.625 32.375q1 22.75 19.375 38.75t41.375 14q22.75-2 38-21t12.5-42zM291.25 74q-5-6.75-14-11.125t-14.5-5.5T245 54.25q-72.75-11.75-141.5.5-10.75 1.75-16.5 3t-13.75 5.5T60.75 74q7.5 7 19 11.375t18.375 5.5T120 93.75Q177 101 232 94q15.75-2 22.375-3t18.125-5.375T291.25 74zm14.25 258.75q-2 6.5-3.875 19.125t-3.5 21-7.125 17.5-14.5 14.125q-21.5 12-47.375 17.875t-50.5 5.5-50.375-4.625q-11.5-2-20.375-4.5T88.75 412 70.5 401.125t-13-15.375q-6.25-24-14.25-73l1.5-4 4.5-2.25q55.75 37 126.625 37t126.875-37q5.25 1.5 6 5.75t-1.25 11.25-2 9.25zM350.75 92.5q-6.5 41.75-27.75 163.75-1.25 7.5-6.75 14t-10.875 10T291.75 288q-63 31.5-152.5 22-62-6.75-98.5-34.75-3.75-3-6.375-6.625t-4.25-8.75-2.25-8.5-1.5-9.875T25 232.75q-2.25-12.5-6.625-37.5t-7-40.375T5.5 118 0 78.5Q.75 72 4.375 66.375T12.25 57t11.25-7.5T35 43.875t12-4.625q31.25-11.5 78.25-16 94.75-9.25 169 12.5Q333 47.25 348 66.25q4 5 4.125 12.75t-1.375 13.5z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="416" height="448" viewBox="0 0 416 448" id="github"><path fill="currentColor" d="M160 304q0 10-3.125 20.5t-10.75 19T128 352t-18.125-8.5-10.75-19T96 304t3.125-20.5 10.75-19T128 256t18.125 8.5 10.75 19T160 304zm160 0q0 10-3.125 20.5t-10.75 19T288 352t-18.125-8.5-10.75-19T256 304t3.125-20.5 10.75-19T288 256t18.125 8.5 10.75 19T320 304zm40 0q0-30-17.25-51T296 232q-10.25 0-48.75 5.25Q229.5 240 208 240t-39.25-2.75Q130.75 232 120 232q-29.5 0-46.75 21T56 304q0 22 8 38.375t20.25 25.75 30.5 15 35 7.375 37.25 1.75h42q20.5 0 37.25-1.75t35-7.375 30.5-15 20.25-25.75T360 304zm56-44q0 51.75-15.25 82.75-9.5 19.25-26.375 33.25t-35.25 21.5-42.5 11.875-42.875 5.5T212 416q-19.5 0-35.5-.75t-36.875-3.125-38.125-7.5-34.25-12.875T37 371.5t-21.5-28.75Q0 312 0 260q0-59.25 34-99-6.75-20.5-6.75-42.5 0-29 12.75-54.5 27 0 47.5 9.875t47.25 30.875Q171.5 96 212 96q37 0 70 8 26.25-20.5 46.75-30.25T376 64q12.75 25.5 12.75 54.5 0 21.75-6.75 42 34 40 34 99.5z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500" id="gitlab"><path fill="currentColor" d="M93.667 473.347l90.684-279.097H2.983l90.684 279.097z" transform="translate(156.198 1.16)"/><path fill="currentColor" d="M221.333 473.345L130.649 194.25H3.557l217.776 279.095z" transform="translate(28.531 1.16)" opacity=".7"/><path fill="currentColor" d="M32 195.155L4.441 279.97a18.773 18.773 0 0 0 6.821 20.99l238.514 173.29L32 195.155z" transform="translate(.089 .256)" opacity=".5"/><path fill="currentColor" d="M2.667-84.844h127.092L75.14-252.942c-2.811-8.649-15.047-8.649-17.856 0L2.667-84.844z" transform="translate(29.422 280.256)"/><path fill="currentColor" d="M2.667 473.345L93.351 194.25h127.092L2.667 473.345z" transform="translate(247.198 1.16)" opacity=".7"/><path fill="currentColor" d="M221.334 195.155l27.559 84.815a18.772 18.772 0 0 1-6.821 20.99L3.557 474.25l217.777-279.095z" transform="translate(246.307 .256)" opacity=".5"/><path fill="currentColor" d="M130.667-84.844H3.575l54.618-168.098c2.811-8.649 15.047-8.649 17.856 0l54.618 168.098z" transform="translate(336.974 280.256)"/></svg>
\ No newline at end of file
!function(e,n,t){function r(e,n){return typeof e===n}function o(e){var n=x.className,t=C._config.classPrefix||"";if(b&&(n=n.baseVal),C._config.enableJSClass){var r=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(r,"$1"+t+"js$2")}C._config.enableClasses&&(n+=" "+t+e.join(" "+t),b?x.className.baseVal=n:x.className=n)}function i(e,n){if("object"==typeof e)for(var t in e)_(e,t)&&i(t,e[t]);else{var r=(e=e.toLowerCase()).split("."),s=C[r[0]];if(2==r.length&&(s=s[r[1]]),void 0!==s)return C;n="function"==typeof n?n():n,1==r.length?C[r[0]]=n:(!C[r[0]]||C[r[0]]instanceof Boolean||(C[r[0]]=new Boolean(C[r[0]])),C[r[0]][r[1]]=n),o([(n&&0!=n?"":"no-")+r.join("-")]),C._trigger(e,n)}return C}function s(e,n){return!!~(""+e).indexOf(n)}function f(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):b?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function a(){var e=n.body;return e||((e=f(b?"svg":"body")).fake=!0),e}function l(e,t,r,o){var i,s,l,u,p="modernizr",d=f("div"),c=a();if(parseInt(r,10))for(;r--;)(l=f("div")).id=o?o[r]:p+(r+1),d.appendChild(l);return i=f("style"),i.type="text/css",i.id="s"+p,(c.fake?c:d).appendChild(i),c.appendChild(d),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(n.createTextNode(e)),d.id=p,c.fake&&(c.style.background="",c.style.overflow="hidden",u=x.style.overflow,x.style.overflow="hidden",x.appendChild(c)),s=t(d,e),c.fake?(c.parentNode.removeChild(c),x.style.overflow=u,x.offsetHeight):d.parentNode.removeChild(d),!!s}function u(e){return e.replace(/([A-Z])/g,function(e,n){return"-"+n.toLowerCase()}).replace(/^ms-/,"-ms-")}function p(n,r){var o=n.length;if("CSS"in e&&"supports"in e.CSS){for(;o--;)if(e.CSS.supports(u(n[o]),r))return!0;return!1}if("CSSSupportsRule"in e){for(var i=[];o--;)i.push("("+u(n[o])+":"+r+")");return i=i.join(" or "),l("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"==getComputedStyle(e,null).position})}return t}function d(e){return e.replace(/([a-z])-([a-z])/g,function(e,n,t){return n+t.toUpperCase()}).replace(/^-/,"")}function c(e,n,o,i){function a(){u&&(delete z.style,delete z.modElem)}if(i=!r(i,"undefined")&&i,!r(o,"undefined")){var l=p(e,o);if(!r(l,"undefined"))return l}for(var u,c,h,m,v,g=["modernizr","tspan"];!z.style;)u=!0,z.modElem=f(g.shift()),z.style=z.modElem.style;for(h=e.length,c=0;c<h;c++)if(m=e[c],v=z.style[m],s(m,"-")&&(m=d(m)),z.style[m]!==t){if(i||r(o,"undefined"))return a(),"pfx"!=n||m;try{z.style[m]=o}catch(e){}if(z.style[m]!=v)return a(),"pfx"!=n||m}return a(),!1}function h(e,n){return function(){return e.apply(n,arguments)}}function m(e,n,t){var o;for(var i in e)if(e[i]in n)return!1===t?e[i]:(o=n[e[i]],r(o,"function")?h(o,t||n):o);return!1}function v(e,n,t,o,i){var s=e.charAt(0).toUpperCase()+e.slice(1),f=(e+" "+P.join(s+" ")+s).split(" ");return r(n,"string")||r(n,"undefined")?c(f,n,o,i):(f=(e+" "+j.join(s+" ")+s).split(" "),m(f,n,t))}function g(e,n,r){return v(e,t,t,n,r)}var y=[],w={_version:"3.3.1",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){y.push({name:e,fn:n,options:t})},addAsyncTest:function(e){y.push({name:null,fn:e})}},C=function(){};C.prototype=w,C=new C;var _,S=[],x=n.documentElement,b="svg"===x.nodeName.toLowerCase();!function(){var e={}.hasOwnProperty;_=r(e,"undefined")||r(e.call,"undefined")?function(e,n){return n in e&&r(e.constructor.prototype[n],"undefined")}:function(n,t){return e.call(n,t)}}(),w._l={},w.on=function(e,n){this._l[e]||(this._l[e]=[]),this._l[e].push(n),C.hasOwnProperty(e)&&setTimeout(function(){C._trigger(e,C[e])},0)},w._trigger=function(e,n){if(this._l[e]){var t=this._l[e];setTimeout(function(){var e;for(e=0;e<t.length;e++)(0,t[e])(n)},0),delete this._l[e]}},C._q.push(function(){w.addTest=i});var P=w._config.usePrefixes?"Moz O ms Webkit".split(" "):[];w._cssomPrefixes=P;var T={elem:f("modernizr")};C._q.push(function(){delete T.elem});var z={style:T.elem.style};C._q.unshift(function(){delete z.style});var j=w._config.usePrefixes?"Moz O ms Webkit".toLowerCase().split(" "):[];w._domPrefixes=j,w.testAllProps=v,w.testAllProps=g;var k=w.testStyles=l,E="CSS"in e&&"supports"in e.CSS,N="supportsCSS"in e;C.addTest("supports",E||N),C.addTest("csstransforms3d",function(){var e=!!g("perspective","1px",!0),n=C._config.usePrefixes;if(e&&(!n||"webkitPerspective"in x.style)){var t;C.supports?t="@supports (perspective: 1px)":(t="@media (transform-3d)",n&&(t+=",(-webkit-transform-3d)")),k("#modernizr{width:0;height:0}"+(t+="{#modernizr{width:7px;height:18px;margin:0;padding:0;border:0}}"),function(n){e=7===n.offsetWidth&&18===n.offsetHeight})}return e}),function(){var e,n,t,o,i,s;for(var f in y)if(y.hasOwnProperty(f)){if(e=[],(n=y[f]).name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(o=r(n.fn,"function")?n.fn():n.fn,i=0;i<e.length;i++)1===(s=e[i].split(".")).length?C[s[0]]=o:(!C[s[0]]||C[s[0]]instanceof Boolean||(C[s[0]]=new Boolean(C[s[0]])),C[s[0]][s[1]]=o),S.push((o?"":"no-")+s.join("-"))}}(),o(S),delete w.addTest,delete w.addAsyncTest;for(var q=0;q<C._q.length;q++)C._q[q]();e.Modernizr=C}(window,document);
\ No newline at end of file
{% import "partials/language.html" as lang %}
<!DOCTYPE html>
<html lang="{{ lang.t('language') }}" class="no-js">
<head>
{% block site_meta %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
{% if page and page.meta.description %}
<meta name="description" content="{{ page.meta.description | first }}">
{% elif config.site_description %}
<meta name="description" content="{{ config.site_description }}">
{% endif %}
{% if page.canonical_url %}
<link rel="canonical" href="{{ page.canonical_url }}">
{% endif %}
{% if page and page.meta.author %}
<meta name="author" content="{{ page.meta.author | first }}">
{% elif config.site_author %}
<meta name="author" content="{{ config.site_author }}">
{% endif %}
{% if config.site_favicon %}
<link rel="shortcut icon" href="{{ base_url }}/{{ config.site_favicon }}">
{% else %}
<link rel="shortcut icon" href="{{ base_url }}/assets/images/favicon.png">
{% endif %}
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-1.6.4">
{% endblock %}
{% block htmltitle %}
{% if page and page.meta.title %}
<title>{{ page.meta.title | first }}</title>
{% elif page and page.title and not page.is_homepage %}
<title>{{ page.title }} - {{ config.site_name }}</title>
{% else %}
<title>{{ config.site_name }}</title>
{% endif %}
{% endblock %}
{% block libs %}
<script src="{{ base_url }}/assets/javascripts/modernizr-1df76c4e58.js"></script>
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-e2807e330f.css">
{% if config.extra.palette %}
<link rel="stylesheet" href="{{ base_url }}/assets/stylesheets/application-f78e5cb881.palette.css">
{% endif %}
{% endblock %}
{% block fonts %}
{% if config.extra.font != false and config.extra.font != "none" %}
{% set text = config.extra.get("font", {}).text | default("Roboto") %}
{% set code = config.extra.get("font", {}).code
| default("Roboto Mono") %}
{% set font = text + ':300,400,400i,700|' + code | replace(' ', '+') %}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family={{ font }}">
<style>body,input{font-family:"{{ text }}","Helvetica Neue",Helvetica,Arial,sans-serif}code,kbd,pre{font-family:"{{ code }}","Courier New",Courier,monospace}</style>
{% endif %}
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
{% endblock %}
{% for path in extra_css %}
<link rel="stylesheet" href="{{ path }}">
{% endfor %}
{% block extrahead %}{% endblock %}
</head>
{% set palette = config.extra.get("palette", {}) %}
{% set primary = palette.primary | replace(" ", "-") | lower %}
{% set accent = palette.accent | replace(" ", "-") | lower %}
{% if primary or accent %}
<body data-md-color-primary="{{ primary }}" data-md-color-accent="{{ accent }}">
{% else %}
<body>
{% endif %}
<svg class="md-svg">
<defs>
{% set platform = config.extra.repo_icon or config.repo_url %}
{% if "github" in platform %}
{% include "assets/images/icons/github-1da075986e.svg" %}
{% elif "gitlab" in platform %}
{% include "assets/images/icons/gitlab-5ad3f9f9e5.svg" %}
{% elif "bitbucket" in platform %}
{% include "assets/images/icons/bitbucket-670608a71a.svg" %}
{% endif %}
</defs>
</svg>
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="drawer">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="search">
<label class="md-overlay" data-md-component="overlay" for="drawer"></label>
{% block header %}
{% include "partials/header.html" %}
{% endblock %}
<div class="md-container">
{% set feature = config.extra.get("feature", {}) %}
{% if feature.tabs %}
{% include "partials/tabs.html" %}
{% endif %}
<main class="md-main">
<div class="md-main__inner md-grid" data-md-component="container">
{% block site_nav %}
{% if nav %}
<div class="md-sidebar md-sidebar--primary" data-md-component="navigation">
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
{% include "partials/nav.html" %}
</div>
</div>
</div>
{% endif %}
{% if page.toc %}
<div class="md-sidebar md-sidebar--secondary" data-md-component="toc">
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
{% include "partials/toc.html" %}
</div>
</div>
</div>
{% endif %}
{% endblock %}
<div class="md-content">
<article class="md-content__inner md-typeset">
{% block content %}
{% if config.edit_uri %}
<a href="{{ page.edit_url }}" title="{{ lang.t('edit.link.title') }}" class="md-icon md-content__icon">edit</a>
{% endif %}
{% if not "\x3ch1" in page.content %}
<h1>{{ page.title | default(config.site_name, true)}}</h1>
{% endif %}
{{ page.content }}
{% block source %}
{% if page.meta.source %}
<h2 id="__source">{{ lang.t('meta.source') }}</h2>
{% set path = (page.meta.path | default([""]) | first) %}
{% for file in page.meta.source %}
<a href="{{ [config.repo_url, path, file] | join('/') }}" title="{{ file }}" class="md-source-file">
{{ file }}
</a>
{% endfor %}
{% endif %}
{% endblock %}
{% endblock %}
{% block disqus %}
{% if config.extra.disqus and not page.is_homepage %}
<h2 id="__comments">{{ lang.t('meta.comments') }}</h2>
{% include "partials/disqus.html" %}
{% endif %}
{% endblock %}
</article>
</div>
</div>
</main>
{% block footer %}
{% include "partials/footer.html" %}
{% endblock %}
</div>
{% block scripts %}
<script src="{{ base_url }}/assets/javascripts/application-06a3e72efd.js"></script>
<script>app.initialize({url:{base:"{{ base_url }}"}})</script>
{% for path in extra_javascript %}
<script src="{{ path }}"></script>
{% endfor %}
{% endblock %}
{% block analytics %}
{% if config.google_analytics %}
<script>!function(e,t,a,n,o,c,i){e.GoogleAnalyticsObject=o,e[o]=e[o]||function(){(e[o].q=e[o].q||[]).push(arguments)},e[o].l=1*new Date,c=t.createElement(a),i=t.getElementsByTagName(a)[0],c.async=1,c.src=n,i.parentNode.insertBefore(c,i)}(window,document,"script","https://www.google-analytics.com/analytics.js","ga"),ga("create","{{ config.google_analytics[0] }}","{{ config.google_analytics[1] }}"),ga("set","anonymizeIp",!0),ga("send","pageview");var links=document.getElementsByTagName("a");Array.prototype.map.call(links,function(e){e.host!=document.location.host&&e.addEventListener("click",function(){var t=e.getAttribute("data-md-action")||"follow";ga("send","event","outbound",t,e.href)})});var query=document.forms.search.query;query.addEventListener("blur",function(){if(this.value){var e=document.location.pathname;ga("send","pageview",e+"?q="+this.value)}})</script>
{% endif %}
{% endblock %}
</body>
</html>
{% extends "base.html" %}
<div id="disqus_thread"></div>
<script>
var disqus_config = function () {
this.page.url = "{{ page.canonical_url }}";
this.page.identifier =
"{{ page.canonical_url | replace(config.site_url, "") }}";
};
(function() {
var d = document, s = d.createElement("script");
s.src = "//{{ config.extra.disqus }}.disqus.com/embed.js";
s.setAttribute("data-timestamp", +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
{% import "partials/language.html" as lang %}
<footer class="md-footer">
{% if page.previous_page or page.next_page %}
<div class="md-footer-nav">
<nav class="md-footer-nav__inner md-grid">
{% if page.previous_page %}
<a href="{{ page.previous_page.url }}" title="{{ page.previous_page.title }}" class="md-flex md-footer-nav__link md-footer-nav__link--prev" rel="prev">
<div class="md-flex__cell md-flex__cell--shrink">
<i class="md-icon md-icon--arrow-back md-footer-nav__button"></i>
</div>
<div class="md-flex__cell md-flex__cell--stretch md-footer-nav__title">
<span class="md-flex__ellipsis">
<span class="md-footer-nav__direction">
{{ lang.t('footer.previous') }}
</span>
{{ page.previous_page.title }}
</span>
</div>
</a>
{% endif %}
{% if page.next_page %}
<a href="{{ page.next_page.url }}" title="{{ page.next_page.title }}" class="md-flex md-footer-nav__link md-footer-nav__link--next" rel="next">
<div class="md-flex__cell md-flex__cell--stretch md-footer-nav__title">
<span class="md-flex__ellipsis">
<span class="md-footer-nav__direction">
{{ lang.t('footer.next') }}
</span>
{{ page.next_page.title }}
</span>
</div>
<div class="md-flex__cell md-flex__cell--shrink">
<i class="md-icon md-icon--arrow-forward md-footer-nav__button"></i>
</div>
</a>
{% endif %}
</nav>
</div>
{% endif %}
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-footer-copyright">
{% if config.copyright %}
<div class="md-footer-copyright__highlight">
{{ config.copyright }}
</div>
{% endif %}
powered by
<a href="http://www.mkdocs.org" title="MkDocs">MkDocs</a>
and
<a href="http://squidfunk.github.io/mkdocs-material/" title="Material for MkDocs">
Material for MkDocs</a>
</div>
{% block social %}
{% include "partials/social.html" %}
{% endblock %}
</div>
</div>
</footer>
<header class="md-header" data-md-component="header">
<nav class="md-header-nav md-grid">
<div class="md-flex">
<div class="md-flex__cell md-flex__cell--shrink">
{% if config.extra.logo %}
<a href="{{ config.site_url | default(nav.homepage.url, true) }}" title="{{ config.site_name }}" class="md-logo md-header-nav__button">
<img src="{{ base_url }}/{{ config.extra.logo }}" width="24" height="24">
</a>
{% else %}
<a href="{{ config.site_url | default(nav.homepage.url, true) }}" title="{{ config.site_name }}" class="md-icon md-icon--home md-header-nav__button">
</a>
{% endif %}
</div>
<div class="md-flex__cell md-flex__cell--shrink">
<label class="md-icon md-icon--menu md-header-nav__button" for="drawer"></label>
</div>
<div class="md-flex__cell md-flex__cell--stretch">
<span class="md-flex__ellipsis md-header-nav__title">
{% block site_name %}
{% if page %}
{% for parent in page.ancestors %}
<span class="md-header-nav__parent">
{{ parent.title }}
</span>
{% endfor %}
{% endif %}
{{ page.title | default(config.site_name, true) }}
{% endblock %}
</span>
</div>
<div class="md-flex__cell md-flex__cell--shrink">
{% block search_box %}
<label class="md-icon md-icon--search md-header-nav__button" for="search"></label>
{% include "partials/search.html" %}
{% endblock %}
</div>
<div class="md-flex__cell md-flex__cell--shrink">
<div <