Skip to content
Snippets Groups Projects
Commit d3e7fe3f authored by Dmytro Bogatov's avatar Dmytro Bogatov :two_hearts:
Browse files

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
Branches
No related tags found
No related merge requests found
Pipeline #2486 failed
Showing
with 128 additions and 265 deletions
POSTGRES_DB=statussite
POSTGRES_USER=statususer
POSTGRES_PASSWORD=SomethingWeird15
# BRANCH TAG (DO NOT MODIFY)
...@@ -60,3 +60,5 @@ debian/build ...@@ -60,3 +60,5 @@ debian/build
test/appsettings.* test/appsettings.*
*lock.json *lock.json
docker-compose-*.yml
stages: stages:
- lint
- build - build
- test - test
- release - release
...@@ -6,6 +7,16 @@ stages: ...@@ -6,6 +7,16 @@ stages:
before_script: before_script:
- export DOTNET_TAG="$CI_BUILD_REF_NAME" - 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
build-docs: build-docs:
...@@ -24,8 +35,7 @@ build-ping: ...@@ -24,8 +35,7 @@ build-ping:
image: golang:alpine image: golang:alpine
stage: build stage: build
script: script:
- apk update - apk --update add bash
- apk add bash
- ./build.sh -f build-ping-server - ./build.sh -f build-ping-server
artifacts: artifacts:
expire_in: 90 min expire_in: 90 min
...@@ -54,13 +64,13 @@ build-app: ...@@ -54,13 +64,13 @@ build-app:
unit-test-app: unit-test-app:
stage: test stage: test
image: microsoft/dotnet:2.0.3-sdk image: microsoft/dotnet:2.0.3-sdk
dependencies: []
script: script:
- printf "{\"Version\":{\"GitHash\":\"%s\"}}" $CI_BUILD_REF > src/version.json - printf "{\"Version\":{\"GitHash\":\"%s\"}}" $CI_BUILD_REF > src/version.json
- cd test - cd test
- dotnet restore - dotnet restore
- dotnet build - dotnet build
- ./test.sh | tee tests.out - ./test.sh | tee /dev/stderr | grep 'Test Run Successful.'
- "cat tests.out | grep 'Test Run Successful.'"
tags: tags:
- docker - docker
...@@ -75,8 +85,8 @@ tidy-app: ...@@ -75,8 +85,8 @@ tidy-app:
- dotnet web.dll > /dev/null & - dotnet web.dll > /dev/null &
- sleep 15 - sleep 15
script: script:
- curl -Ls http://localhost:5555/ | tidy -e - curl -Ls http://localhost | tidy -e
- curl -Ls http://localhost:5555/home/metric/CpuLoad/the-source | tidy -e - curl -Ls http://localhost/home/metric/CpuLoad/the-source | tidy -e
tags: tags:
- docker - docker
...@@ -91,7 +101,7 @@ blc-app: ...@@ -91,7 +101,7 @@ blc-app:
- dotnet web.dll > /dev/null & - dotnet web.dll > /dev/null &
- sleep 15 - sleep 15
script: 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: tags:
- docker - docker
...@@ -101,10 +111,10 @@ blc-docs: ...@@ -101,10 +111,10 @@ blc-docs:
dependencies: dependencies:
- build-docs - build-docs
before_script: before_script:
- http-server documentation/out/ -p 8080 > /dev/null & - http-server documentation/out/ -p 80 > /dev/null &
- sleep 5 - sleep 5
script: 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: tags:
- docker - docker
...@@ -120,12 +130,11 @@ release-app-docs: ...@@ -120,12 +130,11 @@ release-app-docs:
- ./build.sh -f build-docker-images - ./build.sh -f build-docker-images
- docker login -u $DOCKER_USER -p $DOCKER_PASS - docker login -u $DOCKER_USER -p $DOCKER_PASS
- ./build.sh -f push-docker-images - ./build.sh -f push-docker-images
- cp src/appsettings.production.yml appsettings.yml.example - ./build.sh -f build-compose-files
artifacts: artifacts:
paths: paths:
- docker-compose.yml - docker-compose.yml
- .env.example - appsettings.production.yml
- appsettings.yml.example
tags: tags:
- shell - shell
... ...
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
"preLaunchTask": "build-web", "preLaunchTask": "build-web",
"justMyCode": false, "justMyCode": false,
"program": "${workspaceRoot}/src/web/bin/Debug/netcoreapp2.0/web.dll", "program": "${workspaceRoot}/src/web/bin/Debug/netcoreapp2.0/web.dll",
"args": [], "args": ["5555"],
"cwd": "${workspaceRoot}/src/web", "cwd": "${workspaceRoot}/src/web",
"externalConsole": false, "externalConsole": false,
"stopAtEntry": false, "stopAtEntry": false,
... ...
......
// Place your settings in this file to overwrite default and user settings. // 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 # Badges
You may put badges on your websites or in your markdown documents. 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 # Configuration
## Developer perspective ## Developer perspective
...@@ -26,9 +28,10 @@ Configuration is built in the `Startup` method and is available for [Dependency ...@@ -26,9 +28,10 @@ Configuration is built in the `Startup` method and is available for [Dependency
## User perspective ## User perspective
It is required to supply `appsettings.yml` file when launching the application with *docker-compose*. It is required to supply `appsettings.yml` file as a *docker secret* launching the application with `docker stack deploy`.
When [deploying with script](deployment/) it is possible to supply *example configuration* to get app up and running. Please, refer to [Deployment section](/deployment/) to download example config and supply it as docker secret.
Then user is free to change the configuration and restart the app. <!-- 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 ## Configuration specs
...@@ -49,7 +52,7 @@ Configuration spec: ...@@ -49,7 +52,7 @@ Configuration spec:
SecretKey: "asgfdk_876ajhsvdjh" # Secret key provided by Google SecretKey: "asgfdk_876ajhsvdjh" # Secret key provided by Google
GoogleAnalytics: GoogleAnalytics:
TrackingId: "UA-XXXXXXX-X" # Google Analytics tracking number (https://analytics.google.com) 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 Email: # Email settings
Enabled: true # If false, all messages sent to email service will be logged to STDOUT instead Enabled: true # If false, all messages sent to email service will be logged to STDOUT instead
ToEmail: "recipient@domain.com" # Email of the recipient ToEmail: "recipient@domain.com" # Email of the recipient
...@@ -99,9 +102,9 @@ Configuration spec: ...@@ -99,9 +102,9 @@ Configuration spec:
Enabled: true # If true, then the gaps in data will be periodically generated 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 Frequency: 10 # N, where once in N runs a gap is generated
DiscrepancyService: # Settings for discrepancy service of the app 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 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" Gaps: # Settings for type of discrepancy "Gap in data"
MaxDifference: 60 # Number of seconds multiplied by 1.5 to consider as gap MaxDifference: 60 # Number of seconds multiplied by 1.5 to consider as gap
Load: # Settings for type of discrepancy "High load" Load: # Settings for type of discrepancy "High load"
... ...
......
# Deployment # 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/). ### TL;DR
* 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`.
This will install `status-ctl` in your `/usr/bin/` directory and will create config files in `/etc/status-site/` directory. #!bash
* Launch the app. Run `status-ctl start`. # init swarm if necessary
docker swarm init
Great! The app is served on port 5555! # 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
### To update # 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
Update control tool the way you would update any other debian package. # deploy stack
`sudo apt-get update` and `sudo apt-get upgrade`. docker stack deploy --compose-file docker-compose.yml status
### What the tool can do # if you want to bind to port 80
docker service update status_nginx --publish-add 80:80
Run `status-ctl help` or `man status-ctl` to view the available options and commands. # if you want to join existing docker network
docker service update status_nginx --network-add my-overlay
## The old way # verify your deployment
docker stack services status
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.
#!bash ### Prerequisites
curl -Ls https://status.dbogatov.org/docs/deploy.sh | bash -s -- -e
!!! warning You need the following before you can deploy a stack into swarm.
This script does not require `sudo` privileges.
Nevertheless, it is recommended that you examine the script before running it.
!!! tip * Your docker node has to operate in [*swarm* mode](https://docs.docker.com/engine/swarm/).
You may use `-b feature-branch` to deploy a specific branch. * 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.
#!bash Here is the explanation of each of these prerequisites.
curl -Ls https://status.dbogatov.org/docs/deploy.sh | bash -s -- -b feature-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.
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`
!!! note !!! warning
The `e` parameter in `bash -s -- -e` specifies that you want to use example configuration. PostgreSQL database connection string is hardwired into the application.
Example configuration is conservative - most of the features are disabled, but is still enough for a basic operation of the app. It may be changed, though, by manually editing `appsettings.yml` and `docker-compose.yml`.
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. 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/).
## appsettings.yml and .env `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)
There are 4 mandatory files that need to be in the directory alongside with `docker-compose.yml`, so that the app can start. At this point, you are ready to deploy the stack!
`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).
POSTGRES_DB=statussite #!bash
POSTGRES_USER=statususer docker stack deploy --compose-file docker-compose.yml status
POSTGRES_PASSWORD=SomethingWeird15
DOTNET_TAG=master 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.
!!! warning #!bash
Environmental variables define database connection settings which you will use in `appsettings.yml`. docker service update status_nginx --publish-add 80:80
For example, for the above env variables, this would be an appropriate database connection string.
#!yml hl_lines="2" If you want to add stack to an existing docker network, run the following
Secrets:
ConnectionString: "User ID=statususer;Password=SomethingWeird15;Host=database;Port=5432;Database=statussite;Pooling=false;CommandTimeout=300;"
## Manual deployment #!bash
docker service update status_nginx --network-add my-overlay
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. You are all set!
Run `docker stack services status` to verify your deployment.
Manual deployment procedure is as follows: !!! tip
Debian package is under construction, which will automate these tasks for you.
* Download artifacts archive from [GitLab](https://git.dbogatov.org/dbogatov/status-site). ## Other deployment strategies (on you own risk)
* 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`.
Now, the app is served on `http://localhost:5555`. 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.
!!! summary It is also possible to run composition containers manually.
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 %}
articles/material/assets/images/favicon.ico

1.12 KiB

articles/material/assets/images/favicon.png

1.03 KiB

<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
This diff is collapsed.
!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
This diff is collapsed.
{% 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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment