commit 4939206d33dceded6edf965b96248a4352eb0009 Author: Denis Redozubov Date: Wed Dec 9 16:20:24 2020 +0300 Initial open source release diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..cf2980d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* linguist-vendored +*.hs linguist-vendored=false +*.rs linguist-vendored=false diff --git a/.github/workflows/build_docker.yaml b/.github/workflows/build_docker.yaml new file mode 100644 index 0000000..567f818 --- /dev/null +++ b/.github/workflows/build_docker.yaml @@ -0,0 +1,63 @@ +name: Octopod Docker Image +on: + push: + branches: + - master + pull_request: + branches: + - master + - develop + +jobs: + build: + name: build-docker + runs-on: ubuntu-latest + timeout-minutes: 600 + steps: + - uses: actions/checkout@v2 + with: + repo: Aviora/dm + + - name: Install Nix + uses: cachix/install-nix-action@v12 + + - name: Login to Cachix + uses: cachix/cachix-action@v8 + with: + name: octopod + signingKey: ${{ secrets.CACHIX_SIGNING_KEY }} + + - name: Build Docker Images + run: | + # enable required features (see https://github.com/cachix/install-nix-action/issues/19) + mkdir -p ~/.config/nix + echo "system-features = kvm" >> ~/.config/nix/nix.conf + + # build docker images + ./build.sh build + + - name: Login to DockerHub + id: login-docker-hub + if: github.ref == 'refs/heads/master' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push Docker Images to DockerHub + if: github.ref == 'refs/heads/master' + run: | + # push docker images to DockerHub + image_name=`cat octo-docker | docker load | awk '{print $3}'` + docker tag $image_name typeable/octo:latest + docker push typeable/octo:latest + + image_name=`cat octopod-server-docker | docker load | awk '{print $3}'` + docker tag $image_name typeable/octopod:latest + docker push typeable/octopod:latest + + - name: Clean up + if: always() + continue-on-error: true + run: | + docker logout ${{ steps.login-docker-hub.outputs.registry }} diff --git a/.github/workflows/build_octo_cli.yaml b/.github/workflows/build_octo_cli.yaml new file mode 100644 index 0000000..125f5f9 --- /dev/null +++ b/.github/workflows/build_octo_cli.yaml @@ -0,0 +1,68 @@ +name: octo CLI +on: + push: + branches: + - master + pull_request: + branches: + - master + - develop + +jobs: + macOS: + name: Create macOS octo CLI Release (Stack) + runs-on: macos-latest + steps: + - uses: actions/checkout@v1 + - name: Cache stack dependencies + uses: actions/cache@v2 + with: + path: ~/.stack + key: octo-cli-stack-${{ runner.os }} + # TODO: Remove this step once https://github.com/actions/cache/issues/445 is resolved. + - name: Fix macOS cache bug + run: rm -rf ~/.stack/setup-exe-cache + - name: Build + run: stack build octo-cli --local-bin-path out --copy-bins + - uses: actions/upload-artifact@v2 + with: + name: octo-cli-macos + path: out/octo + linux: + name: Create Linux octo CLI Release (Nix) + runs-on: ubuntu-latest + timeout-minutes: 600 + steps: + - uses: actions/checkout@v1 + - uses: cachix/install-nix-action@v12 + - uses: cachix/cachix-action@v8 + with: + name: octopod + signingKey: "${{ secrets.CACHIX_SIGNING_KEY }}" + - name: Build + run: nix-build nix/octo.nix + - uses: actions/upload-artifact@v2 + with: + name: octo-cli-linux + path: result/bin/octo + release: + name: "Release" + if: github.ref == 'refs/heads/master' + runs-on: "ubuntu-latest" + needs: [macOS, linux] + steps: + - uses: actions/download-artifact@v2 + - name: Zip + run: | + chmod +x octo-cli-macos/octo + zip octo-cli-macos octo-cli-macos/octo + chmod +x octo-cli-linux/octo + zip octo-cli-linux octo-cli-linux/octo + - uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "latest" + prerelease: true + title: "Pre-Release" + files: | + *.zip diff --git a/.github/workflows/check_documentation.yaml b/.github/workflows/check_documentation.yaml new file mode 100644 index 0000000..9b80601 --- /dev/null +++ b/.github/workflows/check_documentation.yaml @@ -0,0 +1,23 @@ +name: Documentation +on: + push: + branches: + - master + pull_request: + schedule: + - cron: "0 0 * * *" + +jobs: + build: + name: Check Markdown Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Set up linter + run: | + yarn add remark-cli remark-lint-mdash-style https://github.com/typeable/remark-validate-links#anchors remark-preset-lint-recommended remark-lint-no-dead-urls + + - name: Run linter + run: | + yarn run remark -f -u validate-links -u remark-lint-mdash-style -u remark-lint-final-newline -u remark-lint-list-item-bullet-indent -u remark-lint-no-auto-link-without-protocol -u remark-lint-no-blockquote-without-marker -u remark-lint-ordered-list-marker-style -u remark-lint-no-literal-urls -u remark-lint-hard-break-spaces -u remark-lint-no-duplicate-definitions -u remark-lint-no-heading-content-indent -u remark-lint-no-inline-padding -u remark-lint-no-shortcut-reference-image -u remark-lint-no-shortcut-reference-link -u remark-lint-no-undefined-references -u remark-lint-no-unused-definitions -u remark-lint-no-dead-urls docs README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34d703a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.stack-work/ +package.yaml +result +dms-docker +*~ +dist-newstyle +octopod-config.json +frontend-result +octopod-css/node_modules diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml new file mode 100644 index 0000000..d1b73f7 --- /dev/null +++ b/.stylish-haskell.yaml @@ -0,0 +1,318 @@ +# stylish-haskell configuration file +# ================================== + +# The stylish-haskell tool is mainly configured by specifying steps. These steps +# are a list, so they have an order, and one specific step may appear more than +# once (if needed). Each file is processed by these steps in the given order. +steps: + # Format record definitions. This is disabled by default. + # + # You can control the layout of record fields. The only rules that can't be configured + # are these: + # + # - "|" is always aligned with "=" + # - "," in fields is always aligned with "{" + # - "}" is likewise always aligned with "{" + # + # - records: + # # How to format equals sign between type constructor and data constructor. + # # Possible values: + # # - "same_line" -- leave "=" AND data constructor on the same line as the type constructor. + # # - "indent N" -- insert a new line and N spaces from the beginning of the next line. + # equals: "same_line" + + # # How to format first field of each record constructor. + # # Possible values: + # # - "same_line" -- "{" and first field goes on the same line as the data constructor. + # # - "indent N" -- insert a new line and N spaces from the beginning of the data constructor + # first_field: "indent 2" + + # # How many spaces to insert between the column with "," and the beginning of the comment in the next line. + # field_comment: 2 + + # # # How many spaces to insert before "deriving" clause. Deriving clauses are always on separate lines. + # deriving: 2 + + # Align the right hand side of some elements. This is quite conservative + # and only applies to statements where each element occupies a single + # line. All default to true. + - simple_align: + cases: false + top_level_patterns: false + records: false + + # Import cleanup + - imports: + # There are different ways we can align names and lists. + # + # - global: Align the import names and import list throughout the entire + # file. + # + # - file: Like global, but don't add padding when there are no qualified + # imports in the file. + # + # - group: Only align the imports per group (a group is formed by adjacent + # import lines). + # + # - none: Do not perform any alignment. + # + # Default: global. + align: global + + # The following options affect only import list alignment. + # + # List align has following options: + # + # - after_alias: Import list is aligned with end of import including + # 'as' and 'hiding' keywords. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - with_alias: Import list is aligned with start of alias or hiding. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - with_module_name: Import list is aligned `list_padding` spaces after + # the module name. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # init, last, length) + # + # This is mainly intended for use with `pad_module_names: false`. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # init, last, length, scanl, scanr, take, drop, + # sort, nub) + # + # - new_line: Import list starts always on new line. + # + # > import qualified Data.List as List + # > (concat, foldl, foldr, head, init, last, length) + # + # Default: after_alias + list_align: after_alias + + # Right-pad the module names to align imports in a group: + # + # - true: a little more readable + # + # > import qualified Data.List as List (concat, foldl, foldr, + # > init, last, length) + # > import qualified Data.List.Extra as List (concat, foldl, foldr, + # > init, last, length) + # + # - false: diff-safe + # + # > import qualified Data.List as List (concat, foldl, foldr, init, + # > last, length) + # > import qualified Data.List.Extra as List (concat, foldl, foldr, + # > init, last, length) + # + # Default: true + pad_module_names: false + + # Long list align style takes effect when import is too long. This is + # determined by 'columns' setting. + # + # - inline: This option will put as much specs on same line as possible. + # + # - new_line: Import list will start on new line. + # + # - new_line_multiline: Import list will start on new line when it's + # short enough to fit to single line. Otherwise it'll be multiline. + # + # - multiline: One line per import list entry. + # Type with constructor list acts like single import. + # + # > import qualified Data.Map as M + # > ( empty + # > , singleton + # > , ... + # > , delete + # > ) + # + # Default: inline + long_list_align: new_line + + # Align empty list (importing instances) + # + # Empty list align has following options + # + # - inherit: inherit list_align setting + # + # - right_after: () is right after the module name: + # + # > import Vector.Instances () + # + # Default: inherit + empty_list_align: right_after + + # List padding determines indentation of import list on lines after import. + # This option affects 'long_list_align'. + # + # - : constant value + # + # - module_name: align under start of module name. + # Useful for 'file' and 'group' align settings. + # + # Default: 4 + list_padding: 2 + + # Separate lists option affects formatting of import list for type + # or class. The only difference is single space between type and list + # of constructors, selectors and class functions. + # + # - true: There is single space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable (fold, foldl, foldMap)) + # + # - false: There is no space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable(fold, foldl, foldMap)) + # + # Default: true + separate_lists: false + + # Space surround option affects formatting of import lists on a single + # line. The only difference is single space after the initial + # parenthesis and a single space before the terminal parenthesis. + # + # - true: There is single space associated with the enclosing + # parenthesis. + # + # > import Data.Foo ( foo ) + # + # - false: There is no space associated with the enclosing parenthesis + # + # > import Data.Foo (foo) + # + # Default: false + space_surround: false + + # Language pragmas + - language_pragmas: + # We can generate different styles of language pragma lists. + # + # - vertical: Vertical-spaced language pragmas, one per line. + # + # - compact: A more compact style. + # + # - compact_line: Similar to compact, but wrap each line with + # `{-#LANGUAGE #-}'. + # + # Default: vertical. + style: vertical + + # Align affects alignment of closing pragma brackets. + # + # - true: Brackets are aligned in same column. + # + # - false: Brackets are not aligned together. There is only one space + # between actual import and closing bracket. + # + # Default: true + align: false + + # stylish-haskell can detect redundancy of some language pragmas. If this + # is set to true, it will remove those redundant pragmas. Default: true. + remove_redundant: true + + # Language prefix to be used for pragma declaration, this allows you to + # use other options non case-sensitive like "language" or "Language". + # If a non correct String is provided, it will default to: LANGUAGE. + language_prefix: LANGUAGE + + # Replace tabs by spaces. This is disabled by default. + # - tabs: + # # Number of spaces to use for each tab. Default: 8, as specified by the + # # Haskell report. + # spaces: 8 + + # Remove trailing whitespace + - trailing_whitespace: {} + + # Squash multiple spaces between the left and right hand sides of some + # elements into single spaces. Basically, this undoes the effect of + # simple_align but is a bit less conservative. + - squash: {} + +# A common setting is the number of columns (parts of) code will be wrapped +# to. Different steps take this into account. +# +# Set this to null to disable all line wrapping. +# +# Default: 80. +columns: 80 + +# By default, line endings are converted according to the OS. You can override +# preferred format here. +# +# - native: Native newline format. CRLF on Windows, LF on other OSes. +# +# - lf: Convert to LF ("\n"). +# +# - crlf: Convert to CRLF ("\r\n"). +# +# Default: native. +newline: native + +# Sometimes, language extensions are specified in a cabal file or from the +# command line instead of using language pragmas in the file. stylish-haskell +# needs to be aware of these, so it can parse the file correctly. +# +# No language extensions are enabled by default. +language_extensions: + - BangPatterns + - ConstraintKinds + - CPP + - DataKinds + - DefaultSignatures + - DeriveFoldable + - DeriveFunctor + - DeriveGeneric + - DeriveTraversable + - DerivingVia + - DuplicateRecordFields + - ExistentialQuantification + - ExplicitNamespaces + - FlexibleInstances + - FunctionalDependencies + - GADTs + - GeneralizedNewtypeDeriving + - KindSignatures + - LambdaCase + - MultiParamTypeClasses + - MultiWayIf + - NamedFieldPuns + - NoMonadFailDesugaring + - NumDecimals + - OverloadedStrings + - PartialTypeSignatures + - PolyKinds + - QuantifiedConstraints + - QuasiQuotes + - RankNTypes + - RecordWildCards + - RecursiveDo + - RoleAnnotations + - ScopedTypeVariables + - StandaloneDeriving + - TemplateHaskell + - TupleSections + - TypeApplications + - TypeFamilies + - TypeInType + - TypeOperators + - UndecidableInstances + - ViewPatterns + - OverloadedLabels + +# Attempt to find the cabal file in ancestors of the current directory, and +# parse options (currently only language extensions) from that. +# +# Default: true +cabal: false diff --git a/Caddyfile b/Caddyfile new file mode 120000 index 0000000..7b9c1a3 --- /dev/null +++ b/Caddyfile @@ -0,0 +1 @@ +Caddyfile2 \ No newline at end of file diff --git a/Caddyfile1 b/Caddyfile1 new file mode 100644 index 0000000..f16a376 --- /dev/null +++ b/Caddyfile1 @@ -0,0 +1,42 @@ +localhost:8000 + +mime .css text/css + +rewrite /static/styles { + r (.*) + to /octopod-css/production/styles/{1} +} + +mime .js application/javascript + +rewrite /static/vendors { + r (.*) + to /octopod-css/production/vendors/{1} +} + +rewrite /static/scripts { + r (.*) + to /octopod-css/production/scripts/{1} +} + +rewrite /static/images { + r (.*) + to /octopod-css/production/images/{1} +} + +proxy /api localhost:3002 { + transparent +} + +mime .json application/json +rewrite /config.json /octopod-config.json + +rewrite /ghcjs /frontend-result/bin/frontend.jsexe/index.html + +proxy / localhost:3003 { + transparent + websocket + except /octopod-css/ /frontend-result/ /octopod-config.json +} + +log / stdout diff --git a/Caddyfile2 b/Caddyfile2 new file mode 100644 index 0000000..bb2d6dd --- /dev/null +++ b/Caddyfile2 @@ -0,0 +1,27 @@ +http://localhost:8000 + + +file_server + +@production { + path_regexp production /static/(.*) +} + +rewrite @production /octopod-css/production/{http.regexp.production.1} + +reverse_proxy /api localhost:3002 + +rewrite /config.json /octopod-config.json + +rewrite /ghcjs /frontend-result/bin/frontend.jsexe/index.html + +@3003 { + not path /octopod-css/* /frontend-result/* /octopod-config.json +} + +reverse_proxy @3003 localhost:3003 + +log { + output stdout + format single_field common_log +} diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..78868a2 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,3 @@ +# Changelog for octopod + +## Version 1.0 diff --git a/Development_guide.md b/Development_guide.md new file mode 100644 index 0000000..349f02c --- /dev/null +++ b/Development_guide.md @@ -0,0 +1,73 @@ +# Development guide + +## Git flow + +`master` contains the latest "release version" only. + +All development should be done in the `develop` branch. + +Feature PRs are created to the `develop` branch and merged with all commits **squashed**. This leads to us having every commit in the `develop` branch corresponds to exactly one feature or bug fix. + +When a release is ready, the `develop` branch is merged into the `master` branch using **rebase and merge**. This makes the `master` branch have every commit be a feature or bug fix. Merging to master triggers a CI script that collects all commits since the last merge and creates a new release with a change log of all commits. + +## Building + +### Nix Installation + +Everything is built with [nix](https://nixos.org). To build the project you will need to install it. + +```bash +curl https://nixos.org/nix/install | sh +``` + +### Nix cache + +#### Reflex platform cache + +To speedup initial project builds you will want to set up the Reflex Platform binary nix cache – append the following to `/etc/nix/nix.conf`: + +``` +binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org +binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI= +binary-caches-parallel-connections = 40 +``` + +#### Octopod cache + +The Octopod cache will also be useful to speed up builds: + +1. Install [Cachix](https://cachix.org): + + ```bash + nix-env -iA cachix -f https://cachix.org/api/v1/install + ``` +2. Add cache: + ```bash + cachix use octopod + ``` + +## Development + +We have written a `Makefile` with common targets used during development. + +### Building + +- `build-backend` – builds a release backend executable. +- `build-octo-cli` – builds a release octo CLI executable. NOTE: this is not the octo CLI executable that is used for distribution but the dependencies are close enough for development purposes. +- `build-frontend` – build the frontend release. + +### Development + +For development, we have set up `ghcid` commands that rebuild the project every time you make a change. The targets should self-explanatory: + +- `ghcid-backend` +- `ghcid-cli` +- `ghcid-frontend` + +### Frontend proxy + +The frontend should be accessed through a proxy. We have set up [caddy](https://caddyserver.com) configs to ease development. You will need place an `octopod-config.json` file at the root of the repository containing a [config](../../charts/octopod/templates/octopod-nginx-configmap.yaml#L15-L20). `app_auth` can be an arbitrary string – it will not affect anything when running locally. + +### Stack + +For convenience, the repo currently also contains a `stack.yaml` that can be used for development. It is only used to build the macOS octo CLI release but supports building both octo CLI and the _Octopod Server_ in an environment close enough to the release environment to be useful during development if you prefer stack. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..423b0c9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright Typeable LLC (c) 2020 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..52abed3 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +.PHONY: build-backend build-octo-cli build-frontend docs backend-docs frontend-docs repl shell shell-ghcjs ghcid ghcid-cli ghcid-frontend push-octopod + +build-backend: + nix-build . -A ghc.octopod-backend + +build-octo-cli: + nix-build . -A ghc.octo-cli + +build-frontend: + nix-build . -A ghcjs.octopod-frontend -o frontend-result + +docs: backend-docs frontend-docs + +backend-docs: + nix-build . -A ghc.octopod-backend.doc + +frontend-docs: + nix-build . -A ghcjs.octopod-frontend.doc + +repl: + nix-shell . -A shells.ghc --run "cabal repl lib:octopod-backend" + +shell: + nix-shell . -A shells.ghc + +shell-ghcjs: + nix-shell . -A shells.ghcjs + +ghcid-backend: + nix-shell . -A shells.ghc --run 'ghcid -c "cabal new-repl octopod-backend"' + +ghcid-cli: + nix-shell . -A shells.ghc --run 'ghcid -c "cabal new-repl octo-cli"' + +ghcid-frontend: + nix-shell . -A shells.ghc --run 'ghcid -c "cabal new-repl octopod-frontend -fdevelopment" --test 'Main.main'' + +push-octopod: + ./build.sh build-and-push latest diff --git a/README.md b/README.md new file mode 100644 index 0000000..378591a --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# 🐙 Octopod ![Octopod Docker Image](https://github.com/typeable/octopod/workflows/Octopod%20Docker%20Image/badge.svg?branch=master) ![octo CLI](https://github.com/typeable/octopod/workflows/octo%20CLI/badge.svg?branch=master) ![Documentation](https://github.com/typeable/octopod/workflows/Documentation/badge.svg?branch=master) + +_Octopod_ is a fully open-source self-hosted solution for managing multiple deployments in a _Kubernetes_ cluster with a user-friendly web interface. Managing deployments does not require any technical expertise. + +We created _Octopod_ because we believe that everything we release should be rigorously tested, however, such desires greatly [complicate the development workflow](docs/en/PM_case_study.md) leading to longer release cycles. We use _Octopod_ to mitigate the downsides of rigorously testing each feature by deploying every single change we make to a separate staging environment allowing QA to investigate each feature independently and in parallel. + +## 🖥 Demo + +

+ +## 📑 Documentation + +### 🔭 High-level notes +- [🐙 Overview](docs/en/Overview.md) +- [🧑‍🔬 Project managment case study](docs/en/PM_case_study.md) +- [🧑‍💻 Technical case study](docs/en/Tech_case_study.md) + +### 🛠️ Technical documentation +- [🏗 Technical architecture](docs/en/Technical_architecture.md) [[RU](docs/ru/Technical_architecture.md)] +- [⚙️ Control script guide](docs/en/Control_scripts.md) [[RU](docs/ru/Control_scripts.md)] +- [🔧🐙 Octopod deployment guide](docs/en/Octopod_deployment_guide.md) [[RU](docs/ru/Octopod_deployment_with_K8S.md)] +- [🔧🚀 Helm-based Octopod project setup](docs/en/Helm-based_deployment_guide.md) [[RU](docs/ru/Helm-based_deployment_guide.md)] +- [🐙🎛 octo CLI user guide](docs/en/Octo_user_guide.md) [[RU](docs/ru/Octo_user_guide.md)] +- [🤖 CI integration](docs/en/Integration.md) +- [🔒 Octopod security model](docs/en/Security_model.md) [[RU](docs/ru/Security_model.md)] + +## ℹ️ FAQ + +### How long does it take to set up _Octopod_? + +The longest part of setting up _Octopod_ for your project will probably be writing [_Control Scripts_](docs/en/Control_scripts.md). In total you should be able to get things running in about a day. + +### Will _Octopod_ work with my project if it uses X? + +Yes. _Octopod_ is project-agnostic. If you can run your project in a Docker container, then you can use _Octopod_ with that project. + +### What do I need to know to set up Octopod? + +You need to understand the basics of _Kubernetes_ and be familiar with whatever hosting provider you will be using. There is no need to know any special language – you can write [_Control Scripts_](docs/en/Control_scripts.md) in whatever language you like. + +### Does _Octopod_ work with my CI? + +Yes. If you can run arbitrary executables in your CI, then you will be able to integrate it with _Octopod_. Integration basically consists of calling our _octo CLI_ tool to perform desired actions. You can find more detail in the [CI integration](docs/en/Integration.md) doc. + +### How come I can't see the deployment logs in Octopod web app? + +It's been excluded from the GUI because we don't have a good security story to accompany this feature yet. Some secret and credentials may leak to the project team using Octopod and, potentially, not everyone should have access to this data. + +### Why Haskell and Rust? + +We believe that there is a lot to be gained in programming in general by being able to statically ensure invariants in your code. One of the most practical ways of ensuring invariants is a good static type system. Haskell and Rust are both languages that have very strong type systems. This allows us to move fast without breaking things in the process. + +

diff --git a/Release_checklist.md b/Release_checklist.md new file mode 100644 index 0000000..f8d5b2f --- /dev/null +++ b/Release_checklist.md @@ -0,0 +1,18 @@ +# Release checklist + +1. Merge the `develop` branch into `master`. +2. CI will automatically create a new release in GitHub with _octo CLI_ and update the `latest` tag for both `octo` and `octopod`. Wait for CI to complete. +3. Edit the created release in GitHub to match the version you are releasing. + 1. Change the release name to the version being released. + 2. Uncheck "This is a pre-release" +4. Push the new release of `octo` and `octopod`. To do this run `./release.sh `. +5. Update the referenced tags in documentation +6. If there were changes to the examples: + 1. Build and push the new containers: + 1. octopod-web-app-example + 2. octopod-helm-example + 2. Create a new tag incrementing the integer version number of the tag: + 1. Pull the image (`docker pull`) + 2. Tag it with the new `v` (`docker tag`) + 3. Push the new tag (`docker push`) + 3. Update docs where the tags are referenced. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..f068c5f --- /dev/null +++ b/build.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +set -e + +build_octo_cli_docker_image() { + nix build nixpkgs.octo-cli-container \ + -I nixpkgs=nix \ + -o "$1" +} + +build_octopod_server_docker_image() { + nix build nixpkgs.octopod-server-container \ + --arg migrations "$1" \ + -I nixpkgs=nix \ + -o "$2" +} + +push_docker_images() { + outfile=latest-octopod-server-docker + for image_name in $octo_cli_docker $octopod_server_docker; do + image_type=$(echo "$image_name" | cut -d- -f1) + image=$(ls -ls "$image_name" | awk '{print $12}') + echo "size: $(du -sh $image)" + docker load --input "$image" | tee "$outfile" + nixcontainer=$(awk '{print $3}' $outfile) + docker tag "$nixcontainer" "typeable/${image_type}:$1" + docker push "typeable/${image_type}:$1" + echo "Published: ${image_type}:$1" + done + rm $outfile +} + +build_docker_images() { + build_octo_cli_docker_image "$octo_cli_docker" + build_octopod_server_docker_image "$migrations" "$octopod_server_docker" +} + +export tag=$(git rev-parse HEAD) +export migrations="./migrations" +export octo_cli_docker="octo-docker" +export octopod_server_docker="octopod-server-docker" + +case "$1" in + build-and-push) + echo "$1 mode" + + if test -z "$2" + then + echo "Please provide a tag to upload to" + exit 1 + fi + + build_docker_images + push_docker_images $2 + ;; + build) + echo "$1 mode" + build_docker_images + ;; + *) + echo "usage:" + echo " $0 build Builds the docker images." + echo " $0 build-and-push Builds the docker images and uploads it to Docker Hub under the tag ." + exit 1 + ;; +esac diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..1324643 --- /dev/null +++ b/cabal.project @@ -0,0 +1,6 @@ +packages: + octo-cli/ + octopod-api/ + octopod-backend/ + octopod-common/ + octopod-frontend/ diff --git a/charts/cert-control/.helmignore b/charts/cert-control/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/charts/cert-control/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/cert-control/Chart.yaml b/charts/cert-control/Chart.yaml new file mode 100644 index 0000000..3b1e7df --- /dev/null +++ b/charts/cert-control/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: cert-control +version: 0.1.0 diff --git a/charts/cert-control/templates/cert-control-clusterrole.yaml b/charts/cert-control/templates/cert-control-clusterrole.yaml new file mode 100644 index 0000000..1f7acd2 --- /dev/null +++ b/charts/cert-control/templates/cert-control-clusterrole.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cert-control-clusterrole +rules: + - apiGroups: ["cert-manager.io"] + resources: ["certificates"] + verbs: ["list", "delete", "deletecollection"] diff --git a/charts/cert-control/templates/cert-control-rolebinding.yaml b/charts/cert-control/templates/cert-control-rolebinding.yaml new file mode 100644 index 0000000..f72f157 --- /dev/null +++ b/charts/cert-control/templates/cert-control-rolebinding.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Values.serviceaccount }}-cert-control-rolebinding + namespace: {{ .Values.namespace }} +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: cert-control-clusterrole +subjects: + - kind: ServiceAccount + name: {{ .Values.serviceaccount }} + namespace: {{ .Values.octopod_namespace | default .Values.namespace }} diff --git a/charts/cert-control/values.yaml b/charts/cert-control/values.yaml new file mode 100644 index 0000000..a384640 --- /dev/null +++ b/charts/cert-control/values.yaml @@ -0,0 +1,3 @@ +namespace: deployment +octopod_namespace: octopod +serviceaccount: octopod diff --git a/charts/helm-access/.helmignore b/charts/helm-access/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/charts/helm-access/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/helm-access/Chart.yaml b/charts/helm-access/Chart.yaml new file mode 100644 index 0000000..64371f8 --- /dev/null +++ b/charts/helm-access/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: helm-access +version: 0.1.0 diff --git a/charts/helm-access/templates/helm-clusterrole.yaml b/charts/helm-access/templates/helm-clusterrole.yaml new file mode 100644 index 0000000..95a0a9d --- /dev/null +++ b/charts/helm-access/templates/helm-clusterrole.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: helm-clusterrole +rules: + - apiGroups: [""] + resources: ["pods/portforward"] + verbs: ["create"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["list", "get"] diff --git a/charts/helm-access/templates/helm-clusterrolebinding.yaml b/charts/helm-access/templates/helm-clusterrolebinding.yaml new file mode 100644 index 0000000..0330546 --- /dev/null +++ b/charts/helm-access/templates/helm-clusterrolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.serviceaccount }}-helm-clusterrolebinding +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: helm-clusterrole +subjects: + - kind: ServiceAccount + name: {{ .Values.serviceaccount }} + namespace: {{ .Values.namespace }} diff --git a/charts/helm-access/values.yaml b/charts/helm-access/values.yaml new file mode 100644 index 0000000..c0ab832 --- /dev/null +++ b/charts/helm-access/values.yaml @@ -0,0 +1,2 @@ +namespace: octopod +serviceaccount: octopod diff --git a/charts/kubedog-access/Chart.yaml b/charts/kubedog-access/Chart.yaml new file mode 100644 index 0000000..64371f8 --- /dev/null +++ b/charts/kubedog-access/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: helm-access +version: 0.1.0 diff --git a/charts/kubedog-access/templates/kubedog-clusterrole.yaml b/charts/kubedog-access/templates/kubedog-clusterrole.yaml new file mode 100644 index 0000000..732482a --- /dev/null +++ b/charts/kubedog-access/templates/kubedog-clusterrole.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kubedog-clusterrole +rules: + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["list", "watch"] + - apiGroups: ["apps"] + resources: ["statefulsets"] + verbs: ["list", "watch"] + - apiGroups: ["apps"] + resources: ["replicasets"] + verbs: ["list", "watch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["list"] diff --git a/charts/kubedog-access/templates/kubedog-clusterrolebinding.yaml b/charts/kubedog-access/templates/kubedog-clusterrolebinding.yaml new file mode 100644 index 0000000..a75b465 --- /dev/null +++ b/charts/kubedog-access/templates/kubedog-clusterrolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Values.serviceaccount }}-kubedog-clusterrolebinding +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: kubedog-clusterrole +subjects: + - kind: ServiceAccount + name: {{ .Values.serviceaccount }} + namespace: {{ .Values.namespace }} diff --git a/charts/kubedog-access/values.yaml b/charts/kubedog-access/values.yaml new file mode 100644 index 0000000..c0ab832 --- /dev/null +++ b/charts/kubedog-access/values.yaml @@ -0,0 +1,2 @@ +namespace: octopod +serviceaccount: octopod diff --git a/charts/octopod-infra/.helmignore b/charts/octopod-infra/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/charts/octopod-infra/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/octopod-infra/Chart.yaml b/charts/octopod-infra/Chart.yaml new file mode 100644 index 0000000..448d56f --- /dev/null +++ b/charts/octopod-infra/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: octopod-infra +version: 0.1.0 diff --git a/charts/octopod-infra/templates/postgres-configmap.yaml b/charts/octopod-infra/templates/postgres-configmap.yaml new file mode 100644 index 0000000..52a1626 --- /dev/null +++ b/charts/octopod-infra/templates/postgres-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-postgres-config + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Release.Name }}-postgres +data: + POSTGRES_DB: {{ .Values.postgres_db | default .Release.Name }} + POSTGRES_USER: {{ .Values.postgres_user | default "postgres" }} + POSTGRES_PASSWORD: {{ .Values.postgres_password | default "password" }} diff --git a/charts/octopod-infra/templates/postgres-service.yaml b/charts/octopod-infra/templates/postgres-service.yaml new file mode 100644 index 0000000..5bd6db7 --- /dev/null +++ b/charts/octopod-infra/templates/postgres-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-postgres + namespace: {{ .Release.Namespace }} + labels: + name: {{ .Release.Name }}-postgres +spec: + selector: + app: {{ .Release.Name }}-postgres + clusterIP: None + ports: + - port: 5432 + name: postgres diff --git a/charts/octopod-infra/templates/postgres-statefulset.yaml b/charts/octopod-infra/templates/postgres-statefulset.yaml new file mode 100644 index 0000000..1ed718f --- /dev/null +++ b/charts/octopod-infra/templates/postgres-statefulset.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ .Release.Name }}-postgres + namespace: {{ .Release.Namespace }} +spec: + serviceName: {{ .Release.Name }}-postgres + replicas: 1 + selector: + matchLabels: + app: {{ .Release.Name }}-postgres + template: + metadata: + labels: + app: {{ .Release.Name }}-postgres + spec: + nodeSelector: + role: {{ .Values.nodeselector }} + terminationGracePeriodSeconds: 120 + containers: + - name: postgres + image: postgres:10 + envFrom: + - configMapRef: + name: {{ .Release.Name }}-postgres-config + resources: + requests: + cpu: {{ .Values.requests.cpu }} + memory: {{ .Values.requests.memory }} + limits: + cpu: {{ .Values.limits.cpu }} + memory: {{ .Values.limits.memory }} + ports: + - containerPort: 5432 + name: postgredb + volumeMounts: + - name: postgredb + mountPath: /var/lib/postgresql/data + subPath: postgres + volumeClaimTemplates: + - metadata: + name: postgredb + labels: + app: {{ .Release.Name }}-postgres + spec: + accessModes: + - "ReadWriteOnce" + storageClassName: {{ .Values.storage_class | default "default" }} + resources: + requests: + storage: {{ .Values.storage_size }} diff --git a/charts/octopod-infra/values.yaml b/charts/octopod-infra/values.yaml new file mode 100644 index 0000000..dd18fed --- /dev/null +++ b/charts/octopod-infra/values.yaml @@ -0,0 +1,20 @@ +global: + image_prefix: + image: default + image_tag: + +namespace: octopod +nodeselector: stand + +nodeselector: stand +namespace: octopod +postgres_db: octopod +postgres_user: octopod +postgres_password: octopod +storage_size: 1Gi +requests: + cpu: 0.2 + memory: 256Mi +limits: + cpu: 0.2 + memory: 512Mi diff --git a/charts/octopod/.helmignore b/charts/octopod/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/charts/octopod/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/octopod/Chart.yaml b/charts/octopod/Chart.yaml new file mode 100644 index 0000000..2ffc975 --- /dev/null +++ b/charts/octopod/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: octopod +version: 0.1.0 diff --git a/charts/octopod/templates/_helpers.tpl b/charts/octopod/templates/_helpers.tpl new file mode 100644 index 0000000..77f70f8 --- /dev/null +++ b/charts/octopod/templates/_helpers.tpl @@ -0,0 +1,7 @@ +{{/* +Set dbname +*/}} +{{- define "dbname" -}} +{{- $dbname_release := .Release.Name | replace "." "_" | replace "-" "_" -}} +{{- .Values.dbname | default $dbname_release }} +{{- end -}} diff --git a/charts/octopod/templates/clean-archive-cronjob.yaml b/charts/octopod/templates/clean-archive-cronjob.yaml new file mode 100644 index 0000000..2f05bd9 --- /dev/null +++ b/charts/octopod/templates/clean-archive-cronjob.yaml @@ -0,0 +1,47 @@ +apiVersion: batch/v1beta1 +kind: CronJob +metadata: + name: {{ .Release.Name }}-clean-archive-cronjob + namespace: {{ .Values.namespace }} +spec: + schedule: "0 */1 * * *" + jobTemplate: + spec: + template: + metadata: + labels: + app: {{ .Release.Name }} + annotations: + checksum/config: "{{ .Values.global.deploy_checksum }}" + spec: + nodeSelector: + role: {{ .Values.nodeselector }} + containers: + - name: octo + image: {{ .Values.global.image_prefix }}/{{ .Values.global.octo_image }}:{{ .Values.global.image_tag }} + command: + - /app/octo + args: + - clean-archive + env: + - name: OCTOPOD_URL + value: https://{{ .Values.power_app_domain }}:443 + volumeMounts: + - name: certs + mountPath: /cert.pem + subPath: client_cert.pem + - name: certs + mountPath: /key.pem + subPath: client_key.pem + resources: + requests: + cpu: 0.1 + memory: 256Mi + limits: + cpu: 0.1 + memory: 512Mi + restartPolicy: Never + volumes: + - name: certs + configMap: + name: octopod-certs diff --git a/charts/octopod/templates/octopod-app-ingress.yaml b/charts/octopod/templates/octopod-app-ingress.yaml new file mode 100644 index 0000000..b123832 --- /dev/null +++ b/charts/octopod/templates/octopod-app-ingress.yaml @@ -0,0 +1,32 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-app-nginx-ingress + namespace: {{ .Values.namespace }} + annotations: + kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" + cert-manager.io/issuer: "{{ .Release.Name }}-certs" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-next-upstream: "http_502 error timeout" + nginx.ingress.kubernetes.io/auth-secret: octopod-basic-auth + nginx.ingress.kubernetes.io/auth-secret-type: auth-file + nginx.ingress.kubernetes.io/auth-type: basic + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "https://{{ .Values.domain }}" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, PATCH, OPTIONS" +spec: + tls: + - hosts: + - {{ .Values.app_domain }} + secretName: {{ .Release.Name }}-app-tls + rules: + - host: {{ .Values.app_domain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }} + servicePort: 81 diff --git a/charts/octopod/templates/octopod-configmap.yaml b/charts/octopod/templates/octopod-configmap.yaml new file mode 100644 index 0000000..dd73802 --- /dev/null +++ b/charts/octopod/templates/octopod-configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-config + namespace: {{ .Values.namespace }} +data: + PROJECT_NAME: {{ .Values.project_name }} + BASE_DOMAIN: {{ .Values.base_domain }} + NAMESPACE: {{ .Values.target_namespace }} + STATUS_UPDATE_TIMEOUT: "{{ .Values.status_update_timeout }}" + ARCHIVE_RETENTION: "1209600" + CREATION_COMMAND: /utils/create + UPDATE_COMMAND: /utils/update + ARCHIVE_COMMAND: /utils/archive + CHECKING_COMMAND: /utils/check + CLEANUP_COMMAND: /utils/cleanup + ARCHIVE_CHECKING_COMMAND: /utils/archive_check + TAG_CHECKING_COMMAND: /utils/tag_check + INFO_COMMAND: /utils/info diff --git a/charts/octopod/templates/octopod-deployment.yaml b/charts/octopod/templates/octopod-deployment.yaml new file mode 100644 index 0000000..c64609f --- /dev/null +++ b/charts/octopod/templates/octopod-deployment.yaml @@ -0,0 +1,150 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + namespace: {{ .Values.namespace }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + name: {{ .Release.Name }} + labels: + app: {{ .Release.Name }} + annotations: + checksum/config: "{{ .Values.global.deploy_checksum }}" + spec: + serviceAccountName: {{ .Values.service_account }} + nodeSelector: + role: {{ .Values.nodeselector }} + terminationGracePeriodSeconds: 600 + initContainers: + - name: copy-utils + image: {{ .Values.global.utils_image_prefix }}/{{ .Values.global.utils_image }}:{{ .Values.global.utils_image_tag }} + command: + - sh + - -c + - 'cp /utils/* /copy/' + volumeMounts: + - name: utils + mountPath: /copy + + - name: init + image: {{ .Values.global.image_prefix }}/{{ .Values.global.image }}:{{ .Values.global.image_tag }} + command: + - sh + - -c + - '/utils/init' + securityContext: + runAsGroup: 1000 + runAsUser: 1000 + volumeMounts: + - name: home + mountPath: /home/octopod + - name: utils + mountPath: /utils + + - name: copy-www + image: {{ .Values.global.image_prefix }}/{{ .Values.global.image }}:{{ .Values.global.image_tag }} + command: + - sh + - -c + - 'cp -a /www/* /copy/' + volumeMounts: + - name: www + mountPath: /copy + + containers: + - name: main + image: {{ .Values.global.image_prefix }}/{{ .Values.global.image }}:{{ .Values.global.image_tag }} + ports: + - containerPort: {{ .Values.port }} + protocol: TCP + - containerPort: {{ .Values.ui_port }} + protocol: TCP + args: + - "--port" + - "{{ .Values.port }}" + - "--ui-port" + - "{{ .Values.ui_port }}" + - "--ws-port" + - "{{ .Values.ws_port }}" + - "--db" + - "host='{{ .Values.pg_host }}' port=5432 user='octopod' password='octopod'" + - "--db-pool-size" + - "10" + - "--tls-cert-path" + - "/tls/server_cert.pem" + - "--tls-key-path" + - "/tls/server_key.pem" + - "--tls-store-path" + - "/tls_store" + envFrom: + - configMapRef: + name: {{ .Release.Name }}-config + securityContext: + runAsGroup: 1000 + runAsUser: 1000 + volumeMounts: + - name: home + mountPath: /home/octopod + - name: utils + mountPath: /utils + - name: certs + mountPath: /tls/server_cert.pem + subPath: server_cert.pem + - name: certs + mountPath: /tls/server_key.pem + subPath: server_key.pem + - name: certs + mountPath: /tls_store/server_cert.pem + subPath: server_cert.pem + resources: + requests: + cpu: 0.2 + memory: 256Mi + limits: + cpu: 0.2 + memory: 512Mi + readinessProbe: + httpGet: + port: {{ .Values.ui_port }} + path: /api/v1/ping + periodSeconds: 20 + livenessProbe: + httpGet: + port: {{ .Values.ui_port }} + path: /api/v1/ping + initialDelaySeconds: 15 + periodSeconds: 5 + + - name: nginx + image: nginx:1.17.5 + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/conf.d/app.conf + subPath: app.conf + - name: nginx-config + mountPath: /www/config.json + subPath: config.json + - name: www + mountPath: /www + ports: + - containerPort: 80 + protocol: TCP + + volumes: + - name: home + emptyDir: {} + - name: utils + emptyDir: {} + - name: www + emptyDir: {} + - name: nginx-config + configMap: + name: {{ .Release.Name }}-nginx-config + - name: certs + configMap: + name: octopod-certs diff --git a/charts/octopod/templates/octopod-ingress.yaml b/charts/octopod/templates/octopod-ingress.yaml new file mode 100644 index 0000000..f347444 --- /dev/null +++ b/charts/octopod/templates/octopod-ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-nginx-ingress + namespace: {{ .Values.namespace }} + annotations: + kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" + cert-manager.io/issuer: "{{ .Release.Name }}-certs" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-next-upstream: "http_502 error timeout" + + {{- if .Values.global.auth_url }} + nginx.ingress.kubernetes.io/auth-url: "{{ .Values.global.auth_url }}" + {{- end }} + + {{- if .Values.global.auth_signin }} + nginx.ingress.kubernetes.io/auth-signin: "{{ .Values.global.auth_signin }}" + {{- end }} +spec: + tls: + - hosts: + - {{ .Values.domain }} + secretName: {{ .Release.Name }}-tls + rules: + - host: {{ .Values.domain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }} + servicePort: 80 diff --git a/charts/octopod/templates/octopod-issuer.yaml b/charts/octopod/templates/octopod-issuer.yaml new file mode 100644 index 0000000..4803946 --- /dev/null +++ b/charts/octopod/templates/octopod-issuer.yaml @@ -0,0 +1,18 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ .Release.Name }}-certs + namespace: {{ .Release.Namespace }} +spec: + acme: + email: {{ .Values.acme_registration_email }} + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: {{ .Release.Name }}-letsencrypt + # ACME HTTP-01 provider configurations + solvers: + # An empty 'selector' means that this solver matches all domains + - selector: {} + http01: + ingress: + class: nginx diff --git a/charts/octopod/templates/octopod-migration-job.yaml b/charts/octopod/templates/octopod-migration-job.yaml new file mode 100644 index 0000000..20577d7 --- /dev/null +++ b/charts/octopod/templates/octopod-migration-job.yaml @@ -0,0 +1,75 @@ +{{- if .Values.migrations }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-migration-job + namespace: {{ .Release.Namespace }} + + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": hook-succeeded + +spec: + activeDeadlineSeconds: 600 + template: + spec: + containers: + + - name: copy + image: {{ .Values.global.image_prefix }}/{{ .Values.global.image }}:{{ .Values.global.image_tag }} + command: + - "bash" + - "-ec" + - | + set -ex + + # copy migrations + cp -av /migrations/* /mymigrations + + # create sqitch.conf + echo '[core]' > /mymigrations/sqitch.conf + echo 'engine = pg' >> /mymigrations/sqitch.conf + echo 'plan_file = sqitch.plan' >> /mymigrations/sqitch.conf + echo 'top_dir = .' >> /mymigrations/sqitch.conf + echo '[engine "pg"]' >> /mymigrations/sqitch.conf + echo ' registry = sqitch' >> /mymigrations/sqitch.conf + echo '[deploy]' >> /mymigrations/sqitch.conf + echo ' verify = true' >> /mymigrations/sqitch.conf + echo '[rebase]' >> /mymigrations/sqitch.conf + echo ' verify = true' >> /mymigrations/sqitch.conf + echo '[target "octopod"]' >> /mymigrations/sqitch.conf + echo 'uri = db:pg://{{ .Values.connections.pg_instance }}/{{ template "dbname" . }}' >> /mymigrations/sqitch.conf + volumeMounts: + - name: migrations + mountPath: /mymigrations + + - name: migrations + image: {{ .Values.global.image_prefix }}/{{ .Values.global.image }}:sqitch-v2.0.0 + command: + - "bash" + - "-ec" + - | + set -ex +{{- if .Values.seed }} + echo 'check db' + POSTGRESQL_CONN="psql postgresql://{{ .Values.connections.pg_instance }}/postgres" + DBNAME={{ template "dbname" . }} + ($POSTGRESQL_CONN -Atc "SELECT count(*) FROM pg_database WHERE lower(datname) = lower('$DBNAME');" | grep 1) || $POSTGRESQL_CONN -Atc "create database $DBNAME;" +{{- end }} + echo 'run migrations...' + cd /migrations && /usr/local/bin/sqitch deploy octopod +{{- if .Values.seed }} + echo 'seed' + DB_CONN="psql postgresql://{{ .Values.connections.pg_instance }}/{{ template "dbname" . }}" + cd /migrations && $DB_CONN -1 -f seeds.sql || echo 'ok' +{{- end }} + volumeMounts: + - name: migrations + mountPath: /migrations + + volumes: + - name: migrations + emptyDir: {} + restartPolicy: Never + backoffLimit: 2 +{{- end }} diff --git a/charts/octopod/templates/octopod-nginx-configmap.yaml b/charts/octopod/templates/octopod-nginx-configmap.yaml new file mode 100644 index 0000000..2c4269a --- /dev/null +++ b/charts/octopod/templates/octopod-nginx-configmap.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-nginx-config + namespace: {{ .Values.namespace }} +data: + app.conf: | + server { + listen 80 default_server; + server_name _; + root /www; + index index.html; + error_page 404 =200 /index.html; + } + config.json: | + { + "app_url": "https://{{ .Values.app_domain }}", + "ws_url": "wss://{{ .Values.ws_domain }}", + "app_auth": "Basic {{ .Values.basic_auth_token }}" + } diff --git a/charts/octopod/templates/octopod-power-app-ingress.yaml b/charts/octopod/templates/octopod-power-app-ingress.yaml new file mode 100644 index 0000000..a877b5a --- /dev/null +++ b/charts/octopod/templates/octopod-power-app-ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-power-app-nginx-ingress + namespace: {{ .Values.namespace }} + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-next-upstream: "http_502 error timeout" + nginx.ingress.kubernetes.io/ssl-passthrough: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" +spec: + rules: + - host: {{ .Values.power_app_domain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }} + servicePort: 443 + tls: + - hosts: + - {{ .Values.power_app_domain }} diff --git a/charts/octopod/templates/octopod-service.yaml b/charts/octopod/templates/octopod-service.yaml new file mode 100644 index 0000000..d04272c --- /dev/null +++ b/charts/octopod/templates/octopod-service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + namespace: {{ .Values.namespace }} + labels: + app: {{ .Release.Name }} +spec: + selector: + app: {{ .Release.Name }} + ports: + - name: octopod-power-app + port: 443 + targetPort: {{ .Values.port }} + - name: octopod-ui + port: 80 + targetPort: 80 + - name: octopod-app + port: 81 + targetPort: {{ .Values.ui_port }} + - name: octopod-ws + port: 82 + targetPort: {{ .Values.ws_port }} diff --git a/charts/octopod/templates/octopod-ws-ingress.yaml b/charts/octopod/templates/octopod-ws-ingress.yaml new file mode 100644 index 0000000..0a3011b --- /dev/null +++ b/charts/octopod/templates/octopod-ws-ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-ws-nginx-ingress + namespace: {{ .Values.namespace }} + annotations: + kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" + cert-manager.io/issuer: "{{ .Release.Name }}-certs" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-next-upstream: "http_502 error timeout" +spec: + tls: + - hosts: + - {{ .Values.ws_domain }} + secretName: {{ .Release.Name }}-ws-tls + rules: + - host: {{ .Values.ws_domain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }} + servicePort: 82 diff --git a/charts/octopod/values.yaml b/charts/octopod/values.yaml new file mode 100644 index 0000000..4ea15f8 --- /dev/null +++ b/charts/octopod/values.yaml @@ -0,0 +1,38 @@ +global: + image_prefix: + image: octopod + octo_image: octo + image_tag: + utils_image_prefix: + utils_image: + utils_image_tag: + +namespace: octopod +target_namespace: deployment +nodeselector: stand +service_account: octopod +port: 4443 +ui_port: 4000 +ws_port: 4020 +dbname: octopod +seed: false +migrations: true +replicas: 1 +domain: octopod.stage.example.com +app_domain: octopod-app.stage.example.com +ws_domain: octopod-ws.stage.example.com +power_app_domain: octopod-power-app.stage.example.com +base_domain: stage.example.com +project_name: Octopod +status_update_timeout: 600 +acme_registration_email: +basic_auth_token: +connections: + pg_instance: octopod:octopod@octopod-infra-postgres-0.octopod-infra-postgres.octopod:5432 +pg_host: octopod-infra-postgres-0.octopod-infra-postgres.octopod +requests: + cpu: 200m + memory: 256Mi +limits: + cpu: 200m + memory: 512Mi diff --git a/charts/pvc-control/.helmignore b/charts/pvc-control/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/charts/pvc-control/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/pvc-control/Chart.yaml b/charts/pvc-control/Chart.yaml new file mode 100644 index 0000000..395b1cf --- /dev/null +++ b/charts/pvc-control/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: pvc-control +version: 0.1.0 diff --git a/charts/pvc-control/templates/pvc-control-clusterrole.yaml b/charts/pvc-control/templates/pvc-control-clusterrole.yaml new file mode 100644 index 0000000..fb8672c --- /dev/null +++ b/charts/pvc-control/templates/pvc-control-clusterrole.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pvc-control-clusterrole +rules: + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["list", "delete", "deletecollection"] diff --git a/charts/pvc-control/templates/pvc-control-rolebinding.yaml b/charts/pvc-control/templates/pvc-control-rolebinding.yaml new file mode 100644 index 0000000..0d4994e --- /dev/null +++ b/charts/pvc-control/templates/pvc-control-rolebinding.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Values.serviceaccount }}-pvc-control-rolebinding + namespace: {{ .Values.namespace }} +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: pvc-control-clusterrole +subjects: + - kind: ServiceAccount + name: {{ .Values.serviceaccount }} + namespace: {{ .Values.octopod_namespace | default .Values.namespace }} diff --git a/charts/pvc-control/values.yaml b/charts/pvc-control/values.yaml new file mode 100644 index 0000000..a384640 --- /dev/null +++ b/charts/pvc-control/values.yaml @@ -0,0 +1,3 @@ +namespace: deployment +octopod_namespace: octopod +serviceaccount: octopod diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..ca7669c --- /dev/null +++ b/default.nix @@ -0,0 +1,54 @@ +{ sources ? import ./nix/sources.nix +, reflex-platform ? sources.reflex-platform +}: +(import reflex-platform { }).project ({ pkgs, ... }: { + useWarp = true; + + packages = { + octopod-common = ./octopod-common; + octopod-frontend = ./octopod-frontend; + octopod-backend = ./octopod-backend; + octo-cli = ./octo-cli; + octopod-api = ./octopod-api; + }; + + overrides = hself: hsuper: { + servant-reflex = hsuper.callCabal2nix "servant-reflex" sources.servant-reflex { }; + tabulation = hsuper.callCabal2nix "tabulation" "${sources.obelisk}/lib/tabulation" { }; + obelisk-executable-config-lookup = hsuper.callCabal2nix "obelisk-executable-config-lookup" "${sources.obelisk}/lib/executable-config/lookup" { }; + obelisk-route = hsuper.callCabal2nix "obelisk-route" "${sources.obelisk}/lib/route" { }; + hspec-webdriver = hsuper.callCabal2nix "hspec-webdriver" sources.hspec-webdriver-clone { }; + servant = pkgs.haskell.lib.overrideCabal hsuper.servant (old: { + postInstall = ""; + }); + servant-websockets = hsuper.callHackageDirect + { + pkg = "servant-websockets"; + ver = "2.0.0"; + sha256 = "01bmwg3ysj8gijcqghykxfsd62sqz1pfby2irpzh5ybwyh285pvg"; + } { }; + deriving-aeson = hsuper.callHackageDirect + { + pkg = "deriving-aeson"; + ver = "0.2.3"; + sha256 = "0ckwdi9pr4aqp9psag4mdbx30nygxkkpdf21rg9rfz16cz8079j7"; + } { }; + table-layout = hsuper.callHackageDirect + { + pkg = "table-layout"; + ver = "0.9.0.1"; + sha256 = "12nllfnh6b5mjda9qxfy192v0r0sx181w9zc9j70kvjdn7hgrb0y"; + } { }; + data-default-instances-base = hsuper.callHackageDirect + { + pkg = "data-default-instances-base"; + ver = "0.1.0.1"; + sha256 = "18basdy4qjn246phw008ll9zbi3rpdn6bh2dk0i81a60gsmyn58q"; + } { }; + }; + + shells = { + ghc = [ "octopod-common" "octopod-backend" "octopod-frontend" "octopod-api" "octo-cli" ]; + ghcjs = [ "octopod-common" "octopod-frontend" ]; + }; +}) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..2d501e3 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,31 @@ +.PHONY: docs mermaid plantuml + +docs: mermaid plantuml + +mermaid: + # to install mmdc use https://github.com/mermaidjs/mermaid.cli + # + # mmdc does not support stateDiagram-v2 (technical-architecture-deployment-statuses-fsm.mmd), + # use https://mermaid-js.github.io/mermaid-live-editor to render it + for src in `ls diagrams/src/*.mmd | grep -v technical-architecture-deployment-statuses-fsm.mmd`; do \ + name=`basename $$src .mmd`; \ + mmdc -i $$src -o "diagrams/images/$$name.png" --scale 4 --cssFile style.css; \ + done + +plantuml: + # to install plantuml go to https://plantuml.com/command-line + # + # plantuml takes output paths relative to input file + for src in `ls diagrams/src/*.puml`; do \ + name=`basename $$src .mmd`; \ + plantuml -I $$src -o "../../diagrams/images" -tpng; \ + done + +plantuml: + # to install plantuml go to https://plantuml.com/command-line + # + # plantuml takes output paths relative to input file + for src in `ls diagrams/src/*.puml`; do \ + name=`basename $$src .mmd`; \ + plantuml -I $$src -o "../../diagrams/images" -tpng; \ + done diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..cb2660f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# 🐙📑 Octopod documentation + +## 🔭 High-level notes +- [🐙 Overview](en/Overview.md) +- [🧑‍🔬 Project managment case study](en/PM_case_study.md) +- [🧑‍💻 Technical case study](en/Tech_case_study.md) + +## 🛠️ Technical documentation +- [🏗 Technical architecture](en/Technical_architecture.md) [[RU](ru/Technical_architecture.md)] +- [⚙️ Control script guide](en/Control_scripts.md) [[RU](ru/Control_scripts.md)] +- [🔧🐙 Octopod deployment guide](en/Octopod_deployment_guide.md) [[RU](ru/Octopod_deployment_with_K8S.md)] +- [🔧🚀 Helm-based Octopod project setup](en/Helm-based_deployment_guide.md) [[RU](ru/Helm-based_deployment_guide.md)] +- [🐙🎛 octo CLI user guide](en/Octo_user_guide.md) [[RU](ru/Octo_user_guide.md)] +- [🤖 CI integration](en/Integration.md) +- [🔒 Octopod security model](en/Security_model.md) [[RU](ru/Security_model.md)] diff --git a/docs/diagrams/images/app-architecture.png b/docs/diagrams/images/app-architecture.png new file mode 100644 index 0000000..52fbdf1 Binary files /dev/null and b/docs/diagrams/images/app-architecture.png differ diff --git a/docs/diagrams/images/sample_architecture.png b/docs/diagrams/images/sample_architecture.png new file mode 100644 index 0000000..7003af3 Binary files /dev/null and b/docs/diagrams/images/sample_architecture.png differ diff --git a/docs/diagrams/images/sample_deployment.png b/docs/diagrams/images/sample_deployment.png new file mode 100644 index 0000000..4ecab4e Binary files /dev/null and b/docs/diagrams/images/sample_deployment.png differ diff --git a/docs/diagrams/images/technical-architecture-archive-via-cli.png b/docs/diagrams/images/technical-architecture-archive-via-cli.png new file mode 100644 index 0000000..68695cf Binary files /dev/null and b/docs/diagrams/images/technical-architecture-archive-via-cli.png differ diff --git a/docs/diagrams/images/technical-architecture-archive-via-ui.png b/docs/diagrams/images/technical-architecture-archive-via-ui.png new file mode 100644 index 0000000..1b5b93e Binary files /dev/null and b/docs/diagrams/images/technical-architecture-archive-via-ui.png differ diff --git a/docs/diagrams/images/technical-architecture-cleanup-via-cli.png b/docs/diagrams/images/technical-architecture-cleanup-via-cli.png new file mode 100644 index 0000000..ab1adf6 Binary files /dev/null and b/docs/diagrams/images/technical-architecture-cleanup-via-cli.png differ diff --git a/docs/diagrams/images/technical-architecture-cleanup-via-ui.png b/docs/diagrams/images/technical-architecture-cleanup-via-ui.png new file mode 100644 index 0000000..04aae35 Binary files /dev/null and b/docs/diagrams/images/technical-architecture-cleanup-via-ui.png differ diff --git a/docs/diagrams/images/technical-architecture-create-via-cli.png b/docs/diagrams/images/technical-architecture-create-via-cli.png new file mode 100644 index 0000000..31ee000 Binary files /dev/null and b/docs/diagrams/images/technical-architecture-create-via-cli.png differ diff --git a/docs/diagrams/images/technical-architecture-create-via-ui.png b/docs/diagrams/images/technical-architecture-create-via-ui.png new file mode 100644 index 0000000..d7cefce Binary files /dev/null and b/docs/diagrams/images/technical-architecture-create-via-ui.png differ diff --git a/docs/diagrams/images/technical-architecture-deployment-states-fsm.png b/docs/diagrams/images/technical-architecture-deployment-states-fsm.png new file mode 100644 index 0000000..a321393 Binary files /dev/null and b/docs/diagrams/images/technical-architecture-deployment-states-fsm.png differ diff --git a/docs/diagrams/images/technical-architecture-restore-via-cli.png b/docs/diagrams/images/technical-architecture-restore-via-cli.png new file mode 100644 index 0000000..2d554db Binary files /dev/null and b/docs/diagrams/images/technical-architecture-restore-via-cli.png differ diff --git a/docs/diagrams/images/technical-architecture-restore-via-ui.png b/docs/diagrams/images/technical-architecture-restore-via-ui.png new file mode 100644 index 0000000..7bbeeed Binary files /dev/null and b/docs/diagrams/images/technical-architecture-restore-via-ui.png differ diff --git a/docs/diagrams/images/technical-architecture-update-via-cli.png b/docs/diagrams/images/technical-architecture-update-via-cli.png new file mode 100644 index 0000000..5059914 Binary files /dev/null and b/docs/diagrams/images/technical-architecture-update-via-cli.png differ diff --git a/docs/diagrams/images/technical-architecture-update-via-ui.png b/docs/diagrams/images/technical-architecture-update-via-ui.png new file mode 100644 index 0000000..b57a450 Binary files /dev/null and b/docs/diagrams/images/technical-architecture-update-via-ui.png differ diff --git a/docs/diagrams/src/sample_architecture.puml b/docs/diagrams/src/sample_architecture.puml new file mode 100644 index 0000000..cb45768 --- /dev/null +++ b/docs/diagrams/src/sample_architecture.puml @@ -0,0 +1,16 @@ +@startuml + +top to bottom direction + +database "Redis" as OBR +database "Postgres" as OBP +boundary "nginx" as OBN + +rectangle "Server" as OBSer + +OBSer -down-> OBR +OBSer -down-> OBP +OBN -right-> OBSer + + +@enduml diff --git a/docs/diagrams/src/sample_deployment.puml b/docs/diagrams/src/sample_deployment.puml new file mode 100644 index 0000000..343d581 --- /dev/null +++ b/docs/diagrams/src/sample_deployment.puml @@ -0,0 +1,61 @@ +@startuml + +top to bottom direction + +node "Kubernetes cluster" { + boundary "Kube API Server" as K8sAPI + + rectangle "Kubernetes" as K8s + + K8sAPI -> K8s + + cloud "Orange button staging" as OBS #OldLace { + frame "Common infrastructure" #lightcyan { + database "Redis" as OBR #PowderBlue + database "Postgres" as OBP #PowderBlue + boundary "nginx" as OBN #PowderBlue + } + + rectangle "Server with an orange button" as OBSer #Wheat + + OBSer -down-> OBR + OBSer -down-> OBP + OBN -right-> OBSer + } + + cloud "Green button staging" as GBS #technology { + frame "Common infrastructure " #lightcyan { + database "Redis" as GBR #PowderBlue + database "Postgres" as GBP #PowderBlue + boundary "nginx" as GBN #PowderBlue + } + + rectangle "Server with a green button" as GBSer #Greenyellow + + GBSer -down-> GBR + GBSer -down-> GBP + GBN -right-> GBSer + } + + K8s -down-> OBS : create the staging + K8s -down-> GBS : create the staging +} + + +node Octopod { + boundary "Web UI" as UI + rectangle "Octopod Server" as OctoS + rectangle "Staging control scripts" as SCS + + UI -> OctoS + OctoS -> SCS : delegates k8s logic to control script +} + +SCS -down-> K8sAPI : set up the stagings + + +actor Developer + +Developer -down-> UI : Specifies the git comit hash to deploy in the web UI + +@enduml diff --git a/docs/diagrams/src/technical-architecture-archive-via-cli.mmd b/docs/diagrams/src/technical-architecture-archive-via-cli.mmd new file mode 100644 index 0000000..c95ded4 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-archive-via-cli.mmd @@ -0,0 +1,37 @@ +sequenceDiagram + participant octo CLI + participant Octopod Server + participant PostgreSQL + participant UI + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + octo CLI->>Octopod Server: archive(name) + Octopod Server->>PostgreSQL: status=ArchivePending + alt name not found + PostgreSQL-->>Octopod Server: error: name not found + Octopod Server-->>octo CLI: error: name not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Octopod Server->>Octopod Server/BgWorker: archive + Octopod Server-->>octo CLI: done + Octopod Server/BgWorker->>ControlScripts: archive + ControlScripts->>KubeAPI: archive deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Archived/ArchivePending + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + end + end diff --git a/docs/diagrams/src/technical-architecture-archive-via-ui.mmd b/docs/diagrams/src/technical-architecture-archive-via-ui.mmd new file mode 100644 index 0000000..03e33e2 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-archive-via-ui.mmd @@ -0,0 +1,36 @@ +sequenceDiagram + participant UI + participant Octopod Server + participant PostgreSQL + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + UI->>Octopod Server: archive(name) + Octopod Server->>PostgreSQL: status=ArchivePending + alt name not found + PostgreSQL-->>Octopod Server: error: name not found + Octopod Server-->>UI: error: name not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Octopod Server->>Octopod Server/BgWorker: archive + Octopod Server-->>UI: done + Octopod Server/BgWorker->>ControlScripts: archive + ControlScripts->>KubeAPI: archive deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Archived/ArchivePending + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + end + end diff --git a/docs/diagrams/src/technical-architecture-cleanup-via-cli.mmd b/docs/diagrams/src/technical-architecture-cleanup-via-cli.mmd new file mode 100644 index 0000000..8d30664 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-cleanup-via-cli.mmd @@ -0,0 +1,20 @@ +sequenceDiagram + participant octo CLI + participant Octopod Server + participant PostgreSQL + participant UI + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + octo CLI->>Octopod Server: cleanup(name) + Octopod Server->>Octopod Server/BgWorker: cleanup + Octopod Server-->>octo CLI: done + Octopod Server/BgWorker->>ControlScripts: cleanup + ControlScripts->>KubeAPI: cleanup deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server->>PostgreSQL: delete config and logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info diff --git a/docs/diagrams/src/technical-architecture-cleanup-via-ui.mmd b/docs/diagrams/src/technical-architecture-cleanup-via-ui.mmd new file mode 100644 index 0000000..f0e261a --- /dev/null +++ b/docs/diagrams/src/technical-architecture-cleanup-via-ui.mmd @@ -0,0 +1,19 @@ +sequenceDiagram + participant UI + participant Octopod Server + participant PostgreSQL + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + UI->>Octopod Server: cleanup(name) + Octopod Server->>Octopod Server/BgWorker: cleanup + Octopod Server-->>UI: done + Octopod Server/BgWorker->>ControlScripts: cleanup + ControlScripts->>KubeAPI: cleanup deployment resources + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server->>PostgreSQL: delete config and logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info diff --git a/docs/diagrams/src/technical-architecture-create-via-cli.mmd b/docs/diagrams/src/technical-architecture-create-via-cli.mmd new file mode 100644 index 0000000..0405b21 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-create-via-cli.mmd @@ -0,0 +1,41 @@ +sequenceDiagram + participant octo CLI + participant Octopod Server + participant PostgreSQL + participant UI + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + octo CLI->>Octopod Server: create(name, tag, [override]) + Octopod Server->>PostgreSQL: store config, status=CreatePending + alt name already exists + PostgreSQL->>Octopod Server: error: deployment already exists + Octopod Server-->>octo CLI: error: deployment already exists + else + alt tag not found + Octopod Server-->>octo CLI: error: tag not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Octopod Server->>Octopod Server/BgWorker: create + Octopod Server-->>octo CLI: done + Octopod Server/BgWorker->>ControlScripts: create + ControlScripts->>KubeAPI: create deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + end + end + end diff --git a/docs/diagrams/src/technical-architecture-create-via-ui.mmd b/docs/diagrams/src/technical-architecture-create-via-ui.mmd new file mode 100644 index 0000000..49bb8f5 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-create-via-ui.mmd @@ -0,0 +1,40 @@ +sequenceDiagram + participant UI + participant Octopod Server + participant PostgreSQL + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + UI->>Octopod Server: create(name, tag, [override]) + Octopod Server->>PostgreSQL: store config, status=CreatePending + alt name already exists + PostgreSQL-->>Octopod Server: error: deployment already exists + Octopod Server-->>UI: error: deployment already exists + else + alt tag not found + Octopod Server-->>UI: error: tag not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Octopod Server->>Octopod Server/BgWorker: create + Octopod Server-->>UI: done + Octopod Server/BgWorker->>ControlScripts: create + ControlScripts->>KubeAPI: create deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + end + end + end diff --git a/docs/diagrams/src/technical-architecture-deployment-states-fsm.mmd b/docs/diagrams/src/technical-architecture-deployment-states-fsm.mmd new file mode 100644 index 0000000..1168d81 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-deployment-states-fsm.mmd @@ -0,0 +1,16 @@ +stateDiagram-v2 + [*] --> CreatePending: create + Running --> UpdatePending: update + Failure --> UpdatePending: update + Running --> ArchivePending: archive + Failure --> ArchivePending: archive + Archived --> CreatePending: restore + Archived --> [*]: cleanup + Running --> Failure: 30s passed and 'check' said "nok" + Failure --> Running: 30s passed and 'check' said "ok" + CreatePending --> Running: 5m passed and 'check' said "ok" + CreatePending --> Failure: 5m passed and 'check' said "nok" + UpdatePending --> Running: 5m passed and 'check' said "ok" + UpdatePending --> Failure: 5m passed and 'check' said "nok" + ArchivePending --> Archived: 30s passed and 'archive_check' said "ok" + ArchivePending --> ArchivePending: 30s passed and 'archive_check' said "nok" diff --git a/docs/diagrams/src/technical-architecture-restore-via-cli.mmd b/docs/diagrams/src/technical-architecture-restore-via-cli.mmd new file mode 100644 index 0000000..6ecfad9 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-restore-via-cli.mmd @@ -0,0 +1,41 @@ +sequenceDiagram + participant octo CLI + participant Octopod Server + participant PostgreSQL + participant UI + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + octo CLI->>Octopod Server: restore(name) + Octopod Server->>PostgreSQL: status=CreatePending + alt name not found + PostgreSQL-->>Octopod Server: error: name not found + Octopod Server-->>octo CLI: error: name not found + else + alt tag not found + Octopod Server-->>octo CLI: error: tag not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Octopod Server->>Octopod Server/BgWorker: create + Octopod Server-->>octo CLI: done + Octopod Server/BgWorker->>ControlScripts: create + ControlScripts->>KubeAPI: restore deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + end + end + end diff --git a/docs/diagrams/src/technical-architecture-restore-via-ui.mmd b/docs/diagrams/src/technical-architecture-restore-via-ui.mmd new file mode 100644 index 0000000..1d74815 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-restore-via-ui.mmd @@ -0,0 +1,40 @@ +sequenceDiagram + participant UI + participant Octopod Server + participant PostgreSQL + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + UI->>Octopod Server: restore(name) + Octopod Server->>PostgreSQL: status=CreatePending + alt name not found + PostgreSQL-->>Octopod Server: error: name not found + Octopod Server-->>UI: error: name not found + else + alt tag not found + Octopod Server-->>UI: error: tag not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Octopod Server->>Octopod Server/BgWorker: create + Octopod Server-->>UI: done + Octopod Server/BgWorker->>ControlScripts: create + ControlScripts->>KubeAPI: restore deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + end + end + end diff --git a/docs/diagrams/src/technical-architecture-update-via-cli.mmd b/docs/diagrams/src/technical-architecture-update-via-cli.mmd new file mode 100644 index 0000000..b68cb91 --- /dev/null +++ b/docs/diagrams/src/technical-architecture-update-via-cli.mmd @@ -0,0 +1,41 @@ +sequenceDiagram + participant octo CLI + participant Octopod Server + participant PostgreSQL + participant UI + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + octo CLI->>Octopod Server: create(name, tag, [override]) + Octopod Server->>PostgreSQL: store config, status=UpdatePending + alt name not found + PostgreSQL-->>Octopod Server: error: name not found + Octopod Server-->>octo CLI: error: name not found + else + alt tag not found + Octopod Server-->>octo CLI: error: tag not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->UI: deployments info + Octopod Server->>Octopod Server/BgWorker: update + Octopod Server-->>octo CLI: done + Octopod Server/BgWorker->>ControlScripts: update + ControlScripts->>KubeAPI: upgrade deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->UI: deployments info + end + end + end diff --git a/docs/diagrams/src/technical-architecture-update-via-ui.mmd b/docs/diagrams/src/technical-architecture-update-via-ui.mmd new file mode 100644 index 0000000..94e84fe --- /dev/null +++ b/docs/diagrams/src/technical-architecture-update-via-ui.mmd @@ -0,0 +1,40 @@ +sequenceDiagram + participant UI + participant Octopod Server + participant PostgreSQL + participant Octopod Server/BgWorker + participant Octopod Server/StatusUpdater + participant ControlScripts + participant KubeAPI + UI->>Octopod Server: create(name, tag, [override]) + Octopod Server->>PostgreSQL: store config, status=UpdatePending + alt name not found + PostgreSQL-->>Octopod Server: error: name not found + Octopod Server-->>UI: error: name not found + else + alt tag not found + Octopod Server-->>UI: error: tag not found + else + PostgreSQL-->>Octopod Server: ok + Octopod Server-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Octopod Server->>Octopod Server/BgWorker: update + Octopod Server-->>UI: done + Octopod Server/BgWorker->>ControlScripts: update + ControlScripts->>KubeAPI: upgrade deployment + KubeAPI-->>ControlScripts: done + ControlScripts-->>Octopod Server/BgWorker: done + Octopod Server/BgWorker->>PostgreSQL: write logs + Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + Note over Octopod Server/StatusUpdater: wait 5 minutes + loop check deployment status every 30 seconds + Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure + Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything + UI->>Octopod Server: get deployments info + Octopod Server-->>UI: deployments info + end + end + end diff --git a/docs/en/Control_scripts.md b/docs/en/Control_scripts.md new file mode 100644 index 0000000..e25ff63 --- /dev/null +++ b/docs/en/Control_scripts.md @@ -0,0 +1,294 @@ +# Control scripts + +
+ Table of contents + +- [General behavior](#general-behavior) +- [Scripts](#scripts) + - [🔁 init](#-init) + - [Description](#description) + - [Sample implementation](#sample-implementation) + - [✨ create](#-create) + - [Description](#description-1) + - [Execution example](#execution-example) + - [Sample implementation](#sample-implementation-1) + - [🔧 update](#-update) + - [Description](#description-2) + - [Execution example](#execution-example-1) + - [Sample implementation](#sample-implementation-2) + - [🗃 archive](#-archive) + - [Description](#description-3) + - [Execution example](#execution-example-2) + - [Sample implementation](#sample-implementation-3) + - [✅ check](#-check) + - [Description](#description-4) + - [Execution example](#execution-example-3) + - [Sample implementation](#sample-implementation-4) + - [🚮 cleanup](#-cleanup) + - [Description](#description-5) + - [Execution example](#execution-example-4) + - [Sample implementation](#sample-implementation-5) + - [🗃✅ archive_check](#-archive_check) + - [Description](#description-6) + - [Execution example](#execution-example-5) + - [Sample implementation](#sample-implementation-6) + - [🐋✅ tag_check](#-tag_check) + - [Description](#description-7) + - [Execution example](#execution-example-6) + - [👀 info](#-info) + - [Description](#description-8) + - [Execution example](#execution-example-7) + - [Sample implementation](#sample-implementation-7) + +
+ +## General behavior + +All _control scripts_ receive input as CLI arguments. After executing the required logic they must finish with an _exit code_ of `0` if no errors have occurred and the required actions have all completed. If there was an error and some steps were not executed, the *script* should exit with an *exit code* **distinct from `0`**. Any non-zero exit code will indicate an error. + +Everything the _scripts_ write to _stdout_ and _stderr_ will be collected and stored. DevOps engineers can then view these logs from the _octo CLI_, should that be needed. + +> *NOTE: Logs from `check`, `archive_check` and `tag_check` are not collected because they are called very often.* + +There are four arguments that are passed to **all** *scripts*. The first three arguments come from the [_Kubernetes ConfigMap_][configmap]: +* `--project-name` – the name of the project. It is supplied mostly for informational purposes and can be useful for sending notifications if that is necessary. +* `--base-domain` – the base domain. It can be useful for generating the URLs of deployments. +* `--namespace` – The namespace in which the deployment should be created. +* `--name` – The name of the deployment supplied in the _Web UI_. It can be useful for generating the deployment URL. + +*NOTE:* If an argument is marked with a ⭐, it means that the argument can be passed any number of times. + +## Scripts + +### 🔁 init + +#### Description + +This script is called **once** during the creation of the `Octopod Server` *Kubernetes Pod* to set up the proper environment to execute all other scripts. + +It is guaranteed that this script will be called **before** any of the other scripts. + +You could, for example, set up access to your *version control system*, *cloud providers*, etc. This can be achieved by saving the configuration into files in the `$HOME` directory. + +Unlike all other scripts, this script receives no arguments. + +#### Sample implementation + +```bash +mkdir $HOME/.ssh +echo -e "Host github.com\nHostname github.com\nPort 22\nUser git\nIdentityFile $HOME/.ssh/deploy.key" > $HOME/.ssh/config +echo "MY_DEPLOY_KEY" > $HOME/.ssh/deploy.key" +``` + +### ✨ create + +#### Description + +Creates a new deployment in the _Kubernetes_ cluster. + +This script receives the following additional command-line arguments as input: +* `--tag` – The _Docker Image tag_ that should be deployed. (In practice you can use some other string that identifies a version of your system to deploy – you will need to process it accordingly in the script.) +* `--app-env-override` [⭐](#star) – App-level overrides. These overrides should be passed to the server being deployed. These overrides are specified in the _Web UI_. They are passed in the format of `KEY=VALUE` pairs. +* `--deployment-override` [⭐](#star) – Deployment-level overrides. These overrides should be used to set up the deployment environment itself, rather than be passed to the server being deployed. These overrides are specified in the _Web UI_. They are passed in the format of `KEY=VALUE` pairs. + +#### Execution example + +The script might be called something like this: + +```bash +create --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" --tag "c9bbc3fcc69e5aa094bca110c6f79419ab7be77a" --app-env-override "EMAIL_TOKEN=123123" --app-env-override "SECRET_BUTTON_ENABLED=True" --deployment-override "FANCY_DATABASE=True" +``` + +#### Sample implementation + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +### 🔧 update + +#### Description + +Updates a deployment in _Kubernetes_ to a new *Docker Image tag*. + +This script receives the same additional command-line arguments as [`create`](#-create): +* `--tag` – The _Docker Image tag_ that should be deployed. (In practice you can use some other string that identifies a version of your system to deploy – you will need to process it accordingly in the script.) +* `--app-env-override` [⭐](#star) – App-level overrides. These overrides should be passed to the server being deployed. These overrides are specified in the _Web UI_. They are passed in the format of `KEY=VALUE` pairs. +* `--deployment-override` [⭐](#star) – Deployment-level overrides. These overrides should be used to set up the deployment environment itself, rather than be passed to the server being deployed. These overrides are specified in the _Web UI_. They are passed in the format of `KEY=VALUE` pairs. + +#### Execution example + +The script might be called something like this: + +```bash +update --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" --tag "c9bbc3fcc69e5aa094bca110c6f79419ab7be77a" --app-env-override "EMAIL_TOKEN=123123" --app-env-override "SECRET_BUTTON_ENABLED=True" --deployment-override "FANCY_DATABASE=True" +``` + +#### Sample implementation + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +### 🗃 archive + +#### Description + +"Archives" a deployment. This script should only free the computational resources used by the deployment ― it should remove the _Kubernetes Pods_, but not remove any _Persistent Volumes_ associated with the deployment. It is done this way to provide a period of time in which the user can recover a deployment in the state it was in. + +Deleting the _Persistent Volume Claims_ should be done in the [`cleanup`](#-cleanup) script. + +This script should in some sense be the inverse of [`create`](#-create) (up to _Persistent Volumes_). + +This script receives only [the default command-line arguments](#general-behavior) as input. + +#### Execution example + +The script might be called something like this: + +```bash +archive --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" +``` + +#### Sample implementation + +```bash +helm delete "$name" --purge +``` + +### ✅ check + +#### Description + +This script checks the status of the deployment. + +If the script exits with `0`, it means that the deployment is healthy and up. If the script exits with a non-zero exit code, it means that the deployment is not healthy or down. + +This script receives only [the default command-line arguments](#general-behavior) as input. + +#### Execution example + +The script might be called something like this: + +```bash +check --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" +``` + +#### Sample implementation + +```bash +echo "{\"Deployments\": [{\"ResourceName\": \"app-${name}\", \"Namespace\": \"${namespace}\"}], \"StatefulSets\": [{\"ResourceName\": \"db-${name}\", \"Namespace\": \"${namespace}\"}]}" | \ + kubedog multitrack -t 3 +``` + +### 🚮 cleanup + +#### Description + +Cleans up any persistent resources a deployment might have allocated, such as _Persistent Volumes_. + +This script will always be called **after** [`archive`](#-archive) has been called on the given deployment. + +This script receives only [the default command-line arguments](#general-behavior) as input. + +#### Execution example + +The script might be called something like this: + +```bash +cleanup --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" +``` + +#### Sample implementation + +```bash +kubectl delete pvc -n $namespace -l "app=$name" +``` + +### 🗃✅ archive_check + +#### Description + +This script checks that a given deployment really has been archived and is no longer running. + +If the scripts exits with `0`, it means that the deployment has been archived successfully. If the script exits with a non-zero exit code, it means that the deployment has not been archived. + + +This script receives only [the default command-line arguments](#general-behavior) as input. + +#### Execution example + +The script might be called something like this: + +```bash +archive_check --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" +``` + +#### Sample implementation + +```bash +helm status $name +``` + +### 🐋✅ tag_check + +#### Description + +This script is called right before [`create`](#-create) and [`update`](#-update) scripts to check that a given _Docker Image tag_ exists. This can be useful since it can be very easy to make a typo in the _Docker Image tag_ and deployments are typically not instant. Implementing this script would allow the user of the _Web UI_ to instantly get an error specifically about a wrong _Docker Image tag_. + +This script receives the following additional command-line arguments as input: +* `--tag` – The _Docker Image tag_ that should be checked. + +#### Execution example + +The script might be called something like this: + +```bash +tag_check --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" --tag "c9bbc3fcc69e5aa094bca110c6f79419ab7be77a" +``` + +### 👀 info + +#### Description + +This script returns user-facing metadata about a deployment. Currently, the metadata consists of URLs that are relevant for the deployment. Things like the deployment URL, the URL to view logs, and the database URL. + +The script should return the metadata as a two-column CSV table: + +``` +app,https://foo.example.com +api,https://api.foo.example.com +``` + +This script receives only [the default command-line arguments](#general-behavior) as input. + +#### Execution example + +The script might be called something like this: + +```bash +info --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" +``` + +#### Sample implementation + +```bash +echo "app,https://${name}.example.com" +echo "api,https://api.${name}.example.com" +``` + +[configmap]: https://kubernetes.io/docs/concepts/configuration/configmap/ diff --git a/docs/en/Helm-based_deployment_guide.md b/docs/en/Helm-based_deployment_guide.md new file mode 100644 index 0000000..ae793ba --- /dev/null +++ b/docs/en/Helm-based_deployment_guide.md @@ -0,0 +1,177 @@ +# Helm-based deployment guide + +
+ Table of contents + +- [The web application](#the-web-application) +- [Setting up Octopod](#setting-up-octopod) + - [Control scripts](#control-scripts) + - [A word about TLS](#a-word-about-tls) + - [Deploying Octopod](#deploying-octopod) +- [Testing out the deployment](#testing-out-the-deployment) + - [Setting up _octo CLI_](#setting-up-octo-cli) + - [Setting up certificates](#setting-up-certificates) + - [Setting up the API URL](#setting-up-the-api-url) + - [Creating a deployment](#creating-a-deployment) + - [Adding an override](#adding-an-override) + - [Updating the deployment version](#updating-the-deployment-version) + - [Changing the number of replicas](#changing-the-number-of-replicas) + +
+ +In this guide, we will examine a very simple web application and explore setting up _Octopod_ to deploy it. + +## The web application + +The web application we will be using is a very simple application that serves a single endpoint `/`. The returned HTML markup contains the environment variables the executable has read from the environment. The only variables returned are the ones whose name starts with `APP_ENV`. + +![](../images/first.png) + +The source code can be found in the [examples/web-app](../../examples/web-app) folder of this repository. + +You can also find a second version of the server in the [examples/web-app-v2](../../examples/web-app-v2) folder of this repository. The second version is identical to the first version with the only difference being that it returns the variables as an unordered list. + +![](../images/second.png) + +We have already built and pushed the two versions of the application into the [typeable/octopod-web-app-example](https://hub.docker.com/repository/docker/typeable/octopod-web-app-example) DockerHub registry under the `v1` and `v2` tags. + +## Setting up Octopod + +### Control scripts + +The only thing you need to do to configure _Octopod_ to work with your application is to write appropriate [_control scripts_](Control_scripts.md) to manipulate your deployments. We have already written the appropriate _control scripts_ for this application. You can find them in the [examples/helm-based-control-scripts](../../examples/helm-based-control-scripts) folder of this repository. The scripts are written in the _Rust_ programming language. + +The most interesting of them all is the [create.rs](../../examples/helm-based-control-scripts/src/bin/create.rs) script. The basic order of operations is: + +1. Read the passed command-line arguments +2. Clone the repo to get the _charts_ used to deploy the application with _helm_ +3. Generate the arguments that should be passed to _helm_ +4. Call _helm_ with the downloaded _charts_ and the generated arguments + +> 💡 **NOTE:** You might have noticed that there is no `update.rs`. That is because our application is stateless and packaged up into a single _chart_. This allows us to simply reuse the same script for both creating and updating a deployment. If you have a more complicated setup with a database, for example, you will most likely need a distinct implementation for `update`. + +### A word about TLS + +If you are deploying Web applications, as we are here, you probably want to use TLS to encrypt your connections to your deployment. The most straightforward way of doing this is generating a separate TLS certificate for every deployment (for every subdomain). [_Cert Manager_][cert-manager] creates TLS certificates through [_Let’s Encrypt_][lets-encrypt] and [_Let’s Encrypt_][lets-encrypt] has [a limit on the amount of certificates][lets-encrypt-rate-limits] you can issue within a given time interval. If you exceed this limit you will start getting a _too many registrations for this IP_ error. If that is the case moving the [_Cert Manager_][cert-manager] _Pod_ might help. + +### Deploying Octopod + +To deploy _Octopod_ you will need to follow the [_Octopod_ deployment guide](Octopod_deployment_guide.md). The only modification will be that you will replace the "Control Scripts Setup" section in the last step with the appropriate values. + +These values point to a docker registry where we have already packaged up these _control scripts_ into a _Docker Image_. + +```bash +################################################# +# Control Scripts Setup +# +# if you are just testing things out you can paste the values +# from the Helm Deployment Guide example +################################################# + +# The name of the registry with control scripts +utils_registry="typeable" + +# The name of the image with control scripts +utils_image="octopod-helm-example" + +# The tag of the image to use +utils_image_tag="1.0" +``` + +## Testing out the deployment + +### Setting up _octo CLI_ + +Using the Web UI is fairly straightforward, so we will examine creating deployments with the [_octo CLI_](Octo_user_guide.md). + +#### Setting up certificates + +You will need to get the paths to `client_cert.pem` and `client_key.pem` generated in the [Creating SSL certificates](Octopod_deployment_guide.md#creating-ssl-certificates) step and place them into `TLS_CERT_PATH` and `TLS_KEY_PATH` environment variables: + +```bash +export TLS_CERT_PATH=/tmp/octopod/certs/client_cert.pem +export TLS_KEY_PATH=/tmp/octopod/certs/client_key.pem +``` + +#### Setting up the API URL + +You will also need to set the power API URL (the `power_app_domain` value from the [Installing _Octopod Server_](Octopod_deployment_guide.md#installing-octopod-server) section) as the `OCTOPOD_URL` environment variable: + +```bash +export OCTOPOD_URL= +``` + +### Creating a deployment + +To create a deployment you can now run: + +```bash +$ octo create -n hello-octopod -t v1 -e APP_ENV_KEY1=VALUE1 +``` + +The options are: +- `-n hello-octopod` – specifies that the name (subdomain) of the deployment should be `hello-octopod` +- `-t v1` specifies the version (Docker Image Tag) of the application to deploy to be `v1` +- `-e APP_ENV_KEY1=VALUE1` specifies adds an application-level key-value pair `APP_ENV_KEY1=VALUE1` + +> 💡 **NOTE:** For more detail on _octo CLI_ options please see the [octo CLI user guide](Octo_user_guide.md). + +This will run the `create` _control script_, which in turn will call `helm`. After waiting a couple of seconds you can visit `http://hello-octopod.` to see the running application: + +![](../images/hello-octopod-1.png) + +You can also see the deployed pod in the cluster using `kubectl`: + +```bash +$ kubectl get pods -n deployment +NAME READY STATUS RESTARTS AGE +app-hello-octopod-8965856-qbwvq 1/1 Running 0 15s +``` + +### Adding an override + +You can modify deployments by adding or removing overrides. To add a new application-level override run: + +```bash +$ octo update -n hello-octopod -t v1 -e APP_ENV_KEY2=VALUE2 +``` + +This will run the `update` _control script_ (which is identical to the `create` script in our case), which in turn will call `helm`. After waiting a few seconds you visit the deployment URL again and see the redeployed version: + +![](../images/hello-octopod-2.png) + +### Updating the deployment version + +You can change the version (_Docker Image Tag_) of your deployment like so: + +```bash +$ octo update -n hello-octopod -t v2 +``` + +After waiting a few seconds you visit the deployment URL again and see the redeployed version: + +![](../images/hello-octopod-3.png) + +### Changing the number of replicas + +You can change the number of replicas of your deployment (this is [essentially implemented in the _charts_ that we use](../../examples/web-app/charts/web-app/templates/deployment.yaml#L7)) like so: + +```bash +$ octo update -n hello-octopod -t v2 -o replicas=3 +``` + +`-o replicas=3` adds a deployment-level key-value pair (override) `replicas=3`. + +You can verify that the new replicas have been deployed using `kubectl`: + +```bash +$ kubectl get pods -n deployment +NAME READY STATUS RESTARTS AGE +app-hello-octopod-8965856-qbwvq 1/1 Running 0 97m +app-hello-octopod-8965856-v585c 1/1 Running 0 15s +app-hello-octopod-8965856-v88md 1/1 Running 0 15s +``` + +[cert-manager]: https://cert-manager.io/docs +[lets-encrypt]: https://letsencrypt.org +[lets-encrypt-rate-limits]: https://letsencrypt.org/docs/rate-limits diff --git a/docs/en/Integration.md b/docs/en/Integration.md new file mode 100644 index 0000000..52480e7 --- /dev/null +++ b/docs/en/Integration.md @@ -0,0 +1,35 @@ +# Integration into existing CI/CD pipelines + +
+ Table of contents + +- [✨ Creating deployments](#-creating-deployments) +- [🚀 Updating deployments](#-updating-deployments) + +
+ +You likely already have some form of CI integration with your version control system, such as *GitHub Action* or *Travis CI*, to run various checks on your code. Most of these services are set up by providing what is essentially just a shell script that is run under specific conditions. + +You might want to automate deployments even further ― you might want deployments to be automatically created and updated when developers create and update *Pull Requests*. + +_Octopod_ can be interacted with through the _octo CLI_ tool. This tool can be easily called from within a *CI* script. + +## ✨ Creating deployments + +To create a deployment (given that you have already obtained a *Docker Image* and uploaded it to your _Image Registry_ in one of the previous *CI* steps) you simply need to call _octo CLI_ with the following arguments: + +```bash +octo create -n $NAME -t $IMAGE_TAG +``` + +`$NAME` is the name of the deployment you want to create. You can set it to be the name of the branch for example. + +`$IMAGE_TAG` is the _tag_ of the docker image you want to deploy. + +## 🚀 Updating deployments + +Updating deployments is done using the same arguments, but you need to call `create` command, instead of the `update` command: + +```bash +octo update -n $NAME -t $IMAGE_TAG +``` diff --git a/docs/en/Octo_user_guide.md b/docs/en/Octo_user_guide.md new file mode 100644 index 0000000..d6092bd --- /dev/null +++ b/docs/en/Octo_user_guide.md @@ -0,0 +1,251 @@ +# Octo CLI User Guide + +
+ Table of contents + +- [Environment variables](#environment-variables) + - [`OCTOPOD_URL`](#octopod_url) + - [`TLS_CERT_PATH` and `TLS_KEY_PATH`](#tls_cert_path-and-tls_key_path) +- [Commands](#commands) + - [create](#create) + - [Description](#description) + - [Options](#options) + - [Usage example](#usage-example) + - [list](#list) + - [Description](#description-1) + - [Options](#options-1) + - [Usage example](#usage-example-1) + - [archive](#archive) + - [Description](#description-2) + - [Options](#options-2) + - [Usage example](#usage-example-2) + - [update](#update) + - [Description](#description-3) + - [Options](#options-3) + - [Usage example](#usage-example-3) + - [info](#info) + - [Description](#description-4) + - [Options](#options-4) + - [Usage example](#usage-example-4) + - [cleanup](#cleanup) + - [Description](#description-5) + - [Options](#options-5) + - [Usage example](#usage-example-5) + - [restore](#restore) + - [Description](#description-6) + - [Options](#options-6) + - [Usage example](#usage-example-6) + - [clean-archive](#clean-archive) + - [Description](#description-7) + - [Options](#options-7) + - [Usage example](#usage-example-7) + - [logs](#logs) + - [Description](#description-8) + - [Options](#options-8) + - [Usage example](#usage-example-8) + +
+ +## Environment variables + +All commands _octo CLI_ executes require the executable to send authenticated requests to the _Octopod Server_. For this purpose _octo CLI_ needs both a way to reach your particular instance of _Octopod Server_, and a way for _Octopod Server_ to identify that you are allowed to make the given request. + +### `OCTOPOD_URL` + +> **_NOTE:_** this argument is **required** for _octo CLI_ to function. + +`OCTOPOD_URL` is an environment variable _octo CLI_ reads to find your particular _Octopod Server_ installation. For example, it could contain `https://octopod-power-app.example.com:443`. + +### `TLS_CERT_PATH` and `TLS_KEY_PATH` + +`TLS_CERT_PATH` should contain the path to the TLS certificate you generated when setting up _Octopod Server_ and `TLS_KEY_PATH` should contain the path to the TLS key you generated when setting up _Octopod Server_. These files are used to authenticate the requests to _Octopod Server_. + +If these variables are not set, then _octo CLI_ tries to read the certificate from the path `./cert.pem`, and the key from the path `./key.pem`. + +## Commands + +> ***NOTE:*** If an argument is marked with a ⭐, it means that the argument can be passed any number of times. + +### create + +#### Description + +Creates a new deployment. + +#### Options + +- `-n,--name ARG` – The name of the deployment to create +- `-t,--tag ARG` – The _Docker tag_ to deploy +- `-e,--set-app-env-override ARG` [⭐](#star) – Set an application-level override. Expects a string in the format `KEY=VALUE`. +- `-o,--set-deployment-override ARG` [⭐](#star) – Set a deployment-level override. Expects a string in the format `KEY=VALUE`. + +#### Usage example + +```bash +$ octo create -n hello-octopod -t ca5fd1fe08389f6422a506a59b68a5272ac37ba6 -e KEY1=VALUE1 -e KEY2=VALUE2 +``` + +### list + +#### Description + +Gets a list of all deployment names – both archived and active. + +#### Options + +This command does not require any arguments. + +#### Usage example + +```bash +$ octo list +hello-octopod +foo +bar +``` + +### archive + +#### Description + +Archives a given deployment. + +#### Options + +- `-n,--name ARG` – The name of the deployment to archive. + +#### Usage example + +```bash +$ octo archive -n hello-octopod +``` + +### update + +#### Description + +Updates the parameters of a given deployment. + +#### Options + +- `-n,--name ARG` – The name of the deployment to update +- `-t,--tag ARG` – The new _Docker tag_ to update the deployment to +- `-e,--set-app-env-override ARG` [⭐](#star) – Add a new or replace an existing application-level override. Expects a string in the format `KEY=VALUE`. +- `-E,--unset-app-env-override ARG` [⭐](#star) – Removes an existing application-level override. +- `-o,--set-deployment-override ARG` [⭐](#star) – Add a new or replace an existing deployment-level override. Expects a string in the format `KEY=VALUE`. +- `-O,--unset-deployment-override` [⭐](#star) – Removes an existing deployment-level override. + +#### Usage example + +```bash +$ octo update -n octopod -t 015f16ecf398fcadaac508c1855ae160af0969c4 -E KEY1 -e KEY2=VALUE22222 -a KEY3=VALUE8 +``` + +### info + +#### Description + +Gets detailed information about a deployment, including a log of all preformed actions and the current parameters. + +#### Options + +- `-n,--name ARG` – The name of the deployment + +#### Usage example + +```bash +$ octo info -n hello-octopod +Current settings: +tag: v1 +application overrides: app=1 (Public) + +deployment overrides: dep=2 (Public) + +metadata: + app: https://ree.lvh.me + +Last logs: +┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓ +┃ Created at ┃ Action id ┃ Action ┃ Tag ┃ App overrides ┃ Deployment overrides ┃ Exit code ┃ +┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩ +│ 2020-11-02T17:14:03 │ 7 │ create │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │ +├─────────────────────┼───────────┼────────┼─────┼────────────────┼──────────────────────┼───────────┤ +│ 2020-11-02T19:01:02 │ 8 │ update │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │ +└─────────────────────┴───────────┴────────┴─────┴────────────────┴──────────────────────┴───────────┘ +``` + +### cleanup + +#### Description + +Frees all resources used by a given archived deployment. It will not succeed if the deployment is not archived. You can not recover the deployment after this command. + +#### Options + +- `-n,--name ARG` – The name of the deployment + +#### Usage example + +```bash +$ octo cleanup -n hello-octopod +``` + +### restore + +#### Description + +Restores a previously archived deployment. + +#### Options + +- `-n,--name ARG` – The name of the deployment + +#### Usage example + +```bash +$ octo restore -n hello-octopod +``` + +### clean-archive + +#### Description + +Calls `octo cleanup` on all deployments that were archived more than two weeks ago. This command is used in a cronjob which is automatically set up when deploying _Octopod Server_. + +#### Options + +This command does not have any options. + +#### Usage example + +```bash +$ octo clean-archive +``` + +### logs + +#### Description + +Outputs the logs collected while running an action on a deployment. For example when deploying or updating a deployment. + +#### Options + +- `-a,--action ARG` – the id of the action to print logs for +- `-l,--log-type ARG` – the types of logs that should be printed. Possible values are: `stdout`, `stderr`, `all`. The default value is `all`. + +#### Usage example + +``` +$ octo logs -a 13 + stdout: + + + stderr: + +error: Found argument '--deployment-override' which wasn't expected, or isn't valid in this context + +USAGE: + update --app-env-override ... --base-domain --name --namespace --project-name --tag + +For more information try --help +``` diff --git a/docs/en/Octopod_deployment_guide.md b/docs/en/Octopod_deployment_guide.md new file mode 100644 index 0000000..79d095a --- /dev/null +++ b/docs/en/Octopod_deployment_guide.md @@ -0,0 +1,399 @@ +# Octopod Server deployment guide + +
+ Table of contents + +- [Installing required utilities](#installing-required-utilities) +- [Setting up your cluster](#setting-up-your-cluster) + - [General utilities](#general-utilities) + - [Tiller (Helm)](#tiller-helm) + - [Cluster access privileges](#cluster-access-privileges) + - [A word about TLS](#a-word-about-tls) +- [Downloading project sources code](#downloading-project-sources-code) +- [Creating required namespaces](#creating-required-namespaces) +- [Creating required _Service Accounts_](#creating-required-service-accounts) + - [Creating the actual service account](#creating-the-actual-service-account) + - [Giving the appropriate _Service Account_ roles](#giving-the-appropriate-service-account-roles) +- [Web UI authentication secrets](#web-ui-authentication-secrets) +- [_octo CLI_ authentication certificates](#octo-cli-authentication-certificates) + - [Creating SSL certificates](#creating-ssl-certificates) + - [Enabling SSL passthrough](#enabling-ssl-passthrough) +- [Setting up DNS](#setting-up-dns) + - [Deploying _Octopod_ on localhost](#deploying-octopod-on-localhost) +- [Installing _Octopod_ infrastructure](#installing-octopod-infrastructure) + - [Installing the appropriate _Storage Class_](#installing-the-appropriate-storage-class) + - [Installing the actual infrastructure](#installing-the-actual-infrastructure) +- [Installing _Octopod Server_](#installing-octopod-server) + +
+ +## Installing required utilities + +Installing _Octopod Server_ in your cluster will require that you have the following tools installed on your system: +1. [_kubectl_][kubectl] +2. [_helm 2_][helm] + +## Setting up your cluster + +### General utilities + +_Octopod Server_ requires the following utilities to be installed in your cluster: + +1. [_Ingress Nginx_][ingress-nginx] +2. [_Cert Manager_][cert-manager] + +_Octopod Server_ require the following minimal resources to function properly: 2 CPU, 2 GB of RAM. Make sure you have sufficient resources in your cluster. + +By default _Octopod Server_ will be deployed on nodes with the `role=stand` label. Please make sue you have the appropriate label set in your cluster: + +```bash +kubectl label node role=stand +``` + +### Tiller (Helm) + +[_Tiller_][tiller] is a cluster-side service used by [_helm 2_][helm] to manage deployments. The easiest way to install it is using the following command: + +```bash +helm init +``` + +#### Cluster access privileges + +When installing _Octopod Server_ you might encounter [a problem with cluster access privileges](https://github.com/helm/helm/issues/5100) related to [_Tiller_][tiller]. + +To give sufficient privileges to [_Tiller_][tiller] you can use the following commands: + +```bash +kubectl create -n kube-system serviceaccount tiller +kubectl --namespace kube-system create clusterrolebinding tiller-cluster-admin --clusterrole=cluster-admin --serviceaccount=kube-system:tiller +kubectl --namespace kube-system patch deploy tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}' +``` + +### A word about TLS + +To function properly _Octopod_ needs to generate three TLS certificates for the three subdomains it will be using. [_Cert Manager_][cert-manager] creates TLS certificates through [_Let’s Encrypt_][lets-encrypt] and [_Let’s Encrypt_][lets-encrypt] has [a limit on the amount of certificates][lets-encrypt-rate-limits] you can issue within a given time interval. If you exceed this limit you will start getting a _too many registrations for this IP_ error. If that is the case moving the [_Cert Manager_][cert-manager] _Pod_ might help. + +## Downloading project sources code + +To download the source code required to install _Octopod Sever_ you will need to clone the git repository: + +```bash +git clone https://github.com/typeable/octopod.git /tmp/octopod +``` + +## Creating required namespaces + +_Octopod_ uses the following namespaces in your cluster: + +1. `deployment` – as the name would suggest your deployments will be installed in this namespace +2. `octopod` – this namespace will be used to install the _Octopod_ infrastructure + +To create the two namespaces you can use these commands: + +```bash +kubectl create namespace deployment +kubectl create namespace octopod +``` + +## Creating required [_Service Accounts_][kubernetes-service-account] + +### Creating the actual service account + +_Octopod Server_ requires an `octopod` [_Service Account_][kubernetes-service-account] to function. You can create it using the following command: + +```bash +kubectl create -n octopod serviceaccount octopod +``` + +### Giving the appropriate _Service Account_ roles + +1. If you are planning to use [_helm 2_][helm] in your [_Control scripts_](Control_scripts.md) to deploy your deployments, you will need to give appropriate permissions to the `octopod` _Service Account_: + ```bash + cd /tmp/octopod/charts + helm install --name octopod-helm-access ./helm-access + ``` + +2. If you are planning to delete [_Persistent Volumes Claims_][kubernetes-pvc] in your [_Control scripts_](Control_scripts.md) (might be useful for the `cleanup` script), you will need to give appropriate permissions to the `octopod` _Service Account_: + + ```bash + cd /tmp/octopod/charts + helm install --name octopod-pvc-control ./pvc-control + ``` + +3. If you are planning to use _Octopod_ to delete unused certificates in your [_Control scripts_](Control_scripts.md) (might be useful for the `cleanup` script), you will need to give appropriate permissions to the `octopod` _Service Account_: + + ```bash + cd /tmp/octopod/charts + helm install --name octopod-cert-control ./cert-control + ``` + +4. If you are planning to use [_kubedog_][kubedog] to check the state of your deployments in your [_Control scripts_](Control_scripts.md) (might be useful for the `check` script), you will need to give appropriate permissions to the `octopod` _Service Account_: + + ```bash + cd /tmp/octopod/charts + helm install --name octopod-kubedog-access ./kubedog-access + ``` + +## Web UI authentication secrets + +[Authentication](Security_model.md#web-ui-authentication) between _Octopod Server_ and the _Web UI_ is done through _Basic Auth_. This implies that there needs to be a username and password associated with it. + +You can generate the username and password, and push into your cluster using the following command (of course you will want to generate a secure pair): + +```bash +username="octopod" +password="password" # Please change it to a more secure password +kubectl create secret generic octopod-basic-auth -n octopod --from-literal=auth=$(htpasswd -bn $username $password) +``` + +## _octo CLI_ authentication certificates + +### Creating SSL certificates + +[Authentication](Security_model.md#octo-cli-authentication) between _octo CLI_ and _Octopod Server_ is performed through self-signed SSL certificates. + +You can generate the certificates and push them into your cluster using the following commands: + +```bash +mkdir certs +(cd certs && \ +openssl req -x509 -newkey rsa:4096 -keyout server_key.pem -out server_cert.pem -nodes -subj "/CN=localhost/O=Server" && \ +openssl req -newkey rsa:4096 -keyout client_key.pem -out client_csr.pem -nodes -subj "/CN=Client" && \ +openssl x509 -req -in client_csr.pem -CA server_cert.pem -CAkey server_key.pem -out client_cert.pem -set_serial 01 -days 3650) +kubectl create configmap octopod-certs -n octopod --from-file=./certs +``` + +After executing these command you will find a new `certs` directory containing the certificates used for authentication between _octo CLI_ and _Octopod Server_. `client_key.pem` and `client_cert.pem` should then be [passed to _octo CLI_ through environment variables](Octo_user_guide.md#tls_cert_path-and-tls_key_path). + +### Enabling SSL passthrough + +Since we use custom self-signed SSL certificates for authentication, we will need the certificates used with requests to be passed to the server as-is without any modification. This is not support in default [_ingress-nginx_][ingress-nginx] configurations so you will most likely need to modify it manually. + +Enabling SSL passthrough in [_ingress-nginx_][ingress-nginx] can be done by adding the `--enable-ssl-passthrough` command-line argument to the [_ingress-nginx_][ingress-nginx] config in your cluster. + +To do this you can execute a command similar to this (you will need to lookup the names of the namespace and the deployment in your particular cluster): + +```bash +kubectl edit deploy -n ingress-nginx ingress-nginx-controller +``` + +An editor with a YAML config should open up. You will need to modify to it have, among other things, this parameter: + +```yaml +spec: + ... + template: + ... + spec: + ... + containers: + ... + - args: + ... + - --enable-ssl-passthrough +``` + + +## Setting up DNS + +You will need to set up DNS records to point subdomains of your domain to the IP address of your cluster. The DNS record should look something like this: + +``` +*.octo.example.com A 1.2.3.4 +octo.example.com A 1.2.3.4 +``` + +### Deploying _Octopod_ on localhost + +If you are deploying locally and don't have a separate domain you are trying set up, the lvh.me domain can be useful – it is set up to point to `localhost` and you can use it to work with subdomains. Even so, deploying a fully-functional version of _Octopod_ on `localhost` is non-trivial and will require modifying the deployment _Charts_ to disable HTTPS redirects. (This guide does not cover that.) + +## Installing _Octopod_ infrastructure + +### Installing the appropriate _Storage Class_ + +Before installing the infrastructure you will first need to make sure you have a [_Storage Class_][kubernetes-storage-classes] named `default` installed in your cluster. You can check installed [_Storage Classes_][kubernetes-storage-classes] with the following command: + +```bash +kubectl get storageclass +``` + +If you do not have it, you will need to install it. Installing the [_Storage Class_][kubernetes-storage-classes] in [_minikube_][minikube] can be done with the following command (you will need to modify it to suit your cluster hosting provider): + +```bash +cat < + Table of contents + +- [Intro](#intro) +- [🎯 The aim of Octopod](#-the-aim-of-octopod) +- [🔬 Example](#-example) + - [💽 The server](#-the-server) + - [🎨 Changing button colors](#-changing-button-colors) + - [😖 Why have such a complicated staging deployment?](#-why-have-such-a-complicated-staging-deployment) +- [🛠 The way Octopod is set up](#-the-way-octopod-is-set-up) +- [🎛️ CLI](#️-cli) +- [🔒 Authentication in the UI](#-authentication-in-the-ui) +- [🤖 Automatic deployment / CD](#-automatic-deployment--cd) +- [📗 Glossary](#-glossary) + + + +**NOTE: it is not recommended to use Octopod for managing production environments** + +## Intro + +_Octopod_ is a tool which implements the multi-staging deployment model (MSDM) on top of _Kubernetes_. _MSDM_ implies that every developed feature needs to be not only deployed in a separate environment for QA testing, but also needs to be updated when bugs found during testing are fixed and the feature is refined. + +_Octopod_ exists to reduce the overhead in deploying and maintaining per-feature staging environments. This responsibility may otherwise fall to: + +1. **DevOps engineers** – this might seem natural since deploying and updating systems in new environments is typically the task of a DevOps engineer who has experience in system administration. However, developers and QA engineers would be blocked for additional periods while DevOps engineers deal with the additional load. +2. **Developers** – they might take on the responsibility for deploying and maintaining their feature stagings ― this would most likely waste a lot of time since developers might not have the required experience. + +The process of deploying and updating stagings is likely to be extremely similar across different developed features ― changing the behavior of a button and updating pricing calculations would probably be identical from the point of view of system administration ― a new version of the system needs to be deployed with the same default configuration (the staging configuration, as opposed to a production configuration). + +## 🎯 The aim of Octopod + +Octopod aims to extract the mentioned similarity between deploying different staging deployments while still allowing a certain amount of configuration where per-feature setup is still required. The result is a simple interface, which allows users to manage staging deployments without any system administration expertise or, for that matter, even without deep technical expertise. + +## 🔬 Example + +### 💽 The server + +You are developing a server, which is accessed through [_nginx_](https://www.nginx.com), and the server needs access to a [*Postgres*](https://www.postgresql.org) database and a [*Redis*](https://redis.io) database + +![](../diagrams/images/sample_architecture.png) + +### 🎨 Changing button colors + +Your server serves HTML to the browser, which displays two buttons. Both buttons currently have the same ugly color as the background, and you have two separate tasks: one task to change the first button be orange and another task to make the second button green. (Note that this is a toy example ― imagine that these are two separate complex tasks.) + +Now imagine that two different developers each completed one of the tasks, and you are now deploying the new and updated version of your server to a staging environment. You are very surprised when you find that for some reason the background of the whole page suddenly became pink. Every developer says that they did not make the change, and yet it is there. (Here the background color changing to pink denotes an undesirable change, which impacts the product in significant and apparent ways, and was not made intentionally.) + +A way to mitigate this situation is to test each feature separately, in its own staging deployment, and verify which change made the page background pink, and ideally, you would check each feature before merging them into the final product (merging it into the `master` branch, for example). + +To check each feature before merging would require every developer to build the new version of the server, and set up all required services: [_nginx_](https://www.nginx.com), [*Postgres*](https://www.postgresql.org), [*Redis*](https://redis.io). Developers would also have to manage access to the set up environments ― set up SSL certificates, set up subdomains, make sure databases are not exposed, and make sure the connection between every component is secure and authenticated. + +This is **a lot** of overhead just to test the color of a button. Note that most of the described work would be identical across the vast majority of features ― changing the deployment architecture is a relatively rare task in most projects. Databases, load balancing, caching, and proxying would be set up in much the same way for the majority of feature-specific stagings. The server itself is probably also compiled in exactly the same way for most features. + +_Octopod_ aims to factor out the common parts of creating and deploying a staging. + +If developers were using _Octopod_ to deploy stagings, literally the only thing needed from them would be to specify the git commit hash in the _Web UI_ of _Octopod_. The common infrastructure (shown in **blue**) which is the same across different stagings would not require any additional setup. The only difference between the two button feature staging would be the actual server that required changing the color. And that server is most likely also built in a uniform way, meaning it can be done automatically. + +![](../diagrams/images/sample_deployment.png) + +### 😖 Why have such a complicated staging deployment? + +The purpose of having a staging deployment is to verify the correctness of an implementation of a task as it would behave in a production environment. After all, deploying in a production environment is the only real goal of implementing anything. + +Having a staging deployment that is different from a production environment in any significant way can lead to unexpected behavior that was not obvious ― heavy caching of a request can lead to an inconsistent state and break a feature for example. + +## 🛠 The way Octopod is set up + +To integrate Octopod into your development workflow, a DevOps engineer needs to implement some common staging orchestration logic once for it to be reused by the whole development team. This can be done by implementing [*staging control scripts*](Control_scripts.md) in any programming language which can be executed in the Octopod environment. Statically linked executables don't depend on their environment at all, so languages such as [*Rust*](https://www.rust-lang.org) and [*Go*](https://golang.org) are a good fit. + +When stagings are managed through the *Web UI*, *Octopod* executes appropriate _staging control scripts_ behind the scenes to set up your particular environment. + +## 🎛️ CLI + +For more in-depth control over the staging cluster, we also ship a CLI with _superuser privileges_ which allows a DevOps engineer to examine deployment logs to resolve issues, should they arise. + +You can read more about the _octo CLI_ in the [octo CLI user guide](docs/../Octo_user_guide.md). + +## 🔒 Authentication in the UI + +Authentication to the _web UI_ can be set up through [_Ingress_](https://kubernetes.io/docs/concepts/services-networking/ingress/) which, for example, [supports OAuth authentication](https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth/). This allows you to set up Octopod so that every developer in your GitHub organization has access to manage stagings without any additional permissions management. + +## 🤖 Automatic deployment / CD + +It is possible to set up your existing CI to automatically deploy new versions of your feature staging, reducing friction even further. This can be done by using the CLI to update a staging with the same name as the current branch in git for example. The CLI command can be executed straight in your CI script in services like GitHub Actions, Travis CI, Azure Pipelines, etc. + +Fot more information see the [integration guide](Integration.md). + +## 📗 Glossary + +- _Octopod_ ― the deployment manager, this very system +- _octo CLI_ ― a command-line client, used to access Octopod with _superuser_ privileges +- _Octopod Server_ ― the server responsible for managing deployments +- _deployment control scripts_ ― scripts used to interact with your specific environment setup +- _web UI_ ― the interface developers, project managers, QA engineers, etc. use to manage stagings. +- _overrides_ ― a set of environment variable key-value pairs, which have precedence over the default pairs set up by DevOps engineer. These environment variables are passed to your system during deployment. diff --git a/docs/en/PM_case_study.md b/docs/en/PM_case_study.md new file mode 100644 index 0000000..07ff6b7 --- /dev/null +++ b/docs/en/PM_case_study.md @@ -0,0 +1,122 @@ +# Octopod Case Study + +A deployment model is a critical part of any IT company and is usually +deeply integrated into multiple development processes. Having a good +deployment model lets successful companies create a well-tuned +development workflow – a key to fast and reliable delivery of business +features. + +Development models may vary significantly from one company to another. +However, the problems companies are facing when picking the right model +are usually pretty common. We want new features to be tested rigorously +before they hit production and become available for end users. We also +want the team to deliver in a timely manner, which means the ideal +workflow should exclude any blockers that force one team to wait when +the other team finishes their work. + +## The two models +### The 3-tiered model + +A common approach is to have a 3-tier deployment model which implies +having Development, Staging, and Production Environments. Although there +could be some variations, basically this model is used in the majority +of development teams. + +In such a 3-tier model, the Development server usually pulls the changes +from a master branch where developers merge their changes once they’re +done with the implementation in the feature branch. Once the branch is +merged, the feature can be seen and tested on the Development server. If +the change makes the application unstable or does not work as expected, +a developer has to revert it, push the fix and then redeploy. Once the +feature is verified, the new code is integrated into a Staging +environment. + +Even though this flow is pretty common, it has few significant +downsides which we will examine later in this document. + +### The multi-staging model + +A more interesting approach is a multi-staging model where a new Staging +server is created for each new feature branch. Staging servers are +accessible by unique URLs, like my-cool-feature.staging.company.com, and +are used for both development and QA verification. Albeit from the +developer’s perspective the process is similar, with this approach each +feature can be implemented and tested independently without a need to be +merged into the master branch first, making the process of testing a +release candidate on the Pre-Production server independent. + +## Breaking features + +### The 3-tiered model + +One issue is that no matter how well developers test their feature, it +may still interact with someone else’s code in unexpected ways. It means that a successful +verification of a particular feature on the Development server does not +guarantee that the feature won’t unexpectedly break someone else’s code. +If a critical part of the application, like authentication, gets broken, +this may block the testing process completely until the problem is +resolved. And to top it off, when this happens, it might not be +immediately clear who’s change is to blame. Thus, everyone who did +recent merges begin looking into their code trying to see whether it was +their check-in that broke the server. Of course, it takes time and +blocks the development process as well until the culprit merge is found +and confirmed. + +![](../images/break1.png) + +### The multi-staging model + +With the multi-staging model every deployed staging is separated from a known-to-be-good commit by exactly one feature implementation or bug fix. This means that if at any point we discover that a staging contains a critical bug, we will for sure know which feature broke the project. The "bad" feature can be identified without delaying the merge of "good" branches. + +![](../images/break2.png) + +## Configurable testing environments + +Sometimes features require a special or dangerous environment to be tested in. For example, a feature might require handling a payment error that does not occur in the test payment processing environment, thus the feature will need to be tested in a production payment processing environment. + +### The 3-tiered model + +When QA is forced to test everything on a single staging deployment, by necessity all features tested during the deployment which includes the said feature will need to be tested in a production payment processing environment. This is bad at least because it increases the likelihood of unintentional charges and might complicate the testing of other features. + +![](../images/env1.png) + +### The multi-staging model + +With a multi-staging this issue is mitigated completely since every single feature is tested in a completely separate environment that can be set up the way the feature requires. + +![](../images/env2.png) + +## Feature development lifecycle + +### The 3-tiered model + +Having features deployed and tested in wave-like cycles in tandem with the staging server being occupied by integration tests for a given release can increase the time interval between the developer submitting a feature for testing and getting feedback from QA. This leads to the developer losing the context of the task. Furthermore, an unfortunate series of events can significantly increase the time-to-production of a feature. + +![](../images/dev1.png) + +### The multi-staging model + +With a multi-staging model the problem is lessened significantly. Allowing features to be deployed to staging environments independently reduces both the QA feedback time and the time-to-production of a feature under the same conditions. + +> **NOTE:** Relative block widths have been kept consistent with the previous diagram. + +![](../images/dev2.png) + +## Octopod for the multi-staging model + +While the multi-staging model has many upsides, having a dedicated Staging server for each new feature or bugfix usually +requires a more complicated environment and forces developers to spend +more time on deploying each piece of code. Difficulties in managing such +a swarm of servers often leads to introducing orchestration tools like +Kubernetes, which may not be easy to learn for everyone in the team, +especially when containers are built on top of AWS infrastructure which +implies using AWS-specific commands and tools. Thus, even though this +model provides a significant workflow improvement, it requires +developers to have certain DevOps expertise. + +To overcome these limitations and let more teams use multi-staging +deployment models, we created Octopod. Octopod is a service +that, once installed and set up, supports your multi-tier development +workflow at a level that does not require deep technical knowledge. Octopod +simplifies the procedure of creating new servers and allows the +implementation of common CD solutions in just a few clicks. diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000..62b4da7 --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,15 @@ +# 🐙📑 Octopod documentation + +## 🔭 High-level notes +- [🐙 Overview](Overview.md) +- [🧑‍🔬 Project managment case study](PM_case_study.md) +- [🧑‍💻 Technical case study](Tech_case_study.md) + +## 🛠️ Technical documentation +- [🏗 Technical architecture](Technical_architecture.md) +- [⚙️ Control script guide](Control_scripts.md) +- [🔧🐙 Octopod deployment guide](Octopod_deployment_guide.md) +- [🔧🚀 Helm-based Octopod project setup](Helm-based_deployment_guide.md) +- [🐙🎛 octo CLI user guide](Octo_user_guide.md) +- [🤖 CI integration](Integration.md) +- [🔒 Octopod security model](Security_model.md) diff --git a/docs/en/Security_model.md b/docs/en/Security_model.md new file mode 100644 index 0000000..c29c223 --- /dev/null +++ b/docs/en/Security_model.md @@ -0,0 +1,114 @@ +# Security model + +
+ Table of contents + +- [Octopod roles](#octopod-roles) +- [Kubernetes role-based access control](#kubernetes-role-based-access-control) + - [Privileges to delete certificates](#privileges-to-delete-certificates) + - [Privileges to delete _Persistent Volumes Claims_](#privileges-to-delete-persistent-volumes-claims) +- [Web UI authentication](#web-ui-authentication) +- [Web UI OAuth](#web-ui-oauth) +- [octo CLI authentication](#octo-cli-authentication) + +
+ +## Octopod roles + +There are two user roles in _Octopod_: +* _user_ +* _admin_ + +| role | managing deployments | viewing deployment logs | +| :---: | :------------------: | :---------------------: | +| user | ✅ | ❌ | +| admin | ✅ | ✅ | + +_Web UI_ users have the _user_ role. + +_octo CLI_ users have the _admin_ role. + +There is currently no way to give someone access to _octo CLI_ without giving them the _admin_ role since authentication is done through SSL certificates instead of through OAuth. + +## Kubernetes role-based access control + +_Octopod Server_ is deployed in the `octopod` _Kubernetes_ namespace. Deployments are deployed in the `deployments` namespace. +_Octopod Server_ uses the `octopod` [_Service Account_][kubernetes-service-account]. + +Freeing resources might require _Octopod Server_ / _control scripts_ to have privileges to delete certificates and [_Persistent Volumes Claims_][kubernetes-pvc]. (It depends on the specifics of the _Kubernetes_ setup and _control scripts_) + +Access can be configured through [_RBAC_][kubernetes-rbac]: + +### Privileges to delete certificates + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cert-control-clusterrole +rules: + - apiGroups: ["cert-manager.io"] + resources: ["certificates"] + verbs: ["list", "delete", "deletecollection"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: octopod-cert-control-rolebinding + namespace: deployments +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: cert-control-clusterrole +subjects: + - kind: ServiceAccount + name: octopod + namespace: octopod +``` + +### Privileges to delete _Persistent Volumes Claims_ + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pvc-control-clusterrole +rules: + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["list", "delete", "deletecollection"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: octopod-pvc-control-rolebinding + namespace: deployments +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: pvc-control-clusterrole +subjects: + - kind: ServiceAccount + name: octopod + namespace: octopod +``` + +## Web UI authentication + +Authentication between the _Web UI_ and _Octopod Server_ is done through _Basic Auth_. The _Bearer token_ is read by the _Web UI_ after the page is loaded as part of [the config](../../charts/octopod/templates/octopod-nginx-configmap.yaml#L15-L20). By default, everything, including the config, can be accessed without any authentication. For ways of mitigating this please see the next section. + +## Web UI OAuth + +The [_Web UI_](Technical_architecture.md#-web-ui) on its own does not have any authentication whatsoever, meaning that anyone can open it and manage your deployments. Luckily, _Kubernetes_ [can be configured](../../charts/octopod/templates/octopod-ingress.yaml#L15-L21) to authenticate users before they get access to the _Web UI_. It can be set up to authenticate users through [_Ingress_](https://kubernetes.io/docs/concepts/services-networking/ingress/) which [supports external authentication services][kubernetes-ingress-nginx-external-auth]. You can set up [_OAuth2 Proxy_][oauth2-proxy] in your cluster to support numerous OAuth services. For example, if you use GitHub, you can set up [_OAuth2 Proxy_][oauth2-proxy] to use GitHub to automatically grant users access to Octopod when you add them to your organization in GitHub. + +## octo CLI authentication + +Authentication between _octo CLI_ and _Octopod Server_ is done through an SSL certificate that is generated [when deploying _Octopod_](../en/Octopod_deployment_guide.md#creating-ssl-certificates). + +[kubernetes-service-account]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account +[kubernetes-rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac +[kubernetes-pvc]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#expanding-persistent-volumes-claims +[kubernetes-ingress-nginx-external-auth]: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#external-authentication +[oauth2-proxy]: https://oauth2-proxy.github.io/oauth2-proxy diff --git a/docs/en/Tech_case_study.md b/docs/en/Tech_case_study.md new file mode 100644 index 0000000..2158d17 --- /dev/null +++ b/docs/en/Tech_case_study.md @@ -0,0 +1,74 @@ +# Octopod technical case study + +
+ Table of contents + +- [Single staging model](#single-staging-model) +- [The multi-staging model](#the-multi-staging-model) +- [Implications](#implications) + - [Isolation – separate configurations](#isolation--separate-configurations) + - [Isolation – more information](#isolation--more-information) + - [Isolation – independent state](#isolation--independent-state) + - [Isolation – freedom to experiment](#isolation--freedom-to-experiment) + - [Infrastructure reuse](#infrastructure-reuse) + - [Continuous staging deployment](#continuous-staging-deployment) + +
+ +In this case study, we consider developing a system that is mostly comprised of a server. + +## Single staging model + +This model implies having a single machine (physical or virtual) which is considered a staging environment. This single machine essentially becomes the contact point between Developers and QA engineers. + +This in essence means that at any given point a single build of the service in a single configuration is available to the QA engineers. + +## The multi-staging model + +This model implies having a separate machine (physical or virtual) for *every* developed feature. Of course, deployments that are no longer needed will be removed. + +This means that QA engineers can have access to an indefinite amount of independent builds and configurations of the product. + +## Implications + +### Isolation – separate configurations + +It is sometimes the case that certain features need a "special" environment to be tested in, for example testing a feature might require payment errors to occur that only happen in a production payment processing environment. Having a single staging will require either not testing the said feature, or testing **all** "current" features with a production payment processor. This might prove to be either more difficult than necessary, or might lead to other undesirable consequences. + +"Current" is used here to denote features that have been completed after the last time QA engineers had the opportunity to test the staging of the given project. It is assumed that the completed features are merged into something analogous to a `master` branch in git. + +A way to mitigate the issue might be to have fine-grained control over what features are allowed to be merged into the `master` branch so that when we deploy it, we will have only the one feature which needs the "special" environment, in a sense, batching features based on the environment they require. The main problem with this is that the payment processor is in all likelihood a tiny sliver, a single dimension of the whole space of possible environment setups. Trying to capture and institutionalize the whole breadth of a production environment in your development workflow seems like a futile endeavor. + +Having per-feature stagings allow you to have completely separate and independent configurations for every single feature you develop and test at no additional cost. + +### Isolation – more information + +Sometimes making a change in one part of the codebase can lead to unexpected behavior in seemingly unrelated parts of the project. For example, an implementation of a feature might break the login form altogether. + +With a single staging after discovering that the newly deployed version of your project breaks the login form, you will need to inspect every single feature implemented since the last deployment of your staging environment to find the offending feature. Depending on your workflow, there can be a large number of features. + +With per-feature stagings, you will know precisely what change broke the login form since every single deployment would differ from a known-to-be-stable version by *exactly* one feature implementation in most cases – if you detect an unexpected breakage on one of the stagings, then you know exactly which feature broke it. + +### Isolation – independent state + +Some developed features might require some intricate and fragile state to be properly tested, for example, you might want something special for your 1000th customer each day. (This is a very contrived example. In reality, there will be numerous more subtle situations.) With a single staging environment, it might be very easy to trigger the desired state accidentally while testing a different feature. Having per-feature stagings implies that every staging has a separate state – this problem is mitigated. + +As a bonus, it reduces the amount of data on the staging – this might make reproducing found bugs easier for the developer since the database would have orders of magnitude less data, and only the data relevant to the feature will be present. + +### Isolation – freedom to experiment + +Sometimes it can be useful for a business to test out an idea that might end up not being suitable for production at this time for whatever reason. (We are assuming that the feature is such that this becomes obvious only after implementing a prototype.) + +When you have a single staging, the only real way to test out an idea is to merge into the rest of the codebase and deploy it to the staging in the usual way. If the feature is deemed an improvement – all is well, just continue development. If it is decided that the feature should not be pushed to production, you now need to "unmerge" the changes from the rest of the codebase. Depending on the timeline, other features might have been implemented that rely on the code. In most cases rolling a feature back is a non-trivial task. + +With per-feature stagings, you could, in essence, make a branch of your whole product, and experiment with it however you like without fear that the changes will be relied upon by the rest of the team. Rolling back a feature becomes as easy as removing the git branch. + +### Infrastructure reuse + +When setting up Octopod you will need to implement the exact steps required to set up your whole project infrastructure in a reliable and reproducible way. This clarity, which is otherwise most likely absent due to ad-hoc deployment procedures, allows you to bootstrap new projects quickly. New projects will likely reuse some of the technologies your teams have experience with on other projects; you will be able to easily copy over the already implemented parts of the infrastructure. + +### Continuous staging deployment + +It is easy to set up continuous deployment procedures with Octopod. (Continuous deployment here refers to the process of automatically deploying branches of your repository when they are updated.) This allows you to optimize your development workflow even further. + +A not-so-obvious usage might be to set up a staging to be automatically updated with the `master` branch of your project repository. This has the advantage that anyone can at any time easily test the behavior of the `master` branch. diff --git a/docs/en/Technical_architecture.md b/docs/en/Technical_architecture.md new file mode 100644 index 0000000..04ebd0a --- /dev/null +++ b/docs/en/Technical_architecture.md @@ -0,0 +1,292 @@ +# Technical architecture + +
+ Table of contents + +- [⚒️ Used tools](#️-used-tools) +- [📐 App architecture](#-app-architecture) + - [🖥 Web UI](#-web-ui) + - [🐙 Octopod Server](#-octopod-server) + - [🐘 PostgreSQL](#-postgresql) + - [🎛 octo CLI](#-octo-cli) + - [📑 Control scripts](#-control-scripts) + - [🚮⏲ Clean Archive CronJob](#-clean-archive-cronjob) + - [Kube API Server](#kube-api-server) +- [📦 Octopod Distribution model](#-octopod-distribution-model) +- [Process view](#process-view) + - [✨ Create](#-create) + - [🔧 Update](#-update) + - [🗃 Archive](#-archive) + - [🚮 Cleanup](#-cleanup) + - [🔁 Restore](#-restore) +- [👨‍💻👩‍💻 How we use it](#-how-we-use-it) +- [🗂️ Deployment state transitions](#️-deployment-state-transitions) + +
+ +## ⚒️ Used tools + +The main goal of _Octopod_ is to simplify deployment in [_Kubernetes_][kube]. + +When developing _Octopod_ we were expecting _Octopod_ itself to also be deployed with [_Kubernetes_][kube]. + +## 📐 App architecture + +Users can interact with _Octopod_ through: +1. the [_Web UI_](#-web-ui) (expected to be used by developers, project managers, QA engineers, etc.) +2. the [_octo CLI_](#-octo-cli) (expected to be used by DevOps engineers and programmatically, e. g. on CI) + +Interaction between _Octopod_ and [_Kubernetes_][kube] is done entirely through the [_control scripts_](#-control-scripts). This allows _Octopod_ to be adapted for use in practically any deployment setup. + +_Octopod_ stores all data about deployments and all performed operations in [_PostgreSQL_](#-postgresql). + +![App architecture](../diagrams/images/app-architecture.png) + +### 🖥 Web UI + +_Web UI_ – the user interface used to manipulate deployments. It interacts with [_Octopod Server_](#-octopod-server) through HTTP/1.1 requests and receives events from [_Octopod Server_](#-octopod-server) through _Websockets_. Authentication between _Web UI_ and [_Octopod Server_](#-octopod-server) is done through Basic Auth. The Basic Auth token is read from a *JSON* config which is requested when the page is loaded. Access to the config should be configured through [_Ingress_][ingress]. + +The interface does not contain technical details related to administering deployments ― managing deployments is done in a simple way. The interface is geared towards being used by developers of any level, QA engineers, project managers, and people without a technical background. + +### 🐙 Octopod Server + +_Octopod Server_ – the server that processes deployment management requests and delegates [_Kubernetes_][kube]-specific logic to [_control scripts_](#-control-scripts). + + +The server receives commands from the [_octo CLI_](#-octo-cli) and the [_Web UI_](#-web-ui) through HTTP/1.1 and updates the state of deployments. The server also sends updates to the [_Web UI_](#-web-ui) through _Websockets_. _Octopod Server_ interacts with [_Kube API Server_](#kube-api-server) through the [_control scripts_](#-control-scripts). Settings, deployment states and user action logs are stored in [_PostgreSQL_](#-postgresql). + +### 🐘 PostgreSQL + +[_PostgreSQL_](https://www.postgresql.org) – DBMS used to store settings, deployment states and user action logs. + +### 🎛 octo CLI + +_octo CLI_ – a command-line interface used to manage deployments. It sends HTTP/1.1 requests to [_Octopod Server_](#-octopod-server). The requests are [authenticated through SSL certificates](Security_model.md#octo-cli-authentication). + +It can perform all actions available in the [_Web UI_](#-web-ui), but also has access to view deployment logs. + +The CLI is expected to be used by DevOps engineers, but can also be used if it is necessary to automate deployment management in some way, for example [in CI scripts](Integration.md). + +### 📑 Control scripts + +_Control scripts_ – a _Docker Container_ with executables which encapsulates all of the logic of interacting with the [_Kube API Server_](#kube-api-server), cloud providers, deployments, version control, etc. + +This is necessary to make _Octopod_ itself independent from any particular deployment setup ― it can be set up to work with practically any setup. + +When the [_Octopod Server_](#-octopod-server) _Pod_ starts, the contents of the *control scripts* container are copied into the _Octopod Server_ container file system. This means that the executables need to be either statically linked or interpreted through _Bash_ since it needs to be executed in the _Octopod Server_ container environment. + +These [scripts need to be implemented](Control_scripts.md) to deploy _Octopod_. + +### 🚮⏲ Clean Archive CronJob + +_Clean Archive CronJob_ – a CronJob which is run every hour. It deletes archived deployments older than 14 days. It is done by calling [_octo CLI_](#-octo-cli). + +It is necessary because "deleting" (archiving) deployments should only free the occupied computational resources, _Persistent Volumes_ should not be freed when deleting a deployment. This gives us a window of time in which a deployment can be recovered in the state it was in before being archived. + +### Kube API Server + +[Kube API Server](https://kubernetes.io/docs/concepts/overview/kubernetes-api/) – an API server in [_Kubernetes_][kube] which should be called from [_control scripts_](#-control-scripts). + +## 📦 Octopod Distribution model + +[_octo CLI_](#-octo-cli) is distributed as a *statically linked executable*. The prebuilt binaries can be found in the "Releases" tab of the GitHub repository. + +[_Octopod Server_](#-octopod-server) and [_Web UI_](#-web-ui) are distributed as a single _Docker Image_. [_Charts_][chart] are used [to deploy](Octopod_deployment_guide.md) it in [_Kubernetes_][kube]. + +A _Docker Image_ with [_control scripts_](#-control-scripts) should be provided by the user. They are available in [our _Docker Hub_ registry](https://hub.docker.com/orgs/typeable/repositories). + +## Process view + +Here we provide sequence diagrams for every basic operation that can be performed in _Octopod_. These operations call [_control scripts_](#-control-scripts). On the diagrams, they are labeled as _ControlScripts_. + +### ✨ Create + +_Create_ – creates a new deployment. The main inputs include the name of the deployment, the _Docker Image tag_ and optional overrides. A more detailed description can be found in the [control scripts documentation](Control_scripts.md#-create). + +The arguments are forwarded to the [_create_](Control_scripts.md#-create) script which in turn creates the deployment in the _Kubernetes cluster_. It might call something like: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +
+ Create via CLI sequence diagram + +![Create](../diagrams/images/technical-architecture-create-via-cli.png) + +
+ +
+ Create via UI sequence diagram + +![Create](../diagrams/images/technical-architecture-create-via-ui.png) + +
+ +### 🔧 Update + +_Update_ – updates an existing deployment. The main inputs include the name of the deployment, the _Docker Image tag_ and optional overrides. A more detailed description can be found in the [control scripts documentation](Control_scripts.md#-update). + +[_Overrides_](Overview.md#overrides) are read from the database and merged with the new changes. All arguments are forwarded to the [_update_](Control_scripts.md#-update) script which in turn updates the specified deployment with the new parameters in the _Kubernetes cluster_. It might call something like: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + + +
+ Update via CLI sequence diagram + +![Update](../diagrams/images/technical-architecture-update-via-cli.png) + +
+ +
+ Update via UI sequence diagram + +![Update](../diagrams/images/technical-architecture-update-via-ui.png) + +
+ +### 🗃 Archive + +_Delete_ – archives a deployment. It should only free the computational resources (_Pods_). _Persistent Volumes_ should not be deleted ― they are cleared in the [_cleanup_](#-cleanup) process. This operation can be undone with the [_restore_](#-restore) command. + +The main argument is the name that identifies the deployment. A more detailed description can be found in the [control scripts documentation](Control_scripts.md#-archive). + +The arguments are forwarded to the [_delete_](Control_scripts.md#-archive) script which in turn frees the computational resources. It might call something like: + +```bash +helm delete "$name" --purge +``` + +
+ Archive via CLI sequence diagram + +![Archive](../diagrams/images/technical-architecture-archive-via-cli.png) + +
+ +
+ Archive via UI sequence diagram + +![Archive](../diagrams/images/technical-architecture-archive-via-ui.png) + +
+ +### 🚮 Cleanup + +_Cleanup_ – releases **all** resources captured by the deployment. + +The main argument is the name that identifies the deployment. A more detailed description can be found in the [control scripts documentation](Control_scripts.md#-cleanup). It can only be called after [_archive_](#-archive) has been executed. + +The arguments are forwarded to the [_cleanup_](Control_scripts.md#-cleanup) script which in turn frees all resources captured by the given deployment. It might call something like: + +```bash +kubectl delete pvc -n "$namespace" "$name-postgres-pvc" +kubectl delete certificate -n "$namespace" "$name-postgres-cert" +``` + +
+ Cleanup via CLI sequence diagram + +![Cleanup](../diagrams/images/technical-architecture-cleanup-via-cli.png) + +
+ +
+ Cleanup via UI sequence diagram + +![Cleanup](../diagrams/images/technical-architecture-cleanup-via-ui.png) + +
+ +### 🔁 Restore + +_restore_ – restores an archived deployment in the state it was last in. Calls the same _script_ that is called in [_create_](#-create). + +The main argument is the name that identifies the deployment. A more detailed description can be found in the [control scripts documentation](Control_scripts.md#-create). It can only be called after [_archive_](#-archive) has been executed. + +All necessary setup information is read from the database: [_overrides_](Overview.md#overrides) and the _Docker Image tag_. The arguments are forwarded to the [_create_](Control_scripts.md#-create) script which in turn recreates the deployment. It might call something like: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +
+ Restore via CLI sequence diagram + +![Restore](../diagrams/images/technical-architecture-restore-via-cli.png) + +
+ +
+ Restore via UI sequence diagram + +![Restore](../diagrams/images/technical-architecture-restore-via-ui.png) + +
+ +## 👨‍💻👩‍💻 How we use it + +We deploy several separate [_Kubernetes_][kube] clusters: +- We have separate clusters for every product we deploy +- We also separate _production_ and _staging_ clusters + +That makes two clusters per product. + +So we get a cluster matrix similar to the following table, where each cell is a separate cluster: + +| | Staging (Has _Octopod_ installed) | Production (_Octopod_ not installed) | +| ------------------- | --------------------------------- | ------------------------------------ | +| **Cactus shop** | 🟩 🐙 Cluster *A* | 🟨 Cluster *B* | +| **Pottery service** | 🟦 🐙 Cluster *C* | 🟪 Cluster *D* | +| ... | ... | ... | + +Every color depicts a separate cluster. A 🐙 indicates that _Octopod_ is installed in that cluster. + +Every _staging_ cluster has a separate _Octopod_ installation with separate interfaces to manage the deployments. + +## 🗂️ Deployment state transitions + +A deployment can exist in one of six states: +1. *Running* +2. *Failure* +3. *CreatePending* +4. *UpdatePending* +5. *DeletePending* +6. *Archived* + +_Running_, _Failure_, _Archived_ states are "permanent", meaning the deployment is not in the process of executing a command. + +*CreatePending*, *UpdatePending*, *DeletePending* states are temporary, meaning the deployment is currently in the process of executing a deployment command. + +![Deployment Statuses](../diagrams/images/technical-architecture-deployment-states-fsm.png) + + + + +[kube]: https://kubernetes.io +[chart]: https://helm.sh/docs/topics/charts/ +[ingress]: https://kubernetes.io/docs/concepts/services-networking/ingress/ diff --git a/docs/images/break1.png b/docs/images/break1.png new file mode 100644 index 0000000..38a595c Binary files /dev/null and b/docs/images/break1.png differ diff --git a/docs/images/break2.png b/docs/images/break2.png new file mode 100644 index 0000000..6fb6a02 Binary files /dev/null and b/docs/images/break2.png differ diff --git a/docs/images/dev1.png b/docs/images/dev1.png new file mode 100644 index 0000000..989d392 Binary files /dev/null and b/docs/images/dev1.png differ diff --git a/docs/images/dev2.png b/docs/images/dev2.png new file mode 100644 index 0000000..3a513a7 Binary files /dev/null and b/docs/images/dev2.png differ diff --git a/docs/images/env1.png b/docs/images/env1.png new file mode 100644 index 0000000..b26a420 Binary files /dev/null and b/docs/images/env1.png differ diff --git a/docs/images/env2.png b/docs/images/env2.png new file mode 100644 index 0000000..d159d31 Binary files /dev/null and b/docs/images/env2.png differ diff --git a/docs/images/first.png b/docs/images/first.png new file mode 100644 index 0000000..dab821d Binary files /dev/null and b/docs/images/first.png differ diff --git a/docs/images/hello-octopod-1.png b/docs/images/hello-octopod-1.png new file mode 100644 index 0000000..5c08e05 Binary files /dev/null and b/docs/images/hello-octopod-1.png differ diff --git a/docs/images/hello-octopod-2.png b/docs/images/hello-octopod-2.png new file mode 100644 index 0000000..12d371c Binary files /dev/null and b/docs/images/hello-octopod-2.png differ diff --git a/docs/images/hello-octopod-3.png b/docs/images/hello-octopod-3.png new file mode 100644 index 0000000..f249ec4 Binary files /dev/null and b/docs/images/hello-octopod-3.png differ diff --git a/docs/images/second.png b/docs/images/second.png new file mode 100644 index 0000000..98f6d74 Binary files /dev/null and b/docs/images/second.png differ diff --git a/docs/ru/Control_scripts.md b/docs/ru/Control_scripts.md new file mode 100644 index 0000000..eb7b041 --- /dev/null +++ b/docs/ru/Control_scripts.md @@ -0,0 +1,209 @@ +# Control scripts + +
+ Table of contents + +- [create](#create) +- [update](#update) +- [archive](#archive) +- [check](#check) +- [cleanup](#cleanup) +- [archive_check](#archive_check) +- [tag_check](#tag_check) +- [init](#init) +- [info](#info) + +
+ +## create + +Реализация создания нового развертывания. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя развертывания +* `--tag` – тег развертывания +* `--app-env-override` – override уровня приложения (формат значения `FOO=BAR`, может быть передано 0 или более раз) +* `--deployment-override` – override уровня развертывания (формат значения `FOO=BAR`, может быть передано 0 или более раз) + +Успешность операции определяется по exit code (`0` – успех). + +Например `create` с полученными аргументами может выполнять: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +## update + +Реализация обновления развертывания. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя развертывания +* `--tag` – тег развертывания +* `--app-env-override` – override уровня приложения (формат значения `FOO=BAR`, может быть передано 0 или более раз) +* `--deployment-override` – override уровня развертывания (формат значения `FOO=BAR`, может быть передано 0 или более раз) + +Успешность операции определяется по exit code (`0` – успех). + +Например `update` с полученными аргументами может выполнять: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +## archive + +Реализация удаления развертывания. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя развертывания + +Успешность операции определяется по exit code (`0` – успех). + +Например `archive` с полученными аргументами может выполнять: + +```bash +helm delete "$name" --purge +``` + +## check + +Реализация проверки состояния развертывания. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя развертывания + +Успешность операции определяется по exit code (`0` – успех). + +Например `check` с полученными аргументами может выполнять: + +```bash +echo "{\"Deployments\": [{\"ResourceName\": \"app-${name}\", \"Namespace\": \"${namespace}\"}], \"StatefulSets\": [{\"ResourceName\": \"db-${name}\", \"Namespace\": \"${namespace}\"}]}" | \ + kubedog multitrack -t 3 +``` + +В примере выше используется [_Kubedog_][kubedog]. +Предполагается, что пользователь сам установит [_Kubedog_][kubedog]: сохранит его в `/` контейнера с _Control scripts_ или установит его в `$HOME` из [init](#init). + +## cleanup + +Реализация очистки ресурсов развертывания. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя развертывания + +Успешность операции определяется по exit code (`0` – успех). + +Например `cleanup` с полученными аргументами может выполнять: + +```bash +kubectl delete pvc -n $namespace -l "app=$name" +``` + +## archive_check + +Реализация проверки успешности удаления развертывания. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя развертывания + +Успешность операции определяется по exit code (`0` – успех). + +Например `archive_check` с полученными аргументами может выполнять: + +```bash +helm status $name +``` + +## tag_check + +Реализация проверки существования тега развертывания. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя развертывания +* `--tag` – тег развертывания + +Успешность операции определяется по exit code (`0` – успех). + +## init + +Реализация инициализации рабочего окружения для будущих запусков *Control Scripts*. + +В этом скрипте предлагается настроить доступ к системам контроля версий, облачным провайдерам и т.д. +Для этого можно сохранить настройки в файлы в $HOME. + +Этот скрипт однократно запускается во время создания пода `Octopod Server`. + +Успешность операции определяется по exit code (`0` – успех). + +Например `init` может выполнять: + +```bash +mkdir $HOME/.ssh +echo -e "Host github.com\nHostname github.com\nPort 22\nUser git\nIdentityFile $HOME/.ssh/deploy.key" > $HOME/.ssh/config +echo "MY_DEPLOY_KEY" > $HOME/.ssh/deploy.key" +``` + +## info + +Реализация получения метаданных о deployment. Содержимое _stdout_ будет использованно в качестве дополнительной информации о deployment в _CLI/UI_. +Ожидаемый формат _stdout_ ― _CSV_ `ключ,значение`: + +``` +app,https://foo.example.com +api,https://api.foo.example.com +``` + +Ожидается, что `ключ` не может содержать символ `,`, ограничений на `значение` нет. Пробельные символы остаются без изменений. + +Получает на вход следующие аргументы: +* `--project-name` – название проекта +* `--base-domain` – базовый домен +* `--namespace` – namespace +* `--name` – имя стейджинга + +Успешность операции определяется по exit code (`0` – успех). + +Например `info` с полученными аргументами может выполнять: + +```bash +echo "app,https://${name}.example.com" +echo "api,https://api.${name}.example.com" +``` + +[kubedog]: https://github.com/werf/kubedog diff --git a/docs/ru/Helm-based_deployment_guide.md b/docs/ru/Helm-based_deployment_guide.md new file mode 100644 index 0000000..5e47eb6 --- /dev/null +++ b/docs/ru/Helm-based_deployment_guide.md @@ -0,0 +1,165 @@ +# Helm-based deployment guide + +## Подгодовка + +Предполагается, что у вас в системе уже установлен Docker, +а также Вы установили _Octopod_. Если Вы _Octopod_ не установили, то пожалуйста воспользуйтесь [инструкцией по установке _Octopod_](Octopod_deployment_with_K8S.md). +При установке _Octopod_ потребуется указать название образа с [control scripts](Control_scripts.md): +```bash +--set "global.utils_image_prefix=typeable" \ +--set "global.utils_image=octopod-helm-example" \ +--set "global.utils_image_tag=1.0" \ +``` + +В итоге должно получиться что-то вроде: + +```bash +helm upgrade octopod ./octopod \ + --install \ + --namespace octopod \ + --set "global.deploy_checksum=$sha256_sum" \ + --set "global.image_prefix=typeable" \ + --set "global.image_tag=1.0" \ + --set "global.utils_image_prefix=typeable" \ + --set "global.utils_image=octopod-helm-example" \ + --set "global.utils_image_tag=1.0" \ + --set "global.acme_registration_email=certbot@example.com" \ + --set "global.auth_url=https://oauth.exmaple.com/oauth2/auth" \ + --set "global.auth_signin=https://oauth.exmaple.com/oauth2/start?rd=/redirect/$http_host$request_uri" \ + --set "project_name=MyProject" \ + --set "domain=octopod.example.com" \ + --set "app_domain=octopod-app.example.com" \ + --set "power_app_domain=octopod-power-app.example.com" \ + --set "ws_domain=octopod-ws.example.com" \ + --set "base_domain=example.com" \ + --set "status_update_timeout=600" \ + --wait \ + --timeout 600 \ + --debug +``` + +Для того чтобы развернуть приложение с помощью _Octopod_ потребуется само приложение и набор [control scripts](Control_scripts.md). +Разворачивать можно через _Web UI_ или _octo CLI_. В этом примере мы будем использовать только _octo CLI_. +Также потребуется Docker для запуска контейнера с _octo CLI_. +Перед использованием _octo CLI_ необходимо настроить доступ к _Octopod_, пожалуйста воспользуйтесь [руководством пользователя _octo CLI_](Octo_user_guide.md). + +В качестве примера мы будем использовать простое веб приложение, которое обслуживает единственный endpoint `/`. +По запросу на `/` будет возвращаться список переменных окружения, имена которых начинаются с `APP_ENV`, в HTML разметке. +С исходным кодом приложения можно ознакомиться, перейдя по ссылке [web-app](../../examples/web-app). +Это приложение было упаковано в докер образ и отправлено в регистри: +```bash +docker build -f docker/Dockerfile -t typeable/octopod-web-app-example:v1 . +docker push typeable/octopod-web-app-example:v1 +``` + +Также мы будем производить обновление приложения до более новой версии. +В новой версии изменена HTML разметка: каждая переменная окружения выводится не в отдельном `div`, а в виде элемента списка `li`. +С исходным кодом обновленного приложения можно ознакомиться, перейдя по ссылке [web-app-v2](../../examples/web-app-v2). +Это приложение было упаковано в докер образ и отправлено в регистри: +```bash +docker build -f docker/Dockerfile -t typeable/octopod-web-app-example:v2 . +docker push typeable/octopod-web-app-example:v2 +``` + +Для работы веб приложению нужен 1 TLS сертификат для каждого развертывания. [_Cert Manager_][cert-manager] создает сертификаты через [_Let’s Encrypt_][lets-encrypt]. +У [_Let’s Encrypt_][lets-encrypt] есть лимиты на создание сертификатов [_Let’s Encrypt Rate Limits_][lets-encrypt-rate-limits]. +При достижении лимита `too many registrations for this IP` может помочь перемещение Pod [_Cert Manager_][cert-manager] на другую ноду. + +Набор [control scripts](Control_scripts.md) будет использовать helm v2.x, kubectl, kubedog для управления и проверки статуса деплойментов. +С исходным кодом [control scripts](Control_scripts.md) можно ознакомиться, перейдя по ссылке [helm-based-control-scripts](../../examples/helm-based-control-scripts). +Набор [control scripts](Control_scripts.md) был упакован в докер образ и отправлен в регистри: + +```bash +docker build -f docker/Dockerfile -t typeable/octopod-helm-example:1.0 . +docker push typeable/octopod-helm-example:1.0 +``` +**Примечание:** +В нашем примере используется Stateless приложение упакованное в один chart. +Так что для создания и обновления необходимо выполнить один набор команд (`helm upgrade --install ...`). +Поэтому [control scripts](Control_scripts.md) реализован только `create`, и он же используется вместо `update`. +В случае более сложного приложения упакованного в несколько chart-ов, возможно потребуется выполнить различный набор команд, +это потребует реализации различных `create` и `update` скриптов. + +## Создание нового deployment + +Для создания достаточно выполнить +```bash +octo create -n hello-octopod -t v1 -e APP_ENV_KEY1=VALUE1 +``` + +- `-n hello-octopod` ― название deployment. +- `-t v1` ― тег deployment. +- `-e APP_ENV_KEY1=VALUE1` ― переменная окружения уровня приложения. + +Эта команда вызовет `create` из [control scripts](Control_scripts.md), a `create` вызовет `helm`. +Спустя некоторое время Вы увидите созданный _Pod_ нового deployment +``` +$ kubectl get pods -n deployment +NAME READY STATUS RESTARTS AGE +app-hello-octopod-8965856-qbwvq 1/1 Running 0 15s +``` + +Если откроете в браузере `http://hello-octopod.<домен, указанный при развертывании Octopod>` Вы увидите + +![После создания](../images/hello-octopod-1.png) + +## Добавление новой переменной окружения уровня приложения + +Для добавления новой переменной уровня приложения достаточно выполнить +```bash +octo update -n hello-octopod -t v1 -e APP_ENV_KEY2=VALUE2 +``` + +- `-n hello-octopod` ― название deployment. +- `-t v1` ― тег deployment. +- `-e APP_ENV_KEY2=VALUE2` ― переменная окружения уровня приложения. + +Эта команда вызовет `create` из [control scripts](Control_scripts.md), `create` вызовет `helm` (Смотри примечание раздела _Подготовка_). +Спустя несколько секунд, если откроете в браузере `http://hello-octopod.<домен указанный при развертывании Octopod>` Вы увидите новую переменную окружения уровня приложения: + +![После установки новой переменной окружения уровня приложения](../images/hello-octopod-2.png) + +## Обновление версии приложения + +Для обновления приложения до новой версии достаточно выполнить +```bash +octo update -n hello-octopod -t v2 +``` + +- `-n hello-octopod` ― название deployment. +- `-t v2` ― тег deployment. + +Эта команда вызовет `create` из [control scripts](Control_scripts.md), `create` вызовет `helm` (Смотри примечание раздела _Подготовка_). +Спустя несколько секунд, если откроете в браузере `http://hello-octopod.<домен указанный при развертывании Octopod>` Вы увидите обновленную HTML разметку: + +![После создания](../images/hello-octopod-3.png) + +## Увеличение количества реплик через установку переменной окружения уровня deployment + +Для добавления новой переменной достаточно выполнить +```bash +octo update -n hello-octopod -t v2 -o replicas=3 +``` + +- `-n hello-octopod` ― название deployment. +- `-t v1` ― тег deployment. +- `-o replicas=3` ― переменная окружения уровня deployment. + +Эта команда вызовет `create` из [control scripts](Control_scripts.md), `create` вызовет `helm` (Смотри примечание раздела _Подготовка_). + +Спустя некоторое время Вы увидите, что количество _Pod_-ов стало 3 +```bash +$ kubectl get pods -n deployment +NAME READY STATUS RESTARTS AGE +app-hello-octopod-8965856-qbwvq 1/1 Running 0 97m +app-hello-octopod-8965856-v585c 1/1 Running 0 15s +app-hello-octopod-8965856-v88md 1/1 Running 0 15s +``` + +Если откроете в браузере `http://hello-octopod.<домен указанный при развертывании Octopod>`, Вы увидите, что приложение продолжает обсуживать запросы: + +![После обновления переменной окружения уровня deployment](../images/hello-octopod-3.png) + +[cert-manager]: https://cert-manager.io/docs +[lets-encrypt]: https://letsencrypt.org +[lets-encrypt-rate-limits]: https://letsencrypt.org/docs/rate-limits diff --git a/docs/ru/Octo_user_guide.md b/docs/ru/Octo_user_guide.md new file mode 100644 index 0000000..454fb10 --- /dev/null +++ b/docs/ru/Octo_user_guide.md @@ -0,0 +1,173 @@ +# Octo user guide + +_octo CLI_ – консольная утилита для управления deployments через отправку команд в _Octopod Server_. + +## Environment variables + +`OCTOPOD_URL` – URL к _Octopod Server_, например `https://octopod-power-app.example.com:443`, **необходимо определить, иначе _octo CLI_ не будет работать**. + +`TLS_CERT_PATH` – путь к файлу сертификата, который используется для аутентификации в _Octopod Server_, если не определенно, то используется `./cert.pem`. + +`TLS_KEY_PATH` – путь к файлу ключа, который используется для аутентификации в _Octopod Server_, если не определенно, то используется `./key.pem`. + +## Subcommands + +### create + +Создать новый deployment. + +Аргументы: + +* `-n` или `--name` – название deployment +* `-t` или `--tag` – тег deployment +* `-e` или `--set-app-env-override` – override уровна приложения, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз +* `-o` или `--set-deployment-override` – override увроня deployment, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз + +Пример: + +```bash +$ octo create -n hello-octopod -t ca5fd1fe08389f6422a506a59b68a5272ac37ba6 -e KEY1=VALUE1 -e KEY2=VALUE2 +``` + +### list + +Получить список всех deployments. + +Данная subcommand не требует передачи аргументов. + +Пример: + +```bash +$ octo list +hello-octopod +``` + +### archive + +Удалить deployment. + +Аргументы: + +* `-n` или `--name` – название deployment + +Пример: + +```bash +$ octo archive -n hello-octopod +``` + +### update + +Обновить deployment. + +Аргументы: + +* `-n` или `--name` – название deployment +* `-t` или `--tag` – тег deployment +* `-e` или `--set-app-env-override` – override уровна приложения, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз +* `-E` или `--unset-app-env-override` – override уровна приложения, которое надо удалить, формат: КЛЮЧ, можно использовать 0 и более раз +* `-o` или `--set-deployment-override` – override увроня deployment, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз +* `-O` или `--unset-deployment-override` – override увроня deployment, которое надо удалить, формат: КЛЮЧ, можно использовать 0 и более раз + +Пример: + +```bash +$ octo update -n octopod -t 015f16ecf398fcadaac508c1855ae160af0969c4 -E KEY1 -e KEY2=VALUE22222 +``` + +### info + +Получить информацию о deployment. + +Аргументы: + +* `-n` или `--name` – название deployment + +Пример: + +```bash +$ octo info -n hello-octopod +Current settings: +tag: v1 +application overrides: app=1 (Public) + +deployment overrides: dep=2 (Public) + +metadata: + app: https://ree.lvh.me + +Last logs: +┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓ +┃ Created at ┃ Action id ┃ Action ┃ Tag ┃ App overrides ┃ Deployment overrides ┃ Exit code ┃ +┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩ +│ 2020-11-02T17:14:03 │ 7 │ create │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │ +├─────────────────────┼───────────┼────────┼─────┼────────────────┼──────────────────────┼───────────┤ +│ 2020-11-02T19:01:02 │ 8 │ update │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │ +└─────────────────────┴───────────┴────────┴─────┴────────────────┴──────────────────────┴───────────┘ +``` + +### cleanup + +Очистить ресурсы от deployment. + +Аргументы: + +* `-n` или `--name` – название deployment + +Пример: + +```bash +$ octo cleanup -n hello-octopod +``` + +### restore + +Восстановить deployment. + +Аргументы: + +* `-n` или `--name` – название deployment + +Пример: + +```bash +$ octo restore -n hello-octopod +``` + +### clean-archive + +Удалить архивные deployments старее 2х недель. + +Данная subcommand не требует передачи аргументов. + +Пример: + +```bash +$ octo clean-archive +``` + +### logs + +Печатает логи выбранного деплоя. + +Аргументы: + +* `-a` или `--action` – action id, который вернулся из команды `octo info`. +* `-l` или `--log-type` – Какие логи необходимо вернуть. Значения: `stdout`, `stderr`, `all`. По умолчанию будет `all` + +Пример: + +``` +$ octo logs -a 13 + stdout: + + + stderr: + +error: Found argument '--deployment-override' which wasn't expected, or isn't valid in this context + +USAGE: + update --app-env-override ... --base-domain --name --namespace --project-name --tag + +For more information try --help +``` diff --git a/docs/ru/Octopod_deployment_with_K8S.md b/docs/ru/Octopod_deployment_with_K8S.md new file mode 100644 index 0000000..c1b083b --- /dev/null +++ b/docs/ru/Octopod_deployment_with_K8S.md @@ -0,0 +1,275 @@ +# Octopod deployment with K8S + +1. Установка необходимых утилит + + Для установки потребуется [_kubectl_][kubectl] и [_helm_][helm] версии 2. + +2. Настройка кластера + + _Octopod_ использует [_Ingress Nginx_][ingress-nginx], [_Cert Manager_][cert-manager]. Убедитесь, что они установленны в вашем кластере. + + Для работы _Octopod_ нужны 3 TLS сертификата. [_Cert Manager_][cert-manager] создает сертификаты через [_Let’s Encrypt_][lets-encrypt]. + У [_Let’s Encrypt_][lets-encrypt] есть лимиты на создание сертификатов [_Let’s Encrypt Rate Limits_][lets-encrypt-rate-limits]. + При достижении лимита `too many registrations for this IP` может помочь перемещение Pod [_Cert Manager_][cert-manager] на другую ноду. + + _Octopod_ необходимо не более 2 CPU и 2Gb оперативной памяти, пожалуйста убедитесь что у вас есть необходимые ресурсы. + + _Octopod_ будет развернут на ноду с label `role=stand`, пожалуйста добавте label: + ``` + kubectl label node role=stand + ``` + +3. Также вам потребуется [_Tiller_][tiller] – сервис на стороне кластера, который необходим для работы [_helm 2_][helm]. Самый простой способ его установить это: + + ```bash + helm init + ``` + + Если Вы установили [_Tiller_][tiller], у вас может возникнуть [проблемы с правами доступа](https://github.com/helm/helm/issues/5100) у [_Tiller_][tiller]. + + Чтобы выдать ему необходимые права будет достаточно выполнить следующие команды: + + ```bash + kubectl create -n kube-system serviceaccount tiller + kubectl --namespace kube-system create clusterrolebinding tiller-cluster-admin --clusterrole=cluster-admin --serviceaccount=kube-system:tiller + kubectl --namespace kube-system patch deploy tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}' + ``` + + +4. Скачивание исходного кода проекта + + ```bash + git clone https://github.com/typeable/octopod.git /tmp/octopod + ``` + +5. Создание namespace `deployment` и `octopod` + + ```bash + kubectl create namespace deployment + kubectl create namespace octopod + ``` + +6. Создание [_Service Account_][kubernetes-service-account] `octopod` + + ```bash + kubectl create -n octopod serviceaccount octopod + ``` + +7. Создание секрета, который будет использоваться для Basic Auth между _Web UI_ и _Octopod Server_ + + ```bash + username="octopod" + password="password" # пожалуйста поменяйте на более безопасный пароль + kubectl create secret generic octopod-basic-auth -n octopod --from-literal=auth=$(htpasswd -bn $username $password) + ``` + +8. Создание ConfigMap с сертификатами и ключами, которые будут использоваться для аутентификации между _octo CLI_ и _Octopod Server_ + + ```bash + mkdir certs + (cd certs && \ + openssl req -x509 -newkey rsa:4096 -keyout server_key.pem -out server_cert.pem -nodes -subj "/CN=localhost/O=Server" && \ + openssl req -newkey rsa:4096 -keyout client_key.pem -out client_csr.pem -nodes -subj "/CN=Client" && \ + openssl x509 -req -in client_csr.pem -CA server_cert.pem -CAkey server_key.pem -out client_cert.pem -set_serial 01 -days 3650) + kubectl create configmap octopod-certs -n octopod --from-file=./certs + ``` + + После этого директория `certs` будет содержать сертификаты, используемый для авторизации между _octo CLI_ и _Octopod Server_. `client_key.pem` и `client_cert.pem` необходимо будет [через переменные окружения передать в _octo CLI_](Octo_user_guide.md). + +9. Далее Вам необходимо будет настроить DNS чтобы поддомены вашего домена указывали на адрес вашего кластера: + + ``` + *.octo.example.com A 1.2.3.4 + ``` + +10. Так как авторизация между _octo CLI_ и _Octopod Server_ будет производиться через SSL сертификаты, которые мы только что сами создали, нужно чтобы используемый в кластере [_ingress-nginx_][ingress-nginx] поддерживал _SSL passthrough_ – чтобы мы могли ему сказать "не трогай сертификаты". + + Чтобы этого добиться нам необходимо добавить `--enable-ssl-passthrough` в аргументы запуска [_ingress-nginx_][ingress-nginx] контроллера. Сделать это можно с командой вроде этой (конкретный namespace и название деплоймента нужно будет смотреть в вашем конкретном кластере): + + ```bash + kubectl edit deploy -n ingress-nginx nginx-ingress-ingress-nginx-controller + ``` + + Помимо всех прочих параметров необходимо чтобы там был указан этот: + + ```yaml + spec: + ... + template: + ... + spec: + ... + containers: + ... + - args: + ... + - --enable-ssl-passthrough + + ``` + +11. Установка инфраструктуры Octopod + + Перед началом установки убедитесь, что у вас в кластере есть [_Storage Class_][kubernetes-storage-classes] `default`. + Для [_minikube_][minikube] вы можете создать [_Storage Class_][kubernetes-storage-classes] `default` так: + + ```bash + cat < + Table of contents + +- [Octopod roles](#octopod-roles) +- [Kubernetes role-based access control](#kubernetes-role-based-access-control) + - [Права на удаление сертификатов](#права-на-удаление-сертификатов) + - [Права на удаление Persistent Volumes Claims](#права-на-удаление-persistent-volumes-claims) +- [Users auth example with OAuth](#users-auth-example-with-oauth) +- [CLI client Auth](#cli-client-auth) +- [UI Auth](#ui-auth) + + + +## Octopod roles + +В _Octopod_ есть 2 роли: +* _user_ +* _admin_ + +| role | управление развертываниями | просмотр логов развертываний | +| :---: | :------------------------: | :--------------------------: | +| user | ✅ | ❌ | +| admin | ✅ | ✅ | + +Пользователи _UI_ работают с правами _user_. + +Пользователи _octo CLI_ работают с правами _admin_. + +## Kubernetes role-based access control + +_Octopod Server_ разворачивается в `octopod` namespace. _Octopod Server_ разворачивает развертывания в `deployments` namespace. +_Octopod Server_ использует `octopod` [_Service Account_][kubernetes-service-account]. + +Для очистки ресурсов _Octopod Server_ / _Контейнеру с утилитами (control scripts)_ может потребоваться +(зависит от настроект _Kubernetes_ и релизации _Контейнера с утилитами_) +доступ для удаления сертификатов и [_Persistent Volumes Claims_][kubernetes-pvc]. + +Доступ можно выдать через [_RBAC_][kubernetes-rbac]: + +### Права на удаление сертификатов + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cert-control-clusterrole +rules: + - apiGroups: ["cert-manager.io"] + resources: ["certificates"] + verbs: ["list", "delete", "deletecollection"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: octopod-cert-control-rolebinding + namespace: deployments +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: cert-control-clusterrole +subjects: + - kind: ServiceAccount + name: octopod + namespace: octopod +``` + +### Права на удаление Persistent Volumes Claims + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pvc-control-clusterrole +rules: + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["list", "delete", "deletecollection"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: octopod-pvc-control-rolebinding + namespace: deployments +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: pvc-control-clusterrole +subjects: + - kind: ServiceAccount + name: octopod + namespace: octopod +``` + +## Users auth example with OAuth + +Доступ к _UI_ по умолчанию не ограничен. +Но вы можете добавить аутентификацию через GitHub OAuth в [_Kubernetes Ingress Nginx_][kubernetes-ingress-nginx-external-auth], +для этого потребуется дополнительно развернуть [_OAuth2 Proxy_][oauth2-proxy] в кластере. + +Более подробно описано в примере [_External OAUTH Authentication_][oauth-external-auth]. + +## CLI client Auth + +Для аутентификации _octo CLI_ в _Octopod Server_ используется сертификат. + +## UI Auth + +Для аутентификации _UI_ в _Octopod Server_ используется Basic Auth. + + +[kubernetes-service-account]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account +[kubernetes-rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac +[kubernetes-pvc]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#expanding-persistent-volumes-claims +[kubernetes-ingress-nginx-external-auth]: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#external-authentication +[oauth2-proxy]: https://oauth2-proxy.github.io/oauth2-proxy +[oauth-external-auth]: https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth diff --git a/docs/ru/Technical_architecture.md b/docs/ru/Technical_architecture.md new file mode 100644 index 0000000..687be8a --- /dev/null +++ b/docs/ru/Technical_architecture.md @@ -0,0 +1,275 @@ +# Technical architecture + +
+ Table of contents + +- [Used tools](#used-tools) +- [App architecture](#app-architecture) + - [UI](#ui) + - [Octopod Server](#octopod-server) + - [PostgreSQL](#postgresql) + - [octo CLI](#octo-cli) + - [Скрипты управления развертываниями (control scripts)](#скрипты-управления-развертываниями-control-scripts) + - [Clean Archive CronJob](#clean-archive-cronjob) + - [Kube API Server](#kube-api-server) +- [Octopod Distribution model](#octopod-distribution-model) +- [Process view](#process-view) + - [Create](#create) + - [Create via CLI](#create-via-cli) + - [Create via UI](#create-via-ui) + - [Update](#update) + - [Update via CLI](#update-via-cli) + - [Update via UI](#update-via-ui) + - [Archive](#archive) + - [Archive via CLI](#archive-via-cli) + - [Archive via UI](#archive-via-ui) + - [Cleanup](#cleanup) + - [Cleanup via CLI](#cleanup-via-cli) + - [Cleanup via UI](#cleanup-via-ui) + - [Restore](#restore) + - [Restore via CLI](#restore-via-cli) + - [Restore via UI](#restore-via-ui) +- [How we use it](#how-we-use-it) +- [Deployment state transitions](#deployment-state-transitions) + +
+ +## Used tools + +Основная цель *Octopod* ― упрощение управления разными развертываниями сервисов в [_Kubernetes_][kube]. + +При разработке рассчитывалось, что сам _Octopod_ тоже будет разворачиваться при помощи [_Kubernetes_][kube]. + +## App architecture + +Взаимодействовать с _Octopod_ возможно либо через веб-интерфейс (рассчитано на использование разработчиками, PM, QA и т. д.), либо через консольный клиент [_octo CLI_](#octopod-server) (рассчитано на использования DevOps инженерами и программно, например в CI). + +Все взаимодействие _Octopod_ с самими развертываниями производится через [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts), которые передаются ему при запуске. Это позволяет сделать _Octopod_ очень гибким ― его возможно настроить для работы с практически в любой организацией развертываний. + +Все данные об окружениях развертываний и всех операция совершаемых над ними _Octopod_ хранит в базе данных [_PostgreSQL_](#postgresql). + +![App architecture](../diagrams/images/app-architecture.png) + +### UI + +_UI_ – веб интерфейс, +используется для управления развертываниями посредством отправки команд управления развертываниями. +Взаимодействует с [_Octopod Server_](#octopod-server) посредством отправки HTTP/1.1 запросов, а также получает +события от [_Octopod Server_](#octopod-server) по _Websocket_. Между _UI_ и [_Octopod Server_](#octopod-server) Basic Auth. Написан на Haskell и Reflex-Dom. + +В интерфейсе нет технических деталей системной администрации и оркестрации ― работа с развертываниями производится очень просто. Интерфейс рассчитан на использование разработчиками любого уровня, QA инженерами, PM, а так же людьми без ИТ образования. + +### Octopod Server + +_Octopod Server_ – непосредственно сам сервер, который обрабатывает запросы работы с развертываниями и делегирует базовые операции, модифицирующие развертывания, [_скриптам управления стеджингами (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + +Сервер обрабатывает +команды управления развертываниями от [_octo CLI_](#octopod-server) и [_UI_](#ui), обновляет состояние развертываний. +[_octo CLI_](#octopod-server) и [_UI_](#ui) взаимодействуют с ним путем отправки HTTP/1.1 запросов. +Сервер шлет событие обновления на [_UI_](#ui) по _Websocket_. Сервер взаимодействует с [_Kube API Server_](#kube-api-server) +через [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). Для хранения настроек и статусов развертываний, +и логов действий пользователей используется [_PostgreSQL_](#postgresql). Написан на Haskell. Для работы +с развертываниями использует [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + +### PostgreSQL + +[_PostgreSQL_](https://www.postgresql.org) – РСУБД для хранения настроек и статусов развертываний, и логов +действий пользователей. + +### octo CLI + +_octo CLI_ – консольный клиент +используется для управления развертываниями посредством отправки команд управления +развертываниями. Взаимодействует с [_Octopod Server_](#octopod-server) посредством отправки HTTP/1.1 запросов. +Написан на Haskell. Между _octo CLI_ и [_Octopod Server_](#octopod-server) аутентификация по сертификату, +новый сертификаты создается вовремя каждой сборки и упаковываются в контейнеры с _octo CLI_ и [_Octopod Server_](#octopod-server). + +В нем можно делать все операции, которые можно делать в [_UI_](#ui), но также есть доступ к расширенным операциям над развертываниями, таким как просмотр логов деплоя, что может быть полезно при диагностике возникающих проблем. + +Этот клиент рассчитан на использование DevOps инженерами. + +Его также можно использовать если необходимо автоматизировать управление развертываниями. Например, при настройке автоматического обновления развертываний при успешно пройденном CI. + +### Скрипты управления развертываниями (control scripts) + +_Скрипты управления развертываниями (control scripts)_ – контейнер с исполняемыми файлами, в которые инкапсулирована +логика работы с [_Kube API Server_](#kube-api-server), облачными провайдерами, развертываниями, системами контроля версий и т.д. + +Они нужны для того, чтобы _Octopod_ не был завязан на какую-то конкретную организацию развертываний ― его можно настроить для работы с практически любой организацией развертываний. + +Во время старта пода [_Octopod Server_](#octopod-server) содержимое контейнера с утилитами копируется в ФС контейнера с [_Octopod Server_](#octopod-server), +поэтому исполняемые файлы должны быть либо интерпретируемы через _Bash_, либо статически слинкованны. +Логику исполняемых файлов предлагается реализовать пользователям _Octopod_ (смотри [_Control scripts_](Control_scripts.md)). + +### Clean Archive CronJob + +_Clean Archive CronJob_ – CronJob, которая запускается раз в час и через [_octo CLI_](#octopod-server) удаляет заархивированные развертывания старее 14 дней. + +Это сделано из-за того что при архивировании развертываний удаляются только вычислительные его части, _persistent volumes_ не удаляются, и при необходимости развертывание возможно восстановить в том виде, в котором он был до архивирования. Такая возможность пропадает по истечению 14 дней с момент его архивирования. + +### Kube API Server + +[Kube API Server](https://kubernetes.io/docs/concepts/overview/kubernetes-api/) – сервис на стороне [_Kubernetes_][kube], к которому должны обращаться [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + +## Octopod Distribution model + +[_octo CLI_](#octo-cli) в виде статически слинкованного исполняемого файла. Собранные исполняемые файлы доступны во вкладке "Releases" этого проекта на GitHub. + +[_Octopod Server_](#octopod-server) и [_UI_](#ui) поставляются упаковынными в один Docker образ. Для [развертывания](Octopod_deployment_with_K8S.md) в [_Kubernetes_][kube] используется набор [_Chart_][chart]-ов. Образы доступны в [нашем registry на _Docker Hub_](https://hub.docker.com/orgs/typeable/repositories). + +Docker образ [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts) пользователь _Octopod_ собирает сам. + +## Process view + +Далее здесь для справки приводятся схемы последовательностей для каждой из базовых операция работы с развертываниями. При их исполнении вызываются [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). На схеме они помечены как _ControlScripts_. + +### Create + +_create_ – создание нового развертывания. + В качестве аргументов принимаются `name`, `tag` и опциональные `overrides` (уровня App или Deployment, открытие или секретные). + + Переданные аргументы вместе с `project-name`, `base-domain`, `namespace` передаются в качестве аргументов в `create` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + `create` создает новое развертывание в кластере используя полученные аргументы. + Например, он может выполнять: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +#### Create via CLI +![Create](../diagrams/images/technical-architecture-create-via-cli.png) + +#### Create via UI +![Create](../diagrams/images/technical-architecture-create-via-ui.png) + +### Update + +_update_ – обновление существующего развертывания. + В качестве аргументов принимаются `name`, `tag` и опционально изменения `overrides` (уровня App или Deployment, открытие или секретные). + + Затем из базы достаются `overrides` (уровня App или Deployment, открытие или секретные), они мерджатся с изменениями `overrides` + и передаются вместе с `project-name`, `base-domain`, `namespace` в качестве аргументов в `update` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + `update` обновляет существующее развертывание в кластере используя полученные аргументы. + Например, он может выполнять: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + + +#### Update via CLI +![Update](../diagrams/images/technical-architecture-update-via-cli.png) + +#### Update via UI +![Update](../diagrams/images/technical-architecture-update-via-ui.png) + +### Archive + +_delete_ – архивирование существующего развертывания. + Производится удаление только подов, Persistent Volumes (диски) сохраняются. Отменить действие этой команды можно с помощью команды restore. + В качестве аргументов принимается `name`. + + `name` вместе с `project-name`, `base-domain`, `namespace` передаются в качестве аргументов в `archive` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + `archive` удаляет существующее развертывание в кластере, используя полученные аргументы. + Например он может выполнять: + +```bash +helm delete "$name" --purge +``` + + +#### Archive via CLI +![Archive](../diagrams/images/technical-architecture-archive-via-cli.png) + +#### Archive via UI +![Archive](../diagrams/images/technical-architecture-archive-via-ui.png) + +### Cleanup + +_cleanup_ – полная очистка развертывания. + В качестве аргументов принимаeтся `name`. + + Например, можно удалять сертификаты, Persistent Volume Claim и Persistent Volumes. + + `name` вместе с `project-name`, `base-domain`, `namespace` передаются в качестве аргументов в `cleanup` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + `cleanup` выполняет освобождение ресурсов в кластере, которые ранее использовались одним из удаленных развертываний. + Например, он может выполнять: + +```bash +kubectl delete pvc -n "$namespace" "$name-postgres-pvc" +kubectl delete certificate -n "$namespace" "$name-postgres-cert" +``` + +#### Cleanup via CLI +![Cleanup](../diagrams/images/technical-architecture-cleanup-via-cli.png) + +#### Cleanup via UI +![Cleanup](../diagrams/images/technical-architecture-cleanup-via-ui.png) + +### Restore + +_restore_ – восстановление заархивированного развертывания с последними настройками. + В качестве аргументов принимается `name`. + + Из базы достаются недостающие для восстановления `tag` и опциональные `overrides` (уровня App или Deployment, открытие или секретные). Они передаются вместе с `project-name`, `base-domain`, `namespace` в качестве аргументов в `restore` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). + `restore` выполняет восстановление в кластере ранее удаленного развертывания используя полученные аргументы. + Например, он может выполнять: + +```bash +helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \ + --set "global.project-name=$project_name" \ + --set "global.base-domain=$base-domain" \ + --set "app.tag=$tag" \ + --set "app.env.foo=$app_env_override_1" \ + --set "app.bar=$deployment_override_1" \ + --wait \ + --timeout 300 +``` + +#### Restore via CLI +![Restore](../diagrams/images/technical-architecture-restore-via-cli.png) + +#### Restore via UI +![Restore](../diagrams/images/technical-architecture-restore-via-ui.png) + +## How we use it + +Мы используем несколько кластеров [_Kubernetes_][kube]: отдельный кластер для каждого приложения, а так же разделяем по кластерам _production_ и _deployment_ окружения. + +В каждый _deployment_ кластер мы устанавливаем _Octopod_, через _Octopod_ осуществляем разворачивание различных версий deployment'ов необходимых для QA. + +## Deployment state transitions + +Существует 6 статусов развертываний: +1. *Running* +2. *Failure* +3. *CreatePending* +4. *UpdatePending* +5. *DeletePending* +6. *Archived* + +_Running_, _Failure_, _Archived_ являются постоянными, т.е. развертывание уже не находится в режиме выполнения команды. + +*CreatePending*, *UpdatePending*, *DeletePending* являются переходными, т.е. развертывание находится в режиме выполнения команды. + +![Deployment Statuses](../diagrams/images/technical-architecture-deployment-states-fsm.png) + + + + +[kube]: https://kubernetes.io +[chart]: https://helm.sh/docs/topics/charts/ diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..cfb624f --- /dev/null +++ b/docs/style.css @@ -0,0 +1,90 @@ +body { + background: white; +} + +.actor { + stroke: #ccccff; + fill: #ececff; +} + +text.actor { + fill: black; + stroke: none; + font-family: Helvetica, Helvetica; +} + +.actor-line { + stroke: grey; +} + +.messageLine0 { + stroke-width: 1.5; + stroke-dasharray: '2 2'; + marker-end: 'url(#arrowhead)'; + stroke: black; +} + +.messageLine1 { + stroke-width: 1.5; + stroke-dasharray: '2 2'; + stroke: black; +} + +#arrowhead { + fill: black; +} + +.messageText { + fill: black; + stroke: none; + font-family: Helvetica, 'trebuchet ms', verdana, arial; + font-size: 19px; +} + +.labelBox { + stroke: #ccccff; + fill: #ececff; +} + +.labelText { + fill: black; + stroke: none; + font-family: Helvetica, 'trebuchet ms', verdana, arial; + font-family: Helvetica, 'trebuchet ms', verdana, arial; + font-size: 19px; +} + +.loopText { + fill: black; + stroke: none; + font-family: Helvetica, 'trebuchet ms', verdana, arial; +} + +.loopLine { + stroke-width: 2; + stroke-dasharray: '2 2'; + marker-end: 'url(#arrowhead)'; + stroke: #ccccff; +} + +.note { + stroke: #decc93; + fill: #fff5ad; + font-family: Helvetica, 'trebuchet ms', verdana, arial; + font-size: 19px; +} + +.noteText { + font-size: 19px; + fill: black; + stroke: none; +} + +.sectionTitle, +.noteText, +.taskText, +.titleText, +.taskTextOutsideRight { + font-size: 19px; + font-family: Helvetica, 'trebuchet ms', verdana, arial; +} \ No newline at end of file diff --git a/examples/helm-based-control-scripts/.gitignore b/examples/helm-based-control-scripts/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/examples/helm-based-control-scripts/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/helm-based-control-scripts/Cargo.lock b/examples/helm-based-control-scripts/Cargo.lock new file mode 100644 index 0000000..94ecf0a --- /dev/null +++ b/examples/helm-based-control-scripts/Cargo.lock @@ -0,0 +1,1244 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "cc" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +dependencies = [ + "cfg-if 0.1.10", + "wasm-bindgen", +] + +[[package]] +name = "encoding_rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] +name = "futures-task" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project 1.0.2", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi", +] + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "helm-based-control-scripts" +version = "0.1.0" +dependencies = [ + "clap", + "rand", + "reqwest", + "serde", + "serde_derive", + "serde_json", + "tokio", +] + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "hyper" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project 1.0.2", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" +dependencies = [ + "bytes", + "futures-util", + "hyper", + "log", + "rustls", + "tokio", + "tokio-rustls", + "webpki", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "js-sys" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.6", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi 0.3.9", +] + +[[package]] +name = "net2" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cf75f38f16cb05ea017784dc6dbfd354f76c223dba37701734c4f5a9337d02" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +dependencies = [ + "pin-project-internal 0.4.27", +] + +[[package]] +name = "pin-project" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7" +dependencies = [ + "pin-project-internal 1.0.2", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "reqwest" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb15d6255c792356a0f578d8a645c677904dc02e862bebe2ecc18e0c01b9a0ce" +dependencies = [ + "base64 0.13.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite 0.2.0", + "rustls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "rustls" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +dependencies = [ + "base64 0.12.3", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "socket2" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite 0.1.11", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" +dependencies = [ + "futures-core", + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.11", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" + +[[package]] +name = "tracing" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.0", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project 0.4.27", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0355fa0c1f9b792a09b6dcb6a8be24d51e71e6d74972f9eb4a44c4c004d24a25" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e07b46b98024c2ba2f9e83a10c2ef0515f057f2da299c1762a2017de80438b" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "web-sys" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/examples/helm-based-control-scripts/Cargo.toml b/examples/helm-based-control-scripts/Cargo.toml new file mode 100644 index 0000000..ba62c22 --- /dev/null +++ b/examples/helm-based-control-scripts/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "helm-based-control-scripts" +version = "0.1.0" +authors = ["Typeable LLC "] +edition = "2018" + +[dependencies] +clap = "2.33.3" +rand = "0.7.3" +serde = "1.0.117" +serde_derive = "1.0.117" +serde_json = "1.0.60" +tokio = { version = "0.2.22", default_features = false, features = ["full"] } +reqwest = { version = "0.10.9", default_features = false, features = ["json", "rustls-tls"] } + +[[bin]] +name = "create" +path = "src/bin/create.rs" + +[[bin]] +name = "archive" +path = "src/bin/archive.rs" + +[[bin]] +name = "check" +path = "src/bin/check.rs" + +[[bin]] +name = "cleanup" +path = "src/bin/cleanup.rs" + +[[bin]] +name = "archive_check" +path = "src/bin/archive_check.rs" + +[[bin]] +name = "tag_check" +path = "src/bin/tag_check.rs" + +[[bin]] +name = "init" +path = "src/bin/init.rs" + +[[bin]] +name = "info" +path = "src/bin/info.rs" diff --git a/examples/helm-based-control-scripts/README.md b/examples/helm-based-control-scripts/README.md new file mode 100644 index 0000000..ce5ae78 --- /dev/null +++ b/examples/helm-based-control-scripts/README.md @@ -0,0 +1,9 @@ +# helm-based-control-scripts + +An Example of Helm-based control scripts. + +## Build Docker image + +```bash +docker build -f docker/Dockerfile -t typeable/octopod-helm-example:1.0 . +``` diff --git a/examples/helm-based-control-scripts/docker/Dockerfile b/examples/helm-based-control-scripts/docker/Dockerfile new file mode 100644 index 0000000..5b3b031 --- /dev/null +++ b/examples/helm-based-control-scripts/docker/Dockerfile @@ -0,0 +1,29 @@ +FROM ekidd/rust-musl-builder:1.46.0 as builder + +RUN sudo mkdir /app && \ + sudo chown -R rust:rust /app +WORKDIR /app +RUN USER=root cargo new helm-based-control-scripts +COPY Cargo.toml /app/helm-based-control-scripts/ +COPY Cargo.lock /app/helm-based-control-scripts/ +COPY src /app/helm-based-control-scripts/src +WORKDIR /app/helm-based-control-scripts +RUN cargo build --release + +FROM typeable/kube-things as base-utils + +FROM busybox + +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/create /utils/create +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/create /utils/update +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/archive /utils/archive +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/check /utils/check +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/cleanup /utils/cleanup +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/archive_check /utils/archive_check +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/tag_check /utils/tag_check +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/init /utils/init +COPY --from=builder /app/helm-based-control-scripts/target/x86_64-unknown-linux-musl/release/info /utils/info + +COPY --from=base-utils /utils/kubectl /utils/kubectl +COPY --from=base-utils /utils/helm2 /utils/helm +COPY --from=base-utils /utils/kubedog /utils/kubedog diff --git a/examples/helm-based-control-scripts/src/bin/archive.rs b/examples/helm-based-control-scripts/src/bin/archive.rs new file mode 100644 index 0000000..fd55e77 --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/archive.rs @@ -0,0 +1,59 @@ +use clap::{App, Arg}; +use std::process::{exit, Command}; + +fn main() -> std::io::Result<()> { + let matches = App::new("archive") + .version("0.1") + .arg( + Arg::with_name("project-name") + .long("project-name") + .short("p") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("base-domain") + .long("base-domain") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("namespace") + .long("namespace") + .short("s") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .required(true) + .takes_value(true), + ) + .get_matches(); + + let _project_name = matches + .value_of("project-name") + .expect("could not get project-name"); + let _base_domain = matches + .value_of("base-domain") + .expect("could not get base-domain"); + let _namespace = matches + .value_of("namespace") + .expect("could not get namepace"); + let name = matches.value_of("name").expect("could not get name"); + + let output = Command::new("helm") + .args(&["delete", &format!("app-{}", name), "--purge"]) + .output() + .expect("could not delete app"); + let success = output.status.success(); + + if !success { + exit(1) + } + + Ok(()) +} diff --git a/examples/helm-based-control-scripts/src/bin/archive_check.rs b/examples/helm-based-control-scripts/src/bin/archive_check.rs new file mode 100644 index 0000000..55d8650 --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/archive_check.rs @@ -0,0 +1,59 @@ +use clap::{App, Arg}; +use std::process::{exit, Command}; + +fn main() -> std::io::Result<()> { + let matches = App::new("archive_check") + .version("0.1") + .arg( + Arg::with_name("project-name") + .long("project-name") + .short("p") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("base-domain") + .long("base-domain") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("namespace") + .long("namespace") + .short("s") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .required(true) + .takes_value(true), + ) + .get_matches(); + + let _project_name = matches + .value_of("project-name") + .expect("could not get project-name"); + let _base_domain = matches + .value_of("base-domain") + .expect("could not get base-domain"); + let _namespace = matches + .value_of("namespace") + .expect("could not get namepace"); + let name = matches.value_of("name").expect("could not get name"); + + let output = Command::new("helm") + .args(&["status", name]) + .output() + .expect("could not get status"); + let success = output.status.success(); + + if success { + exit(1) + } + + Ok(()) +} diff --git a/examples/helm-based-control-scripts/src/bin/check.rs b/examples/helm-based-control-scripts/src/bin/check.rs new file mode 100644 index 0000000..170adf2 --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/check.rs @@ -0,0 +1,78 @@ +use clap::{App, Arg}; +use serde_json::json; +use std::io::Write; +use std::process::{exit, Command, Stdio}; + +const KUBEDOG_TIMEOUT: usize = 3; + +fn main() -> std::io::Result<()> { + let matches = App::new("check") + .version("0.1") + .arg( + Arg::with_name("project-name") + .long("project-name") + .short("p") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("base-domain") + .long("base-domain") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("namespace") + .long("namespace") + .short("s") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .required(true) + .takes_value(true), + ) + .get_matches(); + + let _project_name = matches + .value_of("project-name") + .expect("could not get project-name"); + let _base_domain = matches + .value_of("base-domain") + .expect("could not get base-domain"); + let namespace = matches + .value_of("namespace") + .expect("could not get namepace"); + let name = matches.value_of("name").expect("could not get name"); + + let kubedog_stdin = json!({ + "Deployments": [{"ResourceName": format!("app-{}", name), "Namespace": namespace}] + }) + .to_string(); + + let mut child = Command::new("kubedog") + .args(&["multitrack", "-t", &KUBEDOG_TIMEOUT.to_string()]) + .stdin(Stdio::piped()) + .spawn() + .expect("failed to call kubedog"); + + { + let stdin = child.stdin.as_mut().expect("failed to open stdin"); + stdin + .write_all(kubedog_stdin.as_bytes()) + .expect("failed to write to stdin"); + } + + let output = child.wait_with_output().expect("failed to read stdout"); + let success = output.status.success(); + + if !success { + exit(1) + } + + Ok(()) +} diff --git a/examples/helm-based-control-scripts/src/bin/cleanup.rs b/examples/helm-based-control-scripts/src/bin/cleanup.rs new file mode 100644 index 0000000..4c47988 --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/cleanup.rs @@ -0,0 +1,50 @@ +use clap::{App, Arg}; + +fn main() -> std::io::Result<()> { + let matches = App::new("cleanup") + .version("0.1") + .arg( + Arg::with_name("project-name") + .long("project-name") + .short("p") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("base-domain") + .long("base-domain") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("namespace") + .long("namespace") + .short("s") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .required(true) + .takes_value(true), + ) + .get_matches(); + + let _project_name = matches + .value_of("project-name") + .expect("could not get project-name"); + let _base_domain = matches + .value_of("base-domain") + .expect("could not get base-domain"); + let _namespace = matches + .value_of("namespace") + .expect("could not get namepace"); + let _name = matches.value_of("name").expect("could not get name"); + + // nop + + Ok(()) +} diff --git a/examples/helm-based-control-scripts/src/bin/create.rs b/examples/helm-based-control-scripts/src/bin/create.rs new file mode 100644 index 0000000..32d4512 --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/create.rs @@ -0,0 +1,237 @@ +use clap::{App, Arg}; +use rand::{ + distributions::{Alphanumeric, Distribution}, + thread_rng, +}; +use std::convert::TryFrom; +use std::convert::TryInto; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::process::{exit, Command}; + +static REGISTRY: &str = "typeable"; +static IMAGE: &str = "octopod-web-app-example"; +static GIT_REPOSITORY: &str = "https://github.com/typeable/octopod.git"; + +struct TmpDirGuard(PathBuf); + +impl TmpDirGuard { + pub fn new(tmp_dir: &PathBuf) -> Self { + TmpDirGuard(tmp_dir.clone()) + } +} + +impl Drop for TmpDirGuard { + fn drop(&mut self) { + match fs::remove_dir_all(&self.0) { + Ok(_) => (), + Err(err) => eprintln!("remove_dir_all error: {:?}", err), + }; + } +} + +#[derive(Debug, Clone)] +struct Override { + pub key: String, + pub value: String, +} + +impl TryFrom<&str> for Override { + type Error = &'static str; + fn try_from(s: &str) -> Result { + let parts = s.split('=').collect::>(); + if parts.len() == 2 { + Ok(Override { + key: parts[0].to_string(), + value: parts[1].to_string(), + }) + } else { + Err("Malformed environment key-value pair, should be similar to FOO=bar") + } + } +} + +impl ToString for Override { + fn to_string(&self) -> String { + format!("{}={}", self.key, self.value) + } +} + +fn main() -> std::io::Result<()> { + let matches = App::new("create") + .version("0.1") + .arg( + Arg::with_name("project-name") + .long("project-name") + .short("p") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("base-domain") + .long("base-domain") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("namespace") + .long("namespace") + .short("s") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("tag") + .long("tag") + .short("t") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("app-env-override") + .long("app-env-override") + .short("e") + .required(false) + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name("deployment-override") + .long("deployment-override") + .short("o") + .required(false) + .multiple(true) + .takes_value(true), + ) + .get_matches(); + + let _project_name = matches + .value_of("project-name") + .expect("could not get project-name"); + let base_domain = matches + .value_of("base-domain") + .expect("could not get base-domain"); + let namespace = matches + .value_of("namespace") + .expect("could not get namepace"); + let name = matches.value_of("name").expect("could not get name"); + let tag = matches.value_of("tag").expect("could not get tag"); + let app_env_overrides = matches + .values_of("app-env-override") + .unwrap_or_else(Default::default) + .map(|e| e.try_into().expect("could not get valid key=value")) + .collect::>(); + let deployment_overrides = matches + .values_of("deployment-override") + .unwrap_or_else(Default::default) + .map(|e| e.try_into().expect("could not get valid key=value")) + .collect::>(); + + let tmp_dir = tmp_dir(); + let work_dir = Path::new("/tmp").join(tmp_dir); + + let _guard = TmpDirGuard::new(&work_dir); + fs::create_dir(&work_dir)?; + + let success = clone_and_prepare_repo(&work_dir); + + let output = Command::new("helm") + .args(command_args( + base_domain, + namespace, + name, + tag, + app_env_overrides, + deployment_overrides, + )) + .current_dir(&work_dir) + .output() + .expect("could not create app"); + let success2 = output.status.success(); + + if !(success && success2) { + exit(1) + } + + Ok(()) +} + +fn tmp_dir() -> String { + const LENGTH: usize = 10; + + let rng = thread_rng(); + let random_string: String = Alphanumeric.sample_iter(rng).take(LENGTH).collect(); + format!("octopod-{}", random_string) +} + +fn clone_and_prepare_repo(work_dir: &PathBuf) -> bool { + let output = Command::new("git") + .args(&["clone", "--recursive", "--depth=1", GIT_REPOSITORY, "."]) + .current_dir(work_dir) + .output() + .expect("could not clone repo"); + output.status.success() +} + +fn command_args( + domain: &str, + namespace: &str, + name: &str, + tag: &str, + app_env_overrides: Vec, + deployment_overrides: Vec, +) -> Vec { + let mut args = vec![ + "upgrade", + "--install", + "--namespace", + namespace, + &format!("app-{}", name), + "examples/web-app/charts/web-app", + "--set", + &format!("image_prefix={}", REGISTRY), + "--set", + &format!("image={}", IMAGE), + "--set", + &format!("image_tag={}", tag), + "--set", + &format!("domain={}.{}", name, domain), + "--wait", + "--timeout", + "300", + "--debug", + ] + .iter() + .map(ToString::to_string) + .collect::>(); + + let mut app_args = app_env_overrides + .into_iter() + .map(|e| { + vec![ + "--set".to_string(), + format!("env.{}", e.to_string()), + ] + }) + .flatten() + .collect::>(); + args.append(&mut app_args); + + let mut deployment_args = deployment_overrides + .into_iter() + .map(|e| vec!["--set".to_string(), e.to_string()]) + .flatten() + .collect::>(); + args.append(&mut deployment_args); + + args +} diff --git a/examples/helm-based-control-scripts/src/bin/info.rs b/examples/helm-based-control-scripts/src/bin/info.rs new file mode 100644 index 0000000..fe8ebca --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/info.rs @@ -0,0 +1,52 @@ +use clap::{App, Arg}; + +fn main() -> std::io::Result<()> { + let matches = App::new("cleanup") + .version("0.1") + .arg( + Arg::with_name("project-name") + .long("project-name") + .short("p") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("base-domain") + .long("base-domain") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("namespace") + .long("namespace") + .short("s") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .required(true) + .takes_value(true), + ) + .get_matches(); + + let _project_name = matches + .value_of("project-name") + .expect("could not get project-name"); + let base_domain = matches + .value_of("base-domain") + .expect("could not get base-domain"); + let _namespace = matches + .value_of("namespace") + .expect("could not get namepace"); + let name = matches.value_of("name").expect("could not get name"); + + [("app", format!("https://{}.{}", name, base_domain))] + .iter() + .for_each(|(key, value)| println!("{},{}", key, value)); + + Ok(()) +} diff --git a/examples/helm-based-control-scripts/src/bin/init.rs b/examples/helm-based-control-scripts/src/bin/init.rs new file mode 100644 index 0000000..432e59d --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/init.rs @@ -0,0 +1,9 @@ +use std::env; + +fn main() -> std::io::Result<()> { + let _home = env::var("HOME").expect("could not get $HOME"); + + // nop + + Ok(()) +} diff --git a/examples/helm-based-control-scripts/src/bin/tag_check.rs b/examples/helm-based-control-scripts/src/bin/tag_check.rs new file mode 100644 index 0000000..8424e4b --- /dev/null +++ b/examples/helm-based-control-scripts/src/bin/tag_check.rs @@ -0,0 +1,91 @@ +use clap::{App, Arg}; +use serde_derive::Deserialize; +use std::process::exit; + +static REGISTRY: &str = "typeable"; +static REPOSITORY: &str = "octopod-web-app-example"; + +#[derive(Debug, Deserialize)] +struct Resp { + results: Vec, +} + +#[derive(Debug, Deserialize)] +struct Tag { + name: String, +} + +async fn do_request() -> reqwest::Result { + let url = format!( + "https://hub.docker.com/v2/repositories/{}/{}/tags", + REGISTRY, REPOSITORY + ); + let body = reqwest::get(&url).await?.json::().await?; + Ok(body) +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let matches = App::new("tag_check") + .version("0.1") + .arg( + Arg::with_name("project-name") + .long("project-name") + .short("p") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("base-domain") + .long("base-domain") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("namespace") + .long("namespace") + .short("s") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("tag") + .long("tag") + .short("t") + .required(true) + .takes_value(true), + ) + .get_matches(); + + let _project_name = matches + .value_of("project-name") + .expect("could not get project-name"); + let _base_domain = matches + .value_of("base-domain") + .expect("could not get base-domain"); + let _namespace = matches + .value_of("namespace") + .expect("could not get namepace"); + let _name = matches.value_of("name").expect("could not get name"); + let tag = matches.value_of("tag").expect("could not get tag"); + + let mut tag_found = false; + match do_request().await { + Ok(resp) => tag_found = resp.results.iter().any(|t| t.name == tag), + Err(err) => eprintln!("could not get tags, reason: {:?}", err), + } + + if !tag_found { + exit(1) + } + + Ok(()) +} diff --git a/examples/web-app-v2/.gitignore b/examples/web-app-v2/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/examples/web-app-v2/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/web-app-v2/Cargo.lock b/examples/web-app-v2/Cargo.lock new file mode 100644 index 0000000..0e5b6f0 --- /dev/null +++ b/examples/web-app-v2/Cargo.lock @@ -0,0 +1,1935 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "actix-codec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project 0.4.27", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-connect" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "either", + "futures-util", + "http", + "log", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-http" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874" +dependencies = [ + "actix-codec", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils", + "base64", + "bitflags", + "brotli2", + "bytes", + "cookie", + "copyless", + "derive_more", + "either", + "encoding_rs", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2", + "http", + "httparse", + "indexmap", + "itoa", + "language-tags", + "lazy_static", + "log", + "mime", + "percent-encoding", + "pin-project 1.0.2", + "rand", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "sha-1", + "slab", + "time", +] + +[[package]] +name = "actix-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd1f7dbda1645bf7da33554db60891755f6c01c1b2169e2f4c492098d30c235" +dependencies = [ + "bytestring", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227" +dependencies = [ + "actix-macros", + "actix-threadpool", + "copyless", + "futures-channel", + "futures-util", + "smallvec", + "tokio", +] + +[[package]] +name = "actix-server" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "futures-channel", + "futures-util", + "log", + "mio", + "mio-uds", + "num_cpus", + "slab", + "socket2", +] + +[[package]] +name = "actix-service" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0052435d581b5be835d11f4eb3bce417c8af18d87ddf8ace99f8e67e595882bb" +dependencies = [ + "futures-util", + "pin-project 0.4.27", +] + +[[package]] +name = "actix-testing" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c" +dependencies = [ + "actix-macros", + "actix-rt", + "actix-server", + "actix-service", + "log", + "socket2", +] + +[[package]] +name = "actix-threadpool" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30" +dependencies = [ + "derive_more", + "futures-channel", + "lazy_static", + "log", + "num_cpus", + "parking_lot", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb" +dependencies = [ + "actix-codec", + "actix-service", + "actix-utils", + "futures-util", +] + +[[package]] +name = "actix-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "bitflags", + "bytes", + "either", + "futures-channel", + "futures-sink", + "futures-util", + "log", + "pin-project 0.4.27", + "slab", +] + +[[package]] +name = "actix-web" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e641d4a172e7faa0862241a20ff4f1f5ab0ab7c279f00c2d4587b77483477b86" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-testing", + "actix-threadpool", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "awc", + "bytes", + "derive_more", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "log", + "mime", + "pin-project 1.0.2", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "socket2", + "time", + "tinyvec", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "askama" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", +] + +[[package]] +name = "askama_derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522" +dependencies = [ + "askama_shared", + "proc-macro2", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" + +[[package]] +name = "askama_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc" +dependencies = [ + "askama_escape", + "humansize", + "nom", + "num-traits", + "percent-encoding", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", +] + +[[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "awc" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b381e490e7b0cfc37ebc54079b0413d8093ef43d14a4e4747083f7fa47a9e691" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "base64", + "bytes", + "cfg-if 1.0.0", + "derive_more", + "futures-core", + "log", + "mime", + "percent-encoding", + "rand", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "backtrace" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bitvec" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "brotli2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +dependencies = [ + "brotli-sys", + "libc", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytestring" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "const_fn" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" + +[[package]] +name = "cookie" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "derive_more" +version = "0.99.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum-as-inner" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "flate2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "funty" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8" + +[[package]] +name = "futures" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-io" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" + +[[package]] +name = "futures-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] +name = "futures-task" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project 1.0.2", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "humansize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2", + "widestring", + "winapi 0.3.9", + "winreg", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 0.1.10", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "net2" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cf75f38f16cb05ea017784dc6dbfd354f76c223dba37701734c4f5a9337d02" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nom" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0" +dependencies = [ + "bitvec", + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +dependencies = [ + "pin-project-internal 0.4.27", +] + +[[package]] +name = "pin-project" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7" +dependencies = [ + "pin-project-internal 1.0.2", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "regex" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" + +[[package]] +name = "socket2" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "standback" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8" +dependencies = [ + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "syn" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" + +[[package]] +name = "thiserror" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcdaeea317915d59b2b4cd3b5efcd156c309108664277793f5351700c02ce98b" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +dependencies = [ + "bytes", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-uds", + "pin-project-lite 0.1.11", + "signal-hook-registry", + "slab", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.11", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.0", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project 0.4.27", + "tracing", +] + +[[package]] +name = "trust-dns-proto" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53861fcb288a166aae4c508ae558ed18b53838db728d4d310aad08270a7d4c2b" +dependencies = [ + "async-trait", + "backtrace", + "enum-as-inner", + "futures", + "idna", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6759e8efc40465547b0dfce9500d733c65f969a4cbbfbe3ccf68daaa46ef179e" +dependencies = [ + "backtrace", + "cfg-if 0.1.10", + "futures", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" + +[[package]] +name = "web-app" +version = "0.2.0" +dependencies = [ + "actix-rt", + "actix-web", + "askama", +] + +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/examples/web-app-v2/Cargo.toml b/examples/web-app-v2/Cargo.toml new file mode 100644 index 0000000..6d3be66 --- /dev/null +++ b/examples/web-app-v2/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "web-app" +version = "0.2.0" +authors = ["Typeable LLC "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "3.3.2" +actix-rt = "1.1.1" +askama = "0.10.5" diff --git a/examples/web-app-v2/README.md b/examples/web-app-v2/README.md new file mode 100644 index 0000000..ea5ceae --- /dev/null +++ b/examples/web-app-v2/README.md @@ -0,0 +1,5 @@ +# Build Docker image + +```bash +docker build -f docker/Dockerfile -t typeable/octopod-web-app-example:v2 . +``` diff --git a/examples/web-app-v2/charts/web-app/.helmignore b/examples/web-app-v2/charts/web-app/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/examples/web-app-v2/charts/web-app/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/examples/web-app-v2/charts/web-app/Chart.yaml b/examples/web-app-v2/charts/web-app/Chart.yaml new file mode 100644 index 0000000..0e0adae --- /dev/null +++ b/examples/web-app-v2/charts/web-app/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: web-app +version: 0.1.0 diff --git a/examples/web-app-v2/charts/web-app/templates/deployment.yaml b/examples/web-app-v2/charts/web-app/templates/deployment.yaml new file mode 100644 index 0000000..773f07b --- /dev/null +++ b/examples/web-app-v2/charts/web-app/templates/deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + namespace: {{ .Values.namespace }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + name: {{ .Release.Name }} + labels: + app: {{ .Release.Name }} + spec: + nodeSelector: + role: {{ .Values.nodeselector }} + terminationGracePeriodSeconds: 60 + containers: + - name: main + image: {{ .Values.image_prefix }}/{{ .Values.image }}:{{ .Values.image_tag }} + env: + {{- range $key, $value := .Values.env }} + - name: {{ $key | quote }} + value: {{ $value | quote }} + {{- end }} + ports: + - containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 0.1 + memory: 64Mi + limits: + cpu: 0.1 + memory: 128Mi + readinessProbe: + httpGet: + port: 8080 + path: / + periodSeconds: 20 + livenessProbe: + httpGet: + port: 8080 + path: / + initialDelaySeconds: 15 + periodSeconds: 5 diff --git a/examples/web-app-v2/charts/web-app/templates/ingress.yaml b/examples/web-app-v2/charts/web-app/templates/ingress.yaml new file mode 100644 index 0000000..8956770 --- /dev/null +++ b/examples/web-app-v2/charts/web-app/templates/ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-nginx-ingress + namespace: {{ .Values.namespace }} + annotations: + kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" + cert-manager.io/issuer: "{{ .Release.Name }}-certs" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-next-upstream: "http_502 error timeout" +spec: + tls: + - hosts: + - {{ .Values.domain }} + secretName: {{ .Release.Name }}-tls + rules: + - host: {{ .Values.domain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }} + servicePort: 80 diff --git a/examples/web-app-v2/charts/web-app/templates/issuer.yaml b/examples/web-app-v2/charts/web-app/templates/issuer.yaml new file mode 100644 index 0000000..4803946 --- /dev/null +++ b/examples/web-app-v2/charts/web-app/templates/issuer.yaml @@ -0,0 +1,18 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ .Release.Name }}-certs + namespace: {{ .Release.Namespace }} +spec: + acme: + email: {{ .Values.acme_registration_email }} + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: {{ .Release.Name }}-letsencrypt + # ACME HTTP-01 provider configurations + solvers: + # An empty 'selector' means that this solver matches all domains + - selector: {} + http01: + ingress: + class: nginx diff --git a/examples/web-app-v2/charts/web-app/templates/service.yaml b/examples/web-app-v2/charts/web-app/templates/service.yaml new file mode 100644 index 0000000..f657847 --- /dev/null +++ b/examples/web-app-v2/charts/web-app/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + namespace: {{ .Values.namespace }} + labels: + app: {{ .Release.Name }} +spec: + selector: + app: {{ .Release.Name }} + ports: + - name: app + port: 80 + targetPort: 8080 diff --git a/examples/web-app-v2/charts/web-app/values.yaml b/examples/web-app-v2/charts/web-app/values.yaml new file mode 100644 index 0000000..c7f1583 --- /dev/null +++ b/examples/web-app-v2/charts/web-app/values.yaml @@ -0,0 +1,8 @@ +image_prefix: +image: web-app-example +image_tag: +namespace: deployment +domain: +nodeselector: stand +replicas: 1 +env: diff --git a/examples/web-app-v2/docker/Dockerfile b/examples/web-app-v2/docker/Dockerfile new file mode 100644 index 0000000..00d9058 --- /dev/null +++ b/examples/web-app-v2/docker/Dockerfile @@ -0,0 +1,22 @@ +FROM ekidd/rust-musl-builder:1.48.0 as builder + +RUN sudo mkdir /app && \ + sudo chown -R rust:rust /app +WORKDIR /app +RUN USER=root cargo new web-app +WORKDIR /app/web-app +COPY Cargo.toml /app/web-app/ +COPY Cargo.lock /app/web-app/ +RUN cargo build --release +COPY src /app/web-app/src +COPY templates /app/web-app/templates +RUN rm -r ./target/x86_64-unknown-linux-musl/release/deps/web_app* && \ + cargo build --release --frozen + +FROM alpine:3.12.1 + +COPY --from=builder /app/web-app/target/x86_64-unknown-linux-musl/release/web-app /app/web-app +RUN addgroup -g 1000 user && \ + adduser -D -s /bin/sh -g 1000 -G user user + +ENTRYPOINT ["/app/web-app"] diff --git a/examples/web-app-v2/src/main.rs b/examples/web-app-v2/src/main.rs new file mode 100644 index 0000000..267064f --- /dev/null +++ b/examples/web-app-v2/src/main.rs @@ -0,0 +1,43 @@ +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use askama::Template; + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate { + envs: Vec, +} + +#[derive(Debug, Clone, Template)] +#[template(source = "{{ key }} = {{ value }}", ext = "txt")] +struct Env { + key: String, + value: String, +} + +#[get("/")] +async fn index(state: web::Data>) -> impl Responder { + let body = IndexTemplate { + envs: state.to_vec(), + } + .render() + .expect("could not render template"); + HttpResponse::Ok().body(body) +} + +#[actix_rt::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| App::new().data(get_app_envs()).service(index)) + .bind("0.0.0.0:8080")? + .run() + .await +} + +fn get_app_envs() -> Vec { + std::env::vars() + .filter(|env| env.0.starts_with("APP_ENV")) + .map(|env| Env { + key: env.0, + value: env.1, + }) + .collect::>() +} diff --git a/examples/web-app-v2/templates/index.html b/examples/web-app-v2/templates/index.html new file mode 100644 index 0000000..852dcff --- /dev/null +++ b/examples/web-app-v2/templates/index.html @@ -0,0 +1,16 @@ + + + + + + + +
+
    + {% for env in envs %} +
  • {{ env }}
  • + {% endfor %} +
+
+ + diff --git a/examples/web-app/.gitignore b/examples/web-app/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/examples/web-app/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/web-app/Cargo.lock b/examples/web-app/Cargo.lock new file mode 100644 index 0000000..0472ae1 --- /dev/null +++ b/examples/web-app/Cargo.lock @@ -0,0 +1,1935 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "actix-codec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project 0.4.27", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-connect" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "either", + "futures-util", + "http", + "log", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-http" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874" +dependencies = [ + "actix-codec", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils", + "base64", + "bitflags", + "brotli2", + "bytes", + "cookie", + "copyless", + "derive_more", + "either", + "encoding_rs", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2", + "http", + "httparse", + "indexmap", + "itoa", + "language-tags", + "lazy_static", + "log", + "mime", + "percent-encoding", + "pin-project 1.0.2", + "rand", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "sha-1", + "slab", + "time", +] + +[[package]] +name = "actix-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd1f7dbda1645bf7da33554db60891755f6c01c1b2169e2f4c492098d30c235" +dependencies = [ + "bytestring", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227" +dependencies = [ + "actix-macros", + "actix-threadpool", + "copyless", + "futures-channel", + "futures-util", + "smallvec", + "tokio", +] + +[[package]] +name = "actix-server" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "futures-channel", + "futures-util", + "log", + "mio", + "mio-uds", + "num_cpus", + "slab", + "socket2", +] + +[[package]] +name = "actix-service" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0052435d581b5be835d11f4eb3bce417c8af18d87ddf8ace99f8e67e595882bb" +dependencies = [ + "futures-util", + "pin-project 0.4.27", +] + +[[package]] +name = "actix-testing" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c" +dependencies = [ + "actix-macros", + "actix-rt", + "actix-server", + "actix-service", + "log", + "socket2", +] + +[[package]] +name = "actix-threadpool" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30" +dependencies = [ + "derive_more", + "futures-channel", + "lazy_static", + "log", + "num_cpus", + "parking_lot", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb" +dependencies = [ + "actix-codec", + "actix-service", + "actix-utils", + "futures-util", +] + +[[package]] +name = "actix-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "bitflags", + "bytes", + "either", + "futures-channel", + "futures-sink", + "futures-util", + "log", + "pin-project 0.4.27", + "slab", +] + +[[package]] +name = "actix-web" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e641d4a172e7faa0862241a20ff4f1f5ab0ab7c279f00c2d4587b77483477b86" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-testing", + "actix-threadpool", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "awc", + "bytes", + "derive_more", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "log", + "mime", + "pin-project 1.0.2", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "socket2", + "time", + "tinyvec", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "askama" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", +] + +[[package]] +name = "askama_derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522" +dependencies = [ + "askama_shared", + "proc-macro2", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" + +[[package]] +name = "askama_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc" +dependencies = [ + "askama_escape", + "humansize", + "nom", + "num-traits", + "percent-encoding", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", +] + +[[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "awc" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b381e490e7b0cfc37ebc54079b0413d8093ef43d14a4e4747083f7fa47a9e691" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "base64", + "bytes", + "cfg-if 1.0.0", + "derive_more", + "futures-core", + "log", + "mime", + "percent-encoding", + "rand", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "backtrace" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bitvec" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "brotli2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +dependencies = [ + "brotli-sys", + "libc", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytestring" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "const_fn" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" + +[[package]] +name = "cookie" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "derive_more" +version = "0.99.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum-as-inner" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "flate2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "funty" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8" + +[[package]] +name = "futures" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-io" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" + +[[package]] +name = "futures-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] +name = "futures-task" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project 1.0.2", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + +[[package]] +name = "http" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "humansize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2", + "widestring", + "winapi 0.3.9", + "winreg", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 0.1.10", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "net2" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cf75f38f16cb05ea017784dc6dbfd354f76c223dba37701734c4f5a9337d02" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nom" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0" +dependencies = [ + "bitvec", + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +dependencies = [ + "pin-project-internal 0.4.27", +] + +[[package]] +name = "pin-project" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7" +dependencies = [ + "pin-project-internal 1.0.2", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "regex" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" + +[[package]] +name = "socket2" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "standback" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8" +dependencies = [ + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "syn" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" + +[[package]] +name = "thiserror" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcdaeea317915d59b2b4cd3b5efcd156c309108664277793f5351700c02ce98b" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +dependencies = [ + "bytes", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-uds", + "pin-project-lite 0.1.11", + "signal-hook-registry", + "slab", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.11", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.0", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +dependencies = [ + "pin-project 0.4.27", + "tracing", +] + +[[package]] +name = "trust-dns-proto" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53861fcb288a166aae4c508ae558ed18b53838db728d4d310aad08270a7d4c2b" +dependencies = [ + "async-trait", + "backtrace", + "enum-as-inner", + "futures", + "idna", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6759e8efc40465547b0dfce9500d733c65f969a4cbbfbe3ccf68daaa46ef179e" +dependencies = [ + "backtrace", + "cfg-if 0.1.10", + "futures", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" + +[[package]] +name = "web-app" +version = "0.1.0" +dependencies = [ + "actix-rt", + "actix-web", + "askama", +] + +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/examples/web-app/Cargo.toml b/examples/web-app/Cargo.toml new file mode 100644 index 0000000..365b97e --- /dev/null +++ b/examples/web-app/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "web-app" +version = "0.1.0" +authors = ["Typeable "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "3.3.0" +actix-rt = "1.1.1" +askama = "0.10.5" diff --git a/examples/web-app/README.md b/examples/web-app/README.md new file mode 100644 index 0000000..41a4088 --- /dev/null +++ b/examples/web-app/README.md @@ -0,0 +1,5 @@ +# Build Docker image + +```bash +docker build -f docker/Dockerfile -t typeable/octopod-web-app-example:v1 . +``` diff --git a/examples/web-app/charts/web-app/.helmignore b/examples/web-app/charts/web-app/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/examples/web-app/charts/web-app/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/examples/web-app/charts/web-app/Chart.yaml b/examples/web-app/charts/web-app/Chart.yaml new file mode 100644 index 0000000..0e0adae --- /dev/null +++ b/examples/web-app/charts/web-app/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: web-app +version: 0.1.0 diff --git a/examples/web-app/charts/web-app/templates/deployment.yaml b/examples/web-app/charts/web-app/templates/deployment.yaml new file mode 100644 index 0000000..773f07b --- /dev/null +++ b/examples/web-app/charts/web-app/templates/deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + namespace: {{ .Values.namespace }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + name: {{ .Release.Name }} + labels: + app: {{ .Release.Name }} + spec: + nodeSelector: + role: {{ .Values.nodeselector }} + terminationGracePeriodSeconds: 60 + containers: + - name: main + image: {{ .Values.image_prefix }}/{{ .Values.image }}:{{ .Values.image_tag }} + env: + {{- range $key, $value := .Values.env }} + - name: {{ $key | quote }} + value: {{ $value | quote }} + {{- end }} + ports: + - containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 0.1 + memory: 64Mi + limits: + cpu: 0.1 + memory: 128Mi + readinessProbe: + httpGet: + port: 8080 + path: / + periodSeconds: 20 + livenessProbe: + httpGet: + port: 8080 + path: / + initialDelaySeconds: 15 + periodSeconds: 5 diff --git a/examples/web-app/charts/web-app/templates/ingress.yaml b/examples/web-app/charts/web-app/templates/ingress.yaml new file mode 100644 index 0000000..8956770 --- /dev/null +++ b/examples/web-app/charts/web-app/templates/ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ .Release.Name }}-nginx-ingress + namespace: {{ .Values.namespace }} + annotations: + kubernetes.io/ingress.class: "nginx" + kubernetes.io/tls-acme: "true" + cert-manager.io/issuer: "{{ .Release.Name }}-certs" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-next-upstream: "http_502 error timeout" +spec: + tls: + - hosts: + - {{ .Values.domain }} + secretName: {{ .Release.Name }}-tls + rules: + - host: {{ .Values.domain }} + http: + paths: + - path: / + backend: + serviceName: {{ .Release.Name }} + servicePort: 80 diff --git a/examples/web-app/charts/web-app/templates/issuer.yaml b/examples/web-app/charts/web-app/templates/issuer.yaml new file mode 100644 index 0000000..4803946 --- /dev/null +++ b/examples/web-app/charts/web-app/templates/issuer.yaml @@ -0,0 +1,18 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ .Release.Name }}-certs + namespace: {{ .Release.Namespace }} +spec: + acme: + email: {{ .Values.acme_registration_email }} + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: {{ .Release.Name }}-letsencrypt + # ACME HTTP-01 provider configurations + solvers: + # An empty 'selector' means that this solver matches all domains + - selector: {} + http01: + ingress: + class: nginx diff --git a/examples/web-app/charts/web-app/templates/service.yaml b/examples/web-app/charts/web-app/templates/service.yaml new file mode 100644 index 0000000..f657847 --- /dev/null +++ b/examples/web-app/charts/web-app/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + namespace: {{ .Values.namespace }} + labels: + app: {{ .Release.Name }} +spec: + selector: + app: {{ .Release.Name }} + ports: + - name: app + port: 80 + targetPort: 8080 diff --git a/examples/web-app/charts/web-app/values.yaml b/examples/web-app/charts/web-app/values.yaml new file mode 100644 index 0000000..c7f1583 --- /dev/null +++ b/examples/web-app/charts/web-app/values.yaml @@ -0,0 +1,8 @@ +image_prefix: +image: web-app-example +image_tag: +namespace: deployment +domain: +nodeselector: stand +replicas: 1 +env: diff --git a/examples/web-app/docker/Dockerfile b/examples/web-app/docker/Dockerfile new file mode 100644 index 0000000..00d9058 --- /dev/null +++ b/examples/web-app/docker/Dockerfile @@ -0,0 +1,22 @@ +FROM ekidd/rust-musl-builder:1.48.0 as builder + +RUN sudo mkdir /app && \ + sudo chown -R rust:rust /app +WORKDIR /app +RUN USER=root cargo new web-app +WORKDIR /app/web-app +COPY Cargo.toml /app/web-app/ +COPY Cargo.lock /app/web-app/ +RUN cargo build --release +COPY src /app/web-app/src +COPY templates /app/web-app/templates +RUN rm -r ./target/x86_64-unknown-linux-musl/release/deps/web_app* && \ + cargo build --release --frozen + +FROM alpine:3.12.1 + +COPY --from=builder /app/web-app/target/x86_64-unknown-linux-musl/release/web-app /app/web-app +RUN addgroup -g 1000 user && \ + adduser -D -s /bin/sh -g 1000 -G user user + +ENTRYPOINT ["/app/web-app"] diff --git a/examples/web-app/src/main.rs b/examples/web-app/src/main.rs new file mode 100644 index 0000000..267064f --- /dev/null +++ b/examples/web-app/src/main.rs @@ -0,0 +1,43 @@ +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use askama::Template; + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate { + envs: Vec, +} + +#[derive(Debug, Clone, Template)] +#[template(source = "{{ key }} = {{ value }}", ext = "txt")] +struct Env { + key: String, + value: String, +} + +#[get("/")] +async fn index(state: web::Data>) -> impl Responder { + let body = IndexTemplate { + envs: state.to_vec(), + } + .render() + .expect("could not render template"); + HttpResponse::Ok().body(body) +} + +#[actix_rt::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| App::new().data(get_app_envs()).service(index)) + .bind("0.0.0.0:8080")? + .run() + .await +} + +fn get_app_envs() -> Vec { + std::env::vars() + .filter(|env| env.0.starts_with("APP_ENV")) + .map(|env| Env { + key: env.0, + value: env.1, + }) + .collect::>() +} diff --git a/examples/web-app/templates/index.html b/examples/web-app/templates/index.html new file mode 100644 index 0000000..3408b05 --- /dev/null +++ b/examples/web-app/templates/index.html @@ -0,0 +1,14 @@ + + + + + + + +
+ {% for env in envs %} +
{{ env }}
+ {% endfor %} +
+ + diff --git a/hie.yaml b/hie.yaml new file mode 100644 index 0000000..5a2f4e1 --- /dev/null +++ b/hie.yaml @@ -0,0 +1,19 @@ +cradle: + stack: + - path: "octo-cli/src" + component: "octo-cli:exe:octo" + + - path: "octopod-api/src" + component: "octopod-api:lib" + + - path: "octopod-backend/src" + component: "octopod-backend:lib" + + - path: "octopod-backend/app" + component: "octopod-backend:exe:octopod-exe" + + - path: "octopod-backend/test" + component: "octopod-backend:test:octopod-test" + + - path: "octopod-common/src" + component: "octopod-common:lib" diff --git a/img/demo.gif b/img/demo.gif new file mode 100644 index 0000000..c456668 Binary files /dev/null and b/img/demo.gif differ diff --git a/img/typeable.png b/img/typeable.png new file mode 100644 index 0000000..74d8fc3 Binary files /dev/null and b/img/typeable.png differ diff --git a/migrations/deploy/.gitkeep b/migrations/deploy/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/deploy/add_archived_status.sql b/migrations/deploy/add_archived_status.sql new file mode 100644 index 0000000..f5a7d47 --- /dev/null +++ b/migrations/deploy/add_archived_status.sql @@ -0,0 +1,7 @@ +-- Deploy octopod:add_archived_status to pg + +BEGIN; + +COMMIT; + +ALTER TYPE statuses ADD VALUE 'Archived' AFTER 'DeletePending'; diff --git a/migrations/deploy/add_deployment_overrides_and_deployment_log_overrides.sql b/migrations/deploy/add_deployment_overrides_and_deployment_log_overrides.sql new file mode 100644 index 0000000..004ffd1 --- /dev/null +++ b/migrations/deploy/add_deployment_overrides_and_deployment_log_overrides.sql @@ -0,0 +1,83 @@ +-- Deploy octopod:add_deployment_overrides_and_deployment_log_overrides to pg + +BEGIN; + +CREATE TYPE scopes AS ENUM ('App', 'Staging'); +CREATE TYPE visibilities AS ENUM ('Private', 'Public'); + +CREATE TABLE deployment_overrides ( + id BIGSERIAL PRIMARY KEY, + key TEXT NOT NULL, + value TEXT NOT NULL, + deployment_id BIGINT REFERENCES deployments(id), + scope scopes NOT NULL, + visibility visibilities NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE UNIQUE INDEX deployment_overrides_key_deployment_id_key ON deployment_overrides USING BTREE (key, deployment_id, scope); +CREATE INDEX deployment_overrides_deployment_id_idx ON deployment_overrides USING BTREE (deployment_id); + +CREATE TABLE deployment_log_overrides ( + id BIGSERIAL PRIMARY KEY, + key TEXT NOT NULL, + value TEXT NOT NULL, + deployment_log_id BIGINT REFERENCES deployment_logs(id), + scope scopes NOT NULL, + visibility visibilities NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE UNIQUE INDEX deployment_log_overrides_key_deployment_log_id_key ON deployment_log_overrides USING BTREE (key, deployment_log_id, scope); +CREATE INDEX deployment_log_overrides_deployment_log_id_idx ON deployment_log_overrides USING BTREE (deployment_log_id); + +DO LANGUAGE 'plpgsql' $$ +DECLARE + _r RECORD; + _r2 RECORD; + _k TEXT; + _v TEXT; +BEGIN + FOR _r IN SELECT id, envs FROM deployments LOOP + FOR _r2 IN SELECT regexp_split_to_table(_r.envs, '\n') as env LOOP + _k = split_part(_r2.env, '=', 1); + _v = split_part(_r2.env, '=', 2); + IF _k != '' THEN + INSERT INTO deployment_overrides (key, value, deployment_id, scope, visibility) + VALUES (_k, _v, _r.id, 'App', 'Public') + ON CONFLICT (key, deployment_id, scope) + DO + UPDATE SET value = _v, updated_at = now(); + END IF; + END LOOP; + END LOOP; +END +$$; + +DO LANGUAGE 'plpgsql' $$ +DECLARE + _r RECORD; + _r2 RECORD; + _k TEXT; + _v TEXT; +BEGIN + FOR _r IN SELECT id, envs FROM deployment_logs LOOP + FOR _r2 IN SELECT regexp_split_to_table(_r.envs, '\n') as env LOOP + _k = split_part(_r2.env, '=', 1); + _v = split_part(_r2.env, '=', 2); + IF _k != '' THEN + INSERT INTO deployment_log_overrides (key, value, deployment_log_id, scope, visibility) + VALUES (_k, _v, _r.id, 'App', 'Public') + ON CONFLICT (key, deployment_log_id, scope) + DO + UPDATE SET value = _v, updated_at = now(); + END IF; + END LOOP; + END LOOP; +END +$$; + +ALTER TABLE deployments DROP envs; +ALTER TABLE deployment_logs DROP envs; + +COMMIT; diff --git a/migrations/deploy/add_duration_and_stdout_and_stderr_to_deployment_logs.sql b/migrations/deploy/add_duration_and_stdout_and_stderr_to_deployment_logs.sql new file mode 100644 index 0000000..a6a346e --- /dev/null +++ b/migrations/deploy/add_duration_and_stdout_and_stderr_to_deployment_logs.sql @@ -0,0 +1,9 @@ +-- Deploy octopod:add_duration_and_stdout_and_stderr_to_deployment_logs to pg + +BEGIN; + +ALTER TABLE deployment_logs ADD duration INT NOT NULL DEFAULT 0; +ALTER TABLE deployment_logs ADD stdout TEXT NOT NULL DEFAULT ''; +ALTER TABLE deployment_logs ADD stderr TEXT NOT NULL DEFAULT ''; + +COMMIT; diff --git a/migrations/deploy/archived_flag.sql b/migrations/deploy/archived_flag.sql new file mode 100644 index 0000000..da00c1a --- /dev/null +++ b/migrations/deploy/archived_flag.sql @@ -0,0 +1,17 @@ +-- Deploy octopod:archived_flag to pg + +BEGIN; + +ALTER TABLE deployments ADD archived BOOLEAN NOT NULL DEFAULT 'f'; +ALTER TABLE deployments ADD archived_at TIMESTAMPTZ; + +CREATE INDEX deployments_archived_archived_at_idx ON deployments USING BTREE (archived, archived_at); + + +ALTER TABLE deployment_logs ADD archived BOOLEAN NOT NULL DEFAULT 'f'; + + +COMMIT; + +ALTER TYPE actions ADD VALUE 'delete' AFTER 'update'; +ALTER TYPE actions ADD VALUE 'restore' AFTER 'delete'; diff --git a/migrations/deploy/create_deployment_metadata.sql b/migrations/deploy/create_deployment_metadata.sql new file mode 100644 index 0000000..bd6804c --- /dev/null +++ b/migrations/deploy/create_deployment_metadata.sql @@ -0,0 +1,16 @@ +-- Deploy octopod:create_deployment_metadata to pg + +BEGIN; + +CREATE TABLE deployment_metadata ( + id BIGSERIAL PRIMARY KEY, + deployment_id BIGINT NOT NULL REFERENCES deployments(id), + key TEXT NOT NULL, + value TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX deployment_metadata_deployment_id_key_key ON deployment_metadata USING BTREE (deployment_id, key); + +COMMIT; diff --git a/migrations/deploy/deployment_logs.sql b/migrations/deploy/deployment_logs.sql new file mode 100644 index 0000000..b84ca89 --- /dev/null +++ b/migrations/deploy/deployment_logs.sql @@ -0,0 +1,17 @@ +-- Deploy octopod:deployment_logs to pg + +BEGIN; + +CREATE TYPE actions AS ENUM ('create', 'edit', 'update'); + +CREATE TABLE deployment_logs ( + id BIGSERIAL PRIMARY KEY, + deployment_id BIGINT NOT NULL REFERENCES deployments(id), + action actions, + tag TEXT, + envs TEXT, + exit_code SMALLINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +COMMIT; diff --git a/migrations/deploy/deployments.sql b/migrations/deploy/deployments.sql new file mode 100644 index 0000000..1b4c2d3 --- /dev/null +++ b/migrations/deploy/deployments.sql @@ -0,0 +1,16 @@ +-- Deploy octopod:deployments to pg + +BEGIN; + +CREATE TABLE deployments ( + id BIGSERIAL PRIMARY KEY, + name TEXT NOT NULL, + tag TEXT NOT NULL, + envs TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX deployments_name_key ON deployments USING BTREE (name); + +COMMIT; diff --git a/migrations/deploy/rename-delete-to-archive.sql b/migrations/deploy/rename-delete-to-archive.sql new file mode 100644 index 0000000..e695bdd --- /dev/null +++ b/migrations/deploy/rename-delete-to-archive.sql @@ -0,0 +1,8 @@ +-- Deploy octopod:rename-delete-to-archive to pg +BEGIN; + + +ALTER TYPE statuses RENAME VALUE 'DeletePending' TO 'ArchivePending'; + + +COMMIT; diff --git a/migrations/deploy/rename_delete_to_archive_2.sql b/migrations/deploy/rename_delete_to_archive_2.sql new file mode 100644 index 0000000..43ee23f --- /dev/null +++ b/migrations/deploy/rename_delete_to_archive_2.sql @@ -0,0 +1,7 @@ +-- Deploy octopod:rename_delete_to_archive_2 to pg + +BEGIN; + +ALTER TYPE actions RENAME VALUE 'delete' TO 'archive'; + +COMMIT; diff --git a/migrations/deploy/rename_elements_of_scope_enum.sql b/migrations/deploy/rename_elements_of_scope_enum.sql new file mode 100644 index 0000000..23a8f02 --- /dev/null +++ b/migrations/deploy/rename_elements_of_scope_enum.sql @@ -0,0 +1,8 @@ +-- Deploy octopod:rename_elements_of_scope_enum to pg + +BEGIN; + +ALTER TYPE scopes RENAME VALUE 'App' to 'ApplicationScope'; +ALTER TYPE scopes RENAME VALUE 'Staging' to 'DeploymentScope'; + +COMMIT; diff --git a/migrations/deploy/status_and_status_updated_at.sql b/migrations/deploy/status_and_status_updated_at.sql new file mode 100644 index 0000000..a4256da --- /dev/null +++ b/migrations/deploy/status_and_status_updated_at.sql @@ -0,0 +1,13 @@ +-- Deploy octopod:status_and_status_updated_at to pg + +BEGIN; + +CREATE TYPE statuses as ENUM ('Running', 'Failure', 'CreatePending', 'UpdatePending', 'DeletePending'); + +ALTER TABLE deployments ADD status statuses NOT NULL DEFAULT 'Running'; +ALTER TABLE deployments ADD status_updated_at TIMESTAMPTZ NOT NULL DEFAULT now(); +ALTER TABLE deployments ADD checked_at TIMESTAMPTZ NOT NULL DEFAULT now(); + +CREATE INDEX deployments_checked_at_idx ON deployments USING BTREE (checked_at); + +COMMIT; diff --git a/migrations/revert/.gitkeep b/migrations/revert/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/revert/add_archived_status.sql b/migrations/revert/add_archived_status.sql new file mode 100644 index 0000000..f1838c0 --- /dev/null +++ b/migrations/revert/add_archived_status.sql @@ -0,0 +1,7 @@ +-- Revert octopod:add_archived_status from pg + +BEGIN; + +-- XXX Add DDLs here. + +COMMIT; diff --git a/migrations/revert/add_deployment_overrides_and_deployment_log_overrides.sql b/migrations/revert/add_deployment_overrides_and_deployment_log_overrides.sql new file mode 100644 index 0000000..6141f66 --- /dev/null +++ b/migrations/revert/add_deployment_overrides_and_deployment_log_overrides.sql @@ -0,0 +1,46 @@ +-- Revert octopod:add_deployment_overrides_and_deployment_log_overrides from pg + +BEGIN; + +ALTER TABLE deployments ADD envs TEXT NOT NULL DEFAULT ''; +ALTER TABLE deployment_logs ADD envs TEXT NOT NULL DEFAULT ''; + +DO LANGUAGE 'plpgsql' $$ +DECLARE + _r RECORD; + _r2 RECORD; + _buf TEXT; +BEGIN + FOR _r IN SELECT DISTINCT deployment_id AS id FROM deployment_overrides LOOP + _buf = ''; + FOR _r2 IN SELECT key || '=' || value AS env FROM deployment_overrides WHERE deployment_id = _r.id ORDER BY id LOOP + _buf = _buf || _r2.env || E'\n'; + END LOOP; + UPDATE deployments SET envs = rtrim(_buf, E'\n') WHERE id = _r.id; + END LOOP; +END +$$; + +DO LANGUAGE 'plpgsql' $$ +DECLARE + _r RECORD; + _r2 RECORD; + _buf TEXT; +BEGIN + FOR _r IN SELECT DISTINCT deployment_log_id AS id FROM deployment_log_overrides LOOP + _buf = ''; + FOR _r2 IN SELECT key || '=' || value AS env FROM deployment_log_overrides WHERE deployment_log_id = _r.id ORDER BY id LOOP + _buf = _buf || _r2.env || E'\n'; + END LOOP; + UPDATE deployment_logs SET envs = rtrim(_buf, E'\n') WHERE id = _r.id; + END LOOP; +END +$$; + +DROP TABLE deployment_overrides; +DROP TABLE deployment_log_overrides; + +DROP TYPE scopes; +DROP TYPE visibilities; + +COMMIT; diff --git a/migrations/revert/add_duration_and_stdout_and_stderr_to_deployment_logs.sql b/migrations/revert/add_duration_and_stdout_and_stderr_to_deployment_logs.sql new file mode 100644 index 0000000..a3d8236 --- /dev/null +++ b/migrations/revert/add_duration_and_stdout_and_stderr_to_deployment_logs.sql @@ -0,0 +1,9 @@ +-- Revert octopod:add_duration_and_stdout_and_stderr_to_deployment_logs from pg + +BEGIN; + +ALTER TABLE deployment_logs DROP duration; +ALTER TABLE deployment_logs DROP stdout; +ALTER TABLE deployment_logs DROP stderr; + +COMMIT; diff --git a/migrations/revert/archived_flag.sql b/migrations/revert/archived_flag.sql new file mode 100644 index 0000000..c623992 --- /dev/null +++ b/migrations/revert/archived_flag.sql @@ -0,0 +1,10 @@ +-- Revert octopod:archived_flag from pg + +BEGIN; + +ALTER TABLE deployments DROP archived; +ALTER TABLE deployments DROP archived_at; + +ALTER TABLE deployment_logs DROP archived; + +COMMIT; diff --git a/migrations/revert/create_deployment_metadata.sql b/migrations/revert/create_deployment_metadata.sql new file mode 100644 index 0000000..0a9f328 --- /dev/null +++ b/migrations/revert/create_deployment_metadata.sql @@ -0,0 +1,7 @@ +-- Revert octopod:create_deployment_metadata from pg + +BEGIN; + +DROP TABLE deployment_metadata; + +COMMIT; diff --git a/migrations/revert/deployment_logs.sql b/migrations/revert/deployment_logs.sql new file mode 100644 index 0000000..9321f29 --- /dev/null +++ b/migrations/revert/deployment_logs.sql @@ -0,0 +1,9 @@ +-- Revert octopod:deployment_logs from pg + +BEGIN; + +DROP TABLE deployment_logs; + +DROP TYPE actions; + +COMMIT; diff --git a/migrations/revert/deployments.sql b/migrations/revert/deployments.sql new file mode 100644 index 0000000..071eb1c --- /dev/null +++ b/migrations/revert/deployments.sql @@ -0,0 +1,7 @@ +-- Revert octopod:deployments from pg + +BEGIN; + +DROP TABLE deployments; + +COMMIT; diff --git a/migrations/revert/rename-delete-to-archive.sql b/migrations/revert/rename-delete-to-archive.sql new file mode 100644 index 0000000..b2f2d20 --- /dev/null +++ b/migrations/revert/rename-delete-to-archive.sql @@ -0,0 +1,8 @@ +-- Revert octopod:rename-delete-to-archive from pg +BEGIN; + + +ALTER TYPE statuses RENAME VALUE 'ArchivePending' TO 'DeletePending'; + + +COMMIT; diff --git a/migrations/revert/rename_delete_to_archive_2.sql b/migrations/revert/rename_delete_to_archive_2.sql new file mode 100644 index 0000000..a6f32f1 --- /dev/null +++ b/migrations/revert/rename_delete_to_archive_2.sql @@ -0,0 +1,7 @@ +-- Revert octopod:rename_delete_to_archive_2 from pg + +BEGIN; + +ALTER TYPE actions RENAME VALUE 'archive' TO 'delete'; + +COMMIT; diff --git a/migrations/revert/rename_elements_of_scope_enum.sql b/migrations/revert/rename_elements_of_scope_enum.sql new file mode 100644 index 0000000..0571759 --- /dev/null +++ b/migrations/revert/rename_elements_of_scope_enum.sql @@ -0,0 +1,8 @@ +-- Revert octopod:rename_elements_of_scope_enum from pg + +BEGIN; + +ALTER TYPE scopes RENAME VALUE 'ApplicationScope' to 'App'; +ALTER TYPE scopes RENAME VALUE 'DeploymentScope' to 'Staging'; + +COMMIT; diff --git a/migrations/revert/status_and_status_updated_at.sql b/migrations/revert/status_and_status_updated_at.sql new file mode 100644 index 0000000..12c5c86 --- /dev/null +++ b/migrations/revert/status_and_status_updated_at.sql @@ -0,0 +1,11 @@ +-- Revert octopod:status_and_status_updated_at from pg + +BEGIN; + +ALTER TABLE deployments DROP status; +ALTER TABLE deployments DROP status_updated_at; +ALTER TABLE deployments DROP checked_at; + +DROP TYPE statuses; + +COMMIT; diff --git a/migrations/sqitch.conf b/migrations/sqitch.conf new file mode 100644 index 0000000..3447459 --- /dev/null +++ b/migrations/sqitch.conf @@ -0,0 +1,8 @@ +[core] + engine = pg + # plan_file = sqitch.plan + # top_dir = . +# [engine "pg"] + # target = db:pg: + # registry = sqitch + # client = psql diff --git a/migrations/sqitch.plan b/migrations/sqitch.plan new file mode 100644 index 0000000..b89e072 --- /dev/null +++ b/migrations/sqitch.plan @@ -0,0 +1,15 @@ +%syntax-version=1.0.0 +%project=octopod +%uri=https://github.com/sqitchers/sqitch-intro/ + +deployments 2020-02-09T15:47:32Z Typeable LLC # Creates deployments table. +deployment_logs 2020-02-28T21:35:30Z Typeable LLC # Creates deployment_logs table. +archived_flag 2020-06-09T20:30:45Z Typeable LLC # Add archived flag to 'deployments' and 'deployment_logs' tables +add_duration_and_stdout_and_stderr_to_deployment_logs 2020-06-26T09:30:35Z Typeable LLC # Add 'duration', 'stdout' and 'stderr' fields to 'deployment_logs' table +status_and_status_updated_at 2020-07-06T08:08:10Z Typeable LLC # Add status, status_updated_at, checked_at to 'deployments' table +add_deployment_overrides_and_deployment_log_overrides 2020-07-17T08:53:17Z Typeable LLC # Add 'deployment_overrides' and 'deployment_log_overrides' tables +add_archived_status 2020-07-28T11:40:30Z Typeable LLC # Add 'Archived' to statuses +create_deployment_metadata 2020-08-14T12:11:42Z Typeable LLC # Create 'deployment_metadata' table +rename_elements_of_scope_enum 2020-08-19T09:38:40Z Typeable LLC # Rename elements of 'scope' enum +rename-delete-to-archive 2020-11-23T10:53:10Z Ilya # Renamed delete to archive +rename_delete_to_archive_2 2020-11-26T08:28:58Z Typeable LLC # Renamed delete to archive diff --git a/migrations/verify/.gitkeep b/migrations/verify/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/verify/add_archived_status.sql b/migrations/verify/add_archived_status.sql new file mode 100644 index 0000000..dfdfb08 --- /dev/null +++ b/migrations/verify/add_archived_status.sql @@ -0,0 +1,7 @@ +-- Verify octopod:add_archived_status on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/add_deployment_overrides_and_deployment_log_overrides.sql b/migrations/verify/add_deployment_overrides_and_deployment_log_overrides.sql new file mode 100644 index 0000000..8278ca0 --- /dev/null +++ b/migrations/verify/add_deployment_overrides_and_deployment_log_overrides.sql @@ -0,0 +1,7 @@ +-- Verify octopod:add_deployment_overrides_and_deployment_log_overrides on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/add_duration_and_stdout_and_stderr_to_deployment_logs.sql b/migrations/verify/add_duration_and_stdout_and_stderr_to_deployment_logs.sql new file mode 100644 index 0000000..8a6ef34 --- /dev/null +++ b/migrations/verify/add_duration_and_stdout_and_stderr_to_deployment_logs.sql @@ -0,0 +1,7 @@ +-- Verify octopod:add_duration_and_stdout_and_stderr_to_deployment_logs on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/archived_flag.sql b/migrations/verify/archived_flag.sql new file mode 100644 index 0000000..053d1a6 --- /dev/null +++ b/migrations/verify/archived_flag.sql @@ -0,0 +1,7 @@ +-- Verify octopod:archived_flag on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/create_deployment_metadata.sql b/migrations/verify/create_deployment_metadata.sql new file mode 100644 index 0000000..e712984 --- /dev/null +++ b/migrations/verify/create_deployment_metadata.sql @@ -0,0 +1,7 @@ +-- Verify octopod:create_deployment_metadata on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/deployment_logs.sql b/migrations/verify/deployment_logs.sql new file mode 100644 index 0000000..b6e0ef2 --- /dev/null +++ b/migrations/verify/deployment_logs.sql @@ -0,0 +1,7 @@ +-- Verify octopod:deployment_logs on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/deployments.sql b/migrations/verify/deployments.sql new file mode 100644 index 0000000..d389176 --- /dev/null +++ b/migrations/verify/deployments.sql @@ -0,0 +1,7 @@ +-- Verify octopod:deployments on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/rename-delete-to-archive.sql b/migrations/verify/rename-delete-to-archive.sql new file mode 100644 index 0000000..3f73243 --- /dev/null +++ b/migrations/verify/rename-delete-to-archive.sql @@ -0,0 +1,7 @@ +-- Verify octopod:rename-delete-to-archive on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/rename_delete_to_archive_2.sql b/migrations/verify/rename_delete_to_archive_2.sql new file mode 100644 index 0000000..4bc1a3a --- /dev/null +++ b/migrations/verify/rename_delete_to_archive_2.sql @@ -0,0 +1,7 @@ +-- Verify octopod:rename_delete_to_archive_2 on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/rename_elements_of_scope_enum.sql b/migrations/verify/rename_elements_of_scope_enum.sql new file mode 100644 index 0000000..7e0f25b --- /dev/null +++ b/migrations/verify/rename_elements_of_scope_enum.sql @@ -0,0 +1,7 @@ +-- Verify octopod:rename_elements_of_scope_enum on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/migrations/verify/status_and_status_updated_at.sql b/migrations/verify/status_and_status_updated_at.sql new file mode 100644 index 0000000..1035729 --- /dev/null +++ b/migrations/verify/status_and_status_updated_at.sql @@ -0,0 +1,7 @@ +-- Verify octopod:status_and_status_updated_at on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..7bc5949 --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,120 @@ +{ sources ? import ./sources.nix + +, octopod-css ? ../octopod-css + +, migrations ? "please use '--arg migration '" +}: +with { + overlay = _: pkgs: + with pkgs; rec { + octo-cli = haskellPackages.octo-cli-static; + octopod-backend = haskellPackages.octopod-server-static; + octopod-frontend = haskellPackages.octopod-frontend-static; + + cacert' = cacert.overrideAttrs (o: { + fixupPhase = '' + cat $out/etc/ssl/certs/* > $out/etc/ssl/certs/ca-certificates.crt + ''; + }); + + octopod-server-container = dockerTools.buildImage { + name = "octopod-server-container-slim"; + contents = [ + octopod-backend + octopod-frontend + git + coreutils + bash + openssh + gnugrep + cacert' + shadow + ]; + + runAsRoot = '' + mkdir /tmp + chmod 777 /tmp + + mkdir -p /home/octopod + useradd octopod -d /home/octopod + chown octopod.octopod /home/octopod + + mkdir /app + cp -av ${octopod-backend}/bin/octopod-exe /app/octopod-exe + + mkdir -p /migrations/{deploy,revert,verify} + cp -av ${migrations}/* /migrations/ + + mkdir /tls /tls_store + + mkdir -p /www/static/{images,styles,vendors/outline} + cp -av ${octopod-frontend}/bin/frontend.jsexe/* /www/ + cp -av ${octopod-css}/production/images/* /www/static/images/ + cp -av ${octopod-css}/production/styles/* /www/static/styles/ + cp -av ${octopod-css}/production/vendors/outline/* /www/static/vendors/outline/ + ''; + + config = { + Entrypoint = [ "/app/octopod-exe" ]; + Cmd = [ + "--port" + "4443" + "--ui-port" + "4000" + "--ws-port" + "4020" + "--db" + "host='127.0.0.1' port=5432 user='octopod' password='octopod'" + "--db-pool-size" + "10" + "--tls-cert-path" + "/tls/server_cert.pem" + "--tls-key-path" + "/tls/server_key.pem" + "--tls-store-path" + "/tls_store" + ]; + Env = [ + "PATH=/utils:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + "USER=octopod" + ]; + }; + }; + + octo-cli-container = dockerTools.buildImage { + name = "octo-cli-container-slim"; + contents = [ octo-cli coreutils bash ]; + + runAsRoot = '' + mkdir /app + cp -av ${octo-cli}/bin/octo /app/octo + ''; + + config = { + Entrypoint = [ "/app/octo" ]; + Env = [ "EDITOR=${vim}/bin/vim" ]; + }; + }; + + niv = import sources.niv { }; + + haskellPackages = pkgs.haskellPackages.override { + overrides = hself: hsuper: { + octo-cli-static = haskell.lib.justStaticExecutables + (hsuper.callPackage ../default.nix { }).ghc.octo-cli; + + octopod-server-static = haskell.lib.justStaticExecutables + (hsuper.callPackage ../default.nix { }).ghc.octopod-backend; + + octopod-frontend-static = haskell.lib.justStaticExecutables + (hsuper.callPackage ../default.nix { }).ghcjs.octopod-frontend; + }; + }; + }; +}; +import + sources.nixpkgs # and use them again! +{ + overlays = [ overlay ]; + config = { }; +} diff --git a/nix/octo.nix b/nix/octo.nix new file mode 100644 index 0000000..38e8da9 --- /dev/null +++ b/nix/octo.nix @@ -0,0 +1,40 @@ +{ sources ? import ./sources.nix +, compiler ? "ghc865" +, pkgs-expr ? sources.nixpkgs +, static-haskell-nix ? sources.static-haskell-nix +}: +let + normalPkgs = import pkgs-expr { + overlays = [ + (self: super: { + haskell = super.haskell // { + packages = super.haskell.packages // { + "${compiler}" = super.haskell.packages."${compiler}".override { + overrides = + (hself: hsuper: { + octo-cli = hsuper.callCabal2nix "octo-cli" ../octo-cli { }; + octopod-api = hsuper.callCabal2nix "octopod-api" ../octopod-api { }; + octopod-common = hsuper.callCabal2nix "octopod-common" ../octopod-common { }; + hspec-wai = hsuper.callHackage "hspec-wai" "0.9.2" { }; + table-layout = hsuper.callHackageDirect + { + pkg = "table-layout"; + ver = "0.9.0.1"; + sha256 = "12nllfnh6b5mjda9qxfy192v0r0sx181w9zc9j70kvjdn7hgrb0y"; + } { }; + data-default-instances-base = hsuper.callHackageDirect + { + pkg = "data-default-instances-base"; + ver = "0.1.0.1"; + sha256 = "18basdy4qjn246phw008ll9zbi3rpdn6bh2dk0i81a60gsmyn58q"; + } { }; + }); + }; + }; + }; + }) + ]; + }; + static-haskell = import "${static-haskell-nix}/survey" { inherit normalPkgs compiler; }; +in +static-haskell.haskellPackages.octo-cli diff --git a/nix/sources.json b/nix/sources.json new file mode 100644 index 0000000..c4037ba --- /dev/null +++ b/nix/sources.json @@ -0,0 +1,86 @@ +{ + "hspec-webdriver-clone": { + "branch": "master", + "description": "Clone of hspec-webdriver, as the original source is inaccesible", + "homepage": null, + "owner": "dfordivam", + "repo": "hspec-webdriver-clone", + "rev": "0d748b7bb7cd74dce0a55a1ec86b01dbb8a71cd8", + "sha256": "1criynifhvmnqwhrshmzylikqkvlgq98xf72w9cdd2zpjw539qf0", + "type": "tarball", + "url": "https://github.com/dfordivam/hspec-webdriver-clone/archive/0d748b7bb7cd74dce0a55a1ec86b01dbb8a71cd8.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "niv": { + "branch": "master", + "description": "Easy dependency management for Nix projects", + "homepage": "https://github.com/nmattia/niv", + "owner": "nmattia", + "repo": "niv", + "rev": "ba57d5a29b4e0f2085917010380ef3ddc3cf380f", + "sha256": "1kpsvc53x821cmjg1khvp1nz7906gczq8mp83664cr15h94sh8i4", + "type": "tarball", + "url": "https://github.com/nmattia/niv/archive/ba57d5a29b4e0f2085917010380ef3ddc3cf380f.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs": { + "branch": "63ad5ab28ee380a4c6a974c9f70639f81a15f6ad", + "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "63ad5ab28ee380a4c6a974c9f70639f81a15f6ad", + "sha256": "1lppx6rsr52hdd1bp25ni4d6c8b76ip6lvr0iphfv0d1jjdf9r4h", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/63ad5ab28ee380a4c6a974c9f70639f81a15f6ad.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "obelisk": { + "branch": "master", + "description": "Functional reactive web and mobile applications, with batteries included.", + "homepage": "https://reflex-frp.org", + "owner": "obsidiansystems", + "repo": "obelisk", + "rev": "11beb6e8cd2419b2429925b76a98f24035e40985", + "sha256": "0b4m33b7yyzsbkvfz2kwg4v9hlnvbjlmjikbvwd7pg52vy84and0", + "type": "tarball", + "url": "https://github.com/obsidiansystems/obelisk/archive/11beb6e8cd2419b2429925b76a98f24035e40985.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "reflex-platform": { + "branch": "rc/0.6.0.0", + "description": "A curated package set and set of tools that let you build Haskell packages so they can run on a variety of platforms. reflex-platform is built on top of the nix package manager.", + "homepage": "https://reflex-frp.org", + "owner": "reflex-frp", + "repo": "reflex-platform", + "rev": "8d421e9e06b0477cbc065346aaf596c9db6cc387", + "sha256": "06fy5b0mk5k2ps1h78yihf4j76cb855r86y9p4jv5d91nfyl2dck", + "type": "tarball", + "url": "https://github.com/reflex-frp/reflex-platform/archive/8d421e9e06b0477cbc065346aaf596c9db6cc387.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "servant-reflex": { + "branch": "master", + "description": "Generate reflex client functions for querying a servant API", + "homepage": null, + "owner": "imalsogreg", + "repo": "servant-reflex", + "rev": "1109043c8437a375fc00bb48792e8dc377a84494", + "sha256": "0issnp95rnji3v9qifr0brypxsvmjkzanfq5lscj68lynnjv37g0", + "type": "tarball", + "url": "https://github.com/imalsogreg/servant-reflex/archive/1109043c8437a375fc00bb48792e8dc377a84494.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "static-haskell-nix": { + "branch": "master", + "description": "easily build most Haskell programs into fully static Linux executables", + "homepage": "", + "owner": "nh2", + "repo": "static-haskell-nix", + "rev": "382150290ba43b6eb41981c1ab3b32aa31798140", + "sha256": "0zsyplzf1k235rl26irm27y5ljd8ciayw80q575msxa69a9y2nvd", + "type": "tarball", + "url": "https://github.com/nh2/static-haskell-nix/archive/382150290ba43b6eb41981c1ab3b32aa31798140.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + } +} diff --git a/nix/sources.nix b/nix/sources.nix new file mode 100644 index 0000000..b796fff --- /dev/null +++ b/nix/sources.nix @@ -0,0 +1,171 @@ +# This file has been generated by Niv. + +let + + # + # The fetchers. fetch_ fetches specs of type . + # + + fetch_file = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchurl { inherit (spec) url sha256; name = name'; } + else + pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; + + fetch_tarball = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchTarball { name = name'; inherit (spec) url sha256; } + else + pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; + + fetch_git = name: spec: + let + ref = + if spec ? ref then spec.ref else + if spec ? branch then "refs/heads/${spec.branch}" else + if spec ? tag then "refs/tags/${spec.tag}" else + abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; + in + builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; }; + + fetch_local = spec: spec.path; + + fetch_builtin-tarball = name: throw + ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=tarball -a builtin=true''; + + fetch_builtin-url = name: throw + ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=file -a builtin=true''; + + # + # Various helpers + # + + # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 + sanitizeName = name: + ( + concatMapStrings (s: if builtins.isList s then "-" else s) + ( + builtins.split "[^[:alnum:]+._?=-]+" + ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) + ) + ); + + # The set of packages used when specs are fetched using non-builtins. + mkPkgs = sources: system: + let + sourcesNixpkgs = + import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; + hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; + hasThisAsNixpkgsPath = == ./.; + in + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then + import {} + else + abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # The actual fetching function. + fetch = pkgs: name: spec: + + if ! builtins.hasAttr "type" spec then + abort "ERROR: niv spec ${name} does not have a 'type' attribute" + else if spec.type == "file" then fetch_file pkgs name spec + else if spec.type == "tarball" then fetch_tarball pkgs name spec + else if spec.type == "git" then fetch_git name spec + else if spec.type == "local" then fetch_local spec + else if spec.type == "builtin-tarball" then fetch_builtin-tarball name + else if spec.type == "builtin-url" then fetch_builtin-url name + else + abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; + + # If the environment variable NIV_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + replace = name: drv: + let + saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; + ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; + in + if ersatz == "" then drv else ersatz; + + # Ports of functions for older nix versions + + # a Nix version of mapAttrs if the built-in doesn't exist + mapAttrs = builtins.mapAttrs or ( + f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) + ); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatMapStrings = f: list: concatStrings (map f list); + concatStrings = builtins.concatStringsSep ""; + + # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 + optionalAttrs = cond: as: if cond then as else {}; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" then + fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" then + fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) + else + fetchurl attrs; + + # Create the final "sources" from the config + mkSources = config: + mapAttrs ( + name: spec: + if builtins.hasAttr "outPath" spec + then abort + "The values in sources.json should not have an 'outPath' attribute" + else + spec // { outPath = replace name (fetch config.pkgs name spec); } + ) config.sources; + + # The "config" used by the fetchers + mkConfig = + { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null + , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) + , system ? builtins.currentSystem + , pkgs ? mkPkgs sources system + }: rec { + # The sources, i.e. the attribute set of spec name to spec + inherit sources; + + # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers + inherit pkgs; + }; + +in +mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/octo-cli/LICENSE b/octo-cli/LICENSE new file mode 100644 index 0000000..2094028 --- /dev/null +++ b/octo-cli/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2020, Typeable LLC + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Typeable LLC nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/octo-cli/Setup.hs b/octo-cli/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/octo-cli/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/octo-cli/octo-cli.cabal b/octo-cli/octo-cli.cabal new file mode 100644 index 0000000..1ffb332 --- /dev/null +++ b/octo-cli/octo-cli.cabal @@ -0,0 +1,63 @@ +cabal-version: 2.4 + +name: octo-cli +version: 0.1.0.0 +description: Please see the README on GitHub at +homepage: https://github.com/typeable/octopod +bug-reports: https://github.com/typeable/octopod/issues +license: BSD-3-Clause +license-file: LICENSE +author: Typeable +maintainer: octopod@typeable.io + +executable octo + main-is: Main.hs + other-modules: + Octopod.CLI + Octopod.CLI.Args + Octopod.CLI.TLS + Text.Layout.Table.Extras + default-extensions: + BlockArguments + ConstraintKinds + DataKinds + DeriveFunctor + DeriveGeneric + DerivingVia + DuplicateRecordFields + FlexibleContexts + GeneralizedNewtypeDeriving + LambdaCase + OverloadedStrings + ScopedTypeVariables + StandaloneDeriving + TypeApplications + TypeOperators + OverloadedLabels + build-depends: + aeson, + base ^>=4.12.0.0, + bytestring, + chronos, + connection, + data-default-class, + generic-lens, + http-client-tls, + http-client, + http-types, + lens, + octopod-api, + octopod-common, + optparse-applicative, + servant-client-core, + servant-client, + servant, + text, + tls, + transformers, + table-layout ^>= 0.9.0.0, + hs-source-dirs: src + default-language: Haskell2010 + ghc-options: + -Wall + -Werror diff --git a/octo-cli/src/Main.hs b/octo-cli/src/Main.hs new file mode 100644 index 0000000..8acc4b4 --- /dev/null +++ b/octo-cli/src/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Octopod.CLI + +main :: IO () +main = runOcto diff --git a/octo-cli/src/Octopod/CLI.hs b/octo-cli/src/Octopod/CLI.hs new file mode 100644 index 0000000..05e0abb --- /dev/null +++ b/octo-cli/src/Octopod/CLI.hs @@ -0,0 +1,311 @@ +module Octopod.CLI (runOcto) where + +import Chronos +import Control.Lens hiding (List) +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT) +import Data.Aeson (decode) +import qualified Data.ByteString.Char8 as BSC +import qualified Data.ByteString.Lazy.Char8 as LBSC +import Data.Coerce +import Data.Generics.Labels () +import Data.Generics.Product +import Data.Proxy +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Data.Text.Lens +import GHC.IO.Encoding +import Network.HTTP.Client (Manager) +import Prelude as P +import Servant.API +import Servant.Client + (BaseUrl(..), ClientEnv, ClientError, ClientM, client, mkClientEnv, + runClientM) +import Servant.Client.Core + (ClientError(FailureResponse), ResponseF(..), parseBaseUrl) +import System.Environment (lookupEnv) +import System.Exit + +import Common.Types +import Common.Utils (dfiName) +import Octopod.CLI.Args +import Octopod.CLI.TLS (makeClientManager) +import Octopod.PowerAPI +import Text.Layout.Table +import Text.Layout.Table.Extras () + +-- | Runs the octo CLI. +runOcto :: IO () +runOcto = do + setLocaleEncoding utf8 + args <- parseArgs + env <- getBaseUrl + manager <- initClientManager env + let clientEnv = mkClientEnv manager env + flip runReaderT clientEnv $ + case args of + Create tName tTag tSetAp tSetDep tSetPAp tSetPDep -> do + setApp <- liftIO $ parseSetApplicationOverrides Public tSetAp + setDep <- liftIO $ parseSetDeploymentOverrides Public tSetDep + setPApp <- liftIO $ parseSetApplicationOverrides Private tSetPAp + setPDep <- liftIO $ parseSetDeploymentOverrides Private tSetPDep + let + appOvs = setApp ++ setPApp + depOvs = setDep ++ setPDep + handleCreate $ Deployment (coerce tName) (coerce tTag) appOvs depOvs + List -> + handleList + Archive tName -> + handleArchive . coerce $ tName + Update tName tTag tSetAp tUnsAp tSetD tUnsD tSetPAp tSetPD -> do + setApp <- liftIO $ parseSetApplicationOverrides Public tSetAp + setDep <- liftIO $ parseSetDeploymentOverrides Public tSetD + unsetApp <- liftIO $ parseUnsetApplicationOverrides Public tUnsAp + unsetDep <- liftIO $ parseUnsetDeploymentOverrides Public tUnsD + setPApp <- liftIO $ parseSetApplicationOverrides Private tSetPAp + setPDep <- liftIO $ parseSetDeploymentOverrides Private tSetPD + let + appOvs = setApp ++ setPApp + depOvs = setDep ++ setPDep + tName' = coerce tName + tTag' = coerce tTag + handleUpdate tName' tTag' appOvs unsetApp depOvs unsetDep + Info tName -> + handleInfo . coerce $ tName + Cleanup tName -> + handleCleanup . coerce $ tName + Restore tName -> + handleRestore . coerce $ tName + CleanArchive -> + handleCleanArchive + GetActionLogs aId l -> handleGetActionInfo aId l + +-- | Initializes the HTTP connection manager. +initClientManager :: BaseUrl -> IO Manager +initClientManager baseURL = do + let + cert <- lookupEnv "TLS_CERT_PATH" + >>= maybe (pure $ TLSCertPath "cert.pem") (pure . TLSCertPath . BSC.pack) + key <- lookupEnv "TLS_KEY_PATH" + >>= maybe (pure $ TLSKeyPath "key.pem") (pure . TLSKeyPath . BSC.pack) + makeClientManager (baseUrlHost baseURL) cert key + +-- | Returns BaseUrl from 'OCTOPOD_URL' environment variable +-- or exit with exit_code=1. +getBaseUrl :: IO BaseUrl +getBaseUrl = do + octopodURL <- lookupEnv "OCTOPOD_URL" + >>= maybe (die "OCTOPOD_URL is not set") pure + case parseBaseUrl octopodURL of + Just url -> pure url + Nothing -> die "could not parse OCTOPOD_URL" + +-- | Handles the 'create' subcommand. +handleCreate :: Deployment -> ReaderT ClientEnv IO () +handleCreate createCmd = do + clientEnv <- ask + liftIO $ do + response <- runClientM (createH createCmd) clientEnv + handleResponse (const $ pure ()) response + +-- | Handles the 'list' subcommand. +handleList :: ReaderT ClientEnv IO () +handleList = do + clientEnv <- ask + liftIO $ do + resp <- runClientM listH clientEnv + let + getName dep = dep ^.dfiName + names = T.unlines . coerce . (getName <$>) <$> resp + handleResponse T.putStr names + +-- | Handles the 'archive' subcommand. +handleArchive :: DeploymentName -> ReaderT ClientEnv IO () +handleArchive dName = do + clientEnv <- ask + liftIO $ + handleResponse (const $ pure ()) =<< runClientM (archiveH dName) clientEnv + +-- | Handles the 'update' subcommand. +handleUpdate + :: DeploymentName + -> DeploymentTag + -> ApplicationOverrides + -> ApplicationOverrides + -> DeploymentOverrides + -> DeploymentOverrides + -> ReaderT ClientEnv IO () +handleUpdate dName dTag dNewAppOvs dOldAppOvs dNewDepOvs dOldDepOvs = do + clientEnv <- ask + liftIO $ do + let + dUpdate = DeploymentUpdate + { newTag = dTag + , newAppOverrides = dNewAppOvs + , oldAppOverrides = dOldAppOvs + , newDeploymentOverrides = dNewDepOvs + , oldDeploymentOverrides = dOldDepOvs + } + response <- runClientM (updateH dName dUpdate) clientEnv + handleResponse (const $ pure ()) response + +-- | Handles the 'info' subcommand. +handleInfo :: DeploymentName -> ReaderT ClientEnv IO () +handleInfo dName = do + clientEnv <- ask + liftIO $ do + res <- runClientM (infoH dName) clientEnv + case res of + Right (i : _) -> printInfo i + Right [] -> putStrLn notFoundMsg + Left err -> putStrLn $ "request failed, reason: " ++ show err + where + notFoundMsg = "deployment " ++ T.unpack (coerce dName) ++ " not found" + +-- | Handles the 'cleanup' subcommand. +handleCleanup :: DeploymentName -> ReaderT ClientEnv IO () +handleCleanup dName = do + clientEnv <- ask + liftIO $ + handleResponse (const $ pure ()) =<< runClientM (cleanupH dName) clientEnv + +-- | Handles the 'restore' subcommand. +handleRestore :: DeploymentName -> ReaderT ClientEnv IO () +handleRestore dName = do + clientEnv <- ask + liftIO $ + handleResponse (const $ pure ()) =<< runClientM (restoreH dName) clientEnv + +-- | Handles the 'clean-archive' subcommand. +handleCleanArchive :: ReaderT ClientEnv IO () +handleCleanArchive = do + clientEnv <- ask + liftIO $ + handleResponse (const $ pure ()) =<< runClientM cleanArchiveH clientEnv + +-- | Handles the 'logs' subcommand. +handleGetActionInfo :: ActionId -> LogOutput -> ReaderT ClientEnv IO () +handleGetActionInfo aId l = do + clientEnv <- ask + liftIO $ do + runClientM (getActionInfoH aId) clientEnv >>= \case + Left err -> print err + Right x -> case l of + Out -> T.putStrLn $ x ^. #stdout + Err -> T.putStrLn $ x ^. #stderr + ErrOut -> do + T.putStrLn "\t\tstdout:\n" + T.putStrLn $ x ^. #stdout + T.putStrLn "\t\tstderr:\n" + T.putStrLn $ x ^. #stderr + + +listH :: ClientM [DeploymentFullInfo] + +createH :: Deployment -> ClientM CommandResponse + +archiveH :: DeploymentName -> ClientM CommandResponse + +updateH :: DeploymentName -> DeploymentUpdate -> ClientM CommandResponse + +infoH :: DeploymentName -> ClientM [DeploymentInfo] + +_fullInfoH :: DeploymentName -> ClientM DeploymentFullInfo + +_statusH :: DeploymentName -> ClientM CurrentDeploymentStatus + +cleanupH :: DeploymentName -> ClientM CommandResponse + +restoreH :: DeploymentName -> ClientM CommandResponse + +getActionInfoH :: ActionId -> ClientM ActionInfo + +cleanArchiveH :: ClientM CommandResponse + +( listH + :<|> createH + :<|> archiveH + :<|> updateH + :<|> infoH + :<|> _fullInfoH + :<|> _statusH + :<|> cleanupH + :<|> restoreH) + :<|> getActionInfoH + :<|> cleanArchiveH = client (Proxy @PowerAPI) + +-- | Handles response from Octopod Server. +handleResponse :: (a -> IO ()) -> Either ClientError a -> IO () +handleResponse f (Right result) = f result +handleResponse _ (Left (FailureResponse _req res)) = + T.putStrLn . decodeError $ responseBody res +handleResponse _ (Left err) = + T.putStrLn $ "command failed due to unknown reason: " <> T.pack (show err) + +decodeError :: LBSC.ByteString -> Text +decodeError body = + case decode body of + Just (ValidationError nameErrors tagErrors) -> + T.concat ((<> "\n") <$> nameErrors) <> T.concat ((<> "\n") <$> tagErrors) + Just (AppError errorMsg) -> errorMsg + Just Success -> "ok" + _ -> "error: " <> (T.pack . LBSC.unpack $ body) + +-- | Pretty-prints the 'info' subcommand result. +printInfo :: DeploymentInfo -> IO () +printInfo (DeploymentInfo (Deployment _ dTag dAppOvs dStOvs) dMeta dLogs) = do + T.putStrLn "Current settings:" + T.putStrLn $ "tag: " <> coerce dTag + T.putStrLn $ "application overrides: " + <> (formatOverrides $ coerce <$> dAppOvs) + T.putStrLn $ "deployment overrides: " + <> (formatOverrides $ coerce <$> dStOvs) + T.putStrLn $ "metadata: " + forM_ dMeta $ \m -> + T.putStrLn $ + " " <> deploymentMetadataKey m <> ": " <> deploymentMetadataValue m + T.putStrLn "" + T.putStrLn "Last logs:" + ppDeploymentLogs dLogs + +ppDeploymentLogs :: [DeploymentLog] -> IO () +ppDeploymentLogs ds = + putStrLn + . tableString + [ column expand right noAlign def + , column expand center noAlign def + , column expand center noAlign def + , column expand center noAlign def + , column expand left (charAlign '=') def + , column expand left (charAlign '=') def + , column expand center noAlign def + ] + unicodeBoldHeaderS + (titlesH + [ "Created at" + , "Action id" + , "Action" + , "Tag" + , "App overrides" + , "Deployment overrides" + , "Exit code" + ]) + $ ppDeploymentLogRow <$> ds + +-- | Pretty-prints the deployment log. +ppDeploymentLogRow :: DeploymentLog -> RowGroup Text +ppDeploymentLogRow dLog = colsAllG top + [ [encode_YmdHMS SubsecondPrecisionAuto w3c + (timeToDatetime . Time . fromIntegral + $ dLog ^. field @"createdAt" * 10 ^ (9 :: Int))] + , [dLog ^. field @"actionId" . to unActionId . re _Show . packed] + , [dLog ^. field @"action" . coerced] + , [dLog ^. field @"deploymentTag" . coerced] + , dLog ^. field @"deploymentAppOverrides" + . to (fmap $ formatOverride . coerce) + , dLog ^. field @"deploymentDepOverrides" + . to (fmap $ formatOverride . coerce) + , [dLog ^. field @"exitCode" . re _Show . packed] + ] diff --git a/octo-cli/src/Octopod/CLI/Args.hs b/octo-cli/src/Octopod/CLI/Args.hs new file mode 100644 index 0000000..000c2f4 --- /dev/null +++ b/octo-cli/src/Octopod/CLI/Args.hs @@ -0,0 +1,200 @@ +{-| +Module : Octopod.CLI.Args +Description : octo CLI arguments parser utils. +-} + +module Octopod.CLI.Args where + +import Common.Types +import Data.Semigroup ((<>)) +import Data.Text +import Options.Applicative + +-- | octo CLI subcommands and arguments definition. +data Args + = Create + { name :: Text + -- ^ deployment name + , tag :: Text + -- ^ deployment tag + , setAppOverrides :: [Text] + -- ^ application-level overrides to set + , setDeploymentOverrides :: [Text] + -- ^ deployment-level overrides to set + , setAppPrivateOverrides :: [Text] + -- ^ application-level private overrides to set + , setDeploymentPrivateOverrides :: [Text] + -- ^ deployment-level private overrides to set + } + | List + | Archive + { name :: Text + -- ^ deployment name + } + | Update + { name :: Text + -- ^ deployment name + , tag :: Text + -- ^ deployment tag + , setAppOverrides :: [Text] + -- ^ application-level overrides to set + , unsetAppOverrides :: [Text] + -- ^ application-level overrides to unset + , setDeploymentOverrides :: [Text] + -- ^ deployment-level overrides to set + , unsetDeploymentOverrides :: [Text] + -- ^ deployment-level overrides to unset + , setAppPrivateOverrides :: [Text] + -- ^ application-level private overrides to set + , setDeploymentPrivateOverrides :: [Text] + -- ^ deployment-level private overrides to set + } + | Info + { name :: Text + -- ^ deployment name + } + | Cleanup + { name :: Text + -- ^ deployment name + } + | Restore + { name :: Text + -- ^ deployment name + } + | CleanArchive + | GetActionLogs ActionId LogOutput + deriving stock (Show) + +data LogOutput = Err | Out | ErrOut + deriving stock Show + +readLogOutput :: String -> Maybe LogOutput +readLogOutput "stderr" = Just Err +readLogOutput "stdout" = Just Out +readLogOutput "err" = Just Err +readLogOutput "out" = Just Out +readLogOutput "all" = Just ErrOut +readLogOutput "both" = Just ErrOut +readLogOutput _ = Nothing + +-- | Parses octo CLI subcommands and arguments. +parseArgs :: IO Args +parseArgs = execParser $ info (commandArgs <**> helper) fullDesc + +-- | Parses octo CLI subcommands. +commandArgs :: Parser Args +commandArgs = + hsubparser ( + command "create" (info createArgs (progDesc "create a new deployment")) + <> command "list" (info listArgs (progDesc "get names all deployments")) + <> command "archive" (info archiveArgs (progDesc "archive the deployment")) + <> command "update" (info updateArgs (progDesc "update the deployment")) + <> command "info" (info infoArgs (progDesc "get the deployment info")) + <> command "cleanup" (info cleanupArgs (progDesc "cleanup the deployment")) + <> command "restore" (info restoreArgs (progDesc "restore the deployment")) + <> command "clean-archive" + (info cleanupArchiveArgs (progDesc "cleanup all archived deployments")) + <> command "logs" + (info actionLogsArgs (progDesc "get deployment logs of a given action"))) + +-- | Parses arguments of 'create' subcommand. +createArgs :: Parser Args +createArgs = + Create + <$> strOption (long "name" <> short 'n' <> help "deployment name") + <*> strOption (long "tag" <> short 't' <> help "deployment tag") + <*> many (strOption + (long "set-app-env-override" + <> short 'e' + <> help "set application level override")) + <*> many (strOption + (long "set-deployment-override" + <> short 'o' + <> help "set deployment level override")) + <*> many (strOption + (long "set-app-env-private-override" + <> short 'a' + <> help "set application level private override" + <> internal)) + <*> many (strOption + (long "set-deployment-private-override" + <> short 's' + <> help "set deployment level private override" + <> internal)) + +-- | Parses arguments of 'list' subcommand. +listArgs :: Parser Args +listArgs = + pure List + +-- | Parses arguments of 'archive' subcommand. +archiveArgs :: Parser Args +archiveArgs = + Archive + <$> strOption (long "name" <> short 'n' <> help "deployment name") + +-- | Parses arguments of 'update' subcommand. +updateArgs :: Parser Args +updateArgs = + Update + <$> strOption (long "name" <> short 'n' <> help "deployment name") + <*> strOption (long "tag" <> short 't' <> help "deployment tag") + <*> many (strOption + (long "set-app-env-override" + <> short 'e' + <> help "set application level override")) + <*> many (strOption + (long "unset-app-env-override" + <> short 'E' + <> help "unset an application level override")) + <*> many (strOption + (long "set-deployment-override" + <> short 'o' + <> help "set deployment level override")) + <*> many (strOption + (long "unset-deployment-override" + <> short 'O' + <> help "unset a deployment level override")) + <*> many (strOption + (long "set-app-env-private-override" + <> short 'a' + <> help "set application level private override" + <> internal)) + <*> many (strOption + (long "set-deployment-private-override" + <> short 's' + <> help "set deployment level private override" + <> internal)) + +-- | Parses arguments of 'info' subcommand. +infoArgs :: Parser Args +infoArgs = + Info + <$> strOption (long "name" <> short 'n' <> help "deployment name") + +-- | Parses arguments of 'cleanup' subcommand. +cleanupArgs :: Parser Args +cleanupArgs = + Cleanup + <$> strOption (long "name" <> short 'n' <> help "deployment name") + +-- | Parses arguments of 'restore' subcommand. +restoreArgs :: Parser Args +restoreArgs = + Restore + <$> strOption (long "name" <> short 'n' <> help "deployment name") + +-- | Parses arguments of 'clean-archive' subcommand. +cleanupArchiveArgs :: Parser Args +cleanupArchiveArgs = + pure CleanArchive + +actionLogsArgs :: Parser Args +actionLogsArgs = GetActionLogs + <$> (ActionId <$> option auto (long "action" <> short 'a' <> help "action id")) + <*> option (maybeReader readLogOutput) + ( long "log-type" + <> short 'l' + <> help "log types to print. Values are: stdout, stderr, all" + <> value ErrOut + <> completeWith ["stdout", "stderr", "all"]) diff --git a/octo-cli/src/Octopod/CLI/TLS.hs b/octo-cli/src/Octopod/CLI/TLS.hs new file mode 100644 index 0000000..fce4e41 --- /dev/null +++ b/octo-cli/src/Octopod/CLI/TLS.hs @@ -0,0 +1,39 @@ +module Octopod.CLI.TLS + ( makeClientManager + ) where + +import Common.Types +import qualified Data.ByteString.Char8 as BSC +import Data.Coerce +import Data.Default.Class (def) +import Network.Connection +import Network.HTTP.Client + (Manager, managerResponseTimeout, newManager, responseTimeoutMicro) +import Network.HTTP.Client.TLS +import Network.TLS + (ClientHooks(..), clientHooks, clientSupported, credentialLoadX509, + defaultParamsClient, supportedCiphers) +import Network.TLS.Extra.Cipher + +-- | Initializes the HTTP client using TLS +makeClientManager :: String -> TLSCertPath -> TLSKeyPath -> IO Manager +makeClientManager octopodHostName cert key = do + creds <- do + let + cert' = BSC.unpack . coerce $ cert + key' = BSC.unpack . coerce $ key + either error Just <$> credentialLoadX509 cert' key' + let + hooks = def + { onCertificateRequest = \_ -> return creds + , onServerCertificate = \_ _ _ _ -> return [] + } + clientParams = (defaultParamsClient octopodHostName "") + { clientHooks = hooks + , clientSupported = def { supportedCiphers = ciphersuite_default } + } + tlsSettings = TLSSettings clientParams + timeout = 20 * 60 * 10 ^ (6 :: Int) + tlsManagerSettings' = (mkManagerSettings tlsSettings Nothing) + { managerResponseTimeout = responseTimeoutMicro timeout } + in newManager tlsManagerSettings' diff --git a/octo-cli/src/Text/Layout/Table/Extras.hs b/octo-cli/src/Text/Layout/Table/Extras.hs new file mode 100644 index 0000000..08446c6 --- /dev/null +++ b/octo-cli/src/Text/Layout/Table/Extras.hs @@ -0,0 +1,27 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Text.Layout.Table.Extras + ( + ) where + +import Data.Text +import qualified Data.Text as T +import Text.Layout.Table.Cell +import Text.Layout.Table.Primitives.AlignInfo +import Text.Layout.Table.StringBuilder + +instance Cell Text where + dropLeft = T.drop + dropRight = T.dropEnd + visibleLength = T.length + measureAlignment p xs = case T.break p xs of + (ls, rs) -> AlignInfo (T.length ls) $ + if T.null rs + then Nothing + else Just $ T.length rs - 1 + buildCell = stringB . T.unpack + +instance StringBuilder Text where + stringB = T.pack + charB = T.singleton + replicateCharB x c = T.replicate x (T.singleton c) diff --git a/octopod-api/LICENSE b/octopod-api/LICENSE new file mode 100644 index 0000000..2094028 --- /dev/null +++ b/octopod-api/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2020, Typeable LLC + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Typeable LLC nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/octopod-api/Setup.hs b/octopod-api/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/octopod-api/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/octopod-api/octopod-api.cabal b/octopod-api/octopod-api.cabal new file mode 100644 index 0000000..fa43bd6 --- /dev/null +++ b/octopod-api/octopod-api.cabal @@ -0,0 +1,46 @@ +cabal-version: 2.4 + +name: octopod-api +version: 0.1.0.0 +description: Please see the README on GitHub at +homepage: https://github.com/typeable/octopod +bug-reports: https://github.com/typeable/octopod/issues +license: BSD-3-Clause +license-file: LICENSE +author: Typeable +maintainer: octopod@typeable.io + +library + exposed-modules: + Octopod.API + Octopod.API.WebSocket + Octopod.PowerAPI + default-extensions: + BlockArguments + ConstraintKinds + DataKinds + DeriveFunctor + DeriveGeneric + DerivingVia + DuplicateRecordFields + FlexibleContexts + GeneralizedNewtypeDeriving + LambdaCase + OverloadedStrings + ScopedTypeVariables + StandaloneDeriving + TypeApplications + TypeOperators + build-depends: + base ^>=4.12.0.0, + servant, + octopod-common, + if !impl(ghcjs) + build-depends: + servant-websockets, + aeson, + hs-source-dirs: src + default-language: Haskell2010 + ghc-options: + -Wall + -Werror diff --git a/octopod-api/src/Octopod/API.hs b/octopod-api/src/Octopod/API.hs new file mode 100644 index 0000000..2e2c9d6 --- /dev/null +++ b/octopod-api/src/Octopod/API.hs @@ -0,0 +1,65 @@ +{-| +Module : Octopod.API +Description : Backend endpoints. + +This module contains backend endpoints. +-} + + +module Octopod.API + ( module Octopod.API.WebSocket + , API + ) where + +import Servant.API + +import Common.Types +import Octopod.API.WebSocket + +type CaptureName = Capture "name" DeploymentName + +type ListEndpoint = Get '[JSON] [DeploymentFullInfo] +type CreateEndpoint = + ReqBody '[JSON] Deployment :> Post '[JSON] CommandResponse +type ArchiveEndpoint c = c :> Delete '[JSON] CommandResponse +type UpdateEndpoint c = + c :> ReqBody '[JSON] DeploymentUpdate :> Put '[JSON] CommandResponse +type InfoEndpoint c = c :> "info" :> Get '[JSON] [DeploymentInfo] +type FullInfoEndpoint c = c :> "full_info" :> Get '[JSON] DeploymentFullInfo +type StatusEndpoint c = c :> "status" :> Get '[JSON] CurrentDeploymentStatus +type RestoreEndpoint c = + c :> "restore" :> Patch '[JSON] CommandResponse + +type PingEndpoint = + "ping" :> GetNoContent '[PlainText] NoContent +type ProjectNameEndpoint = + "project_name" :> Get '[JSON] ProjectName + +type DeploymentAPI' c = + "api" :> "v1" :> + ( "deployments" :> + ( ListEndpoint + -- endpoint to get deployment list + :<|> CreateEndpoint + -- endpoint to create a new deployment + :<|> ArchiveEndpoint c + -- endpoint to archive existing deployment + :<|> UpdateEndpoint c + -- endpoint to update exists deployment + :<|> InfoEndpoint c + -- endpoint to get deployment info + :<|> FullInfoEndpoint c + -- endpoint to get deployment full info + :<|> StatusEndpoint c + -- endpoint to get deployment status + :<|> RestoreEndpoint c + -- endpoint to restore deployment + ) + :<|> PingEndpoint + -- endpoint to liveness probe + :<|> ProjectNameEndpoint + -- endpoint to get project name + ) + +-- | API for frontend +type API = DeploymentAPI' CaptureName diff --git a/octopod-api/src/Octopod/API/WebSocket.hs b/octopod-api/src/Octopod/API/WebSocket.hs new file mode 100644 index 0000000..433ed87 --- /dev/null +++ b/octopod-api/src/Octopod/API/WebSocket.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE CPP #-} + +module Octopod.API.WebSocket where + +#ifndef ghcjs_HOST_OS + +import Data.Aeson +import Servant.API +import Servant.API.WebSocketConduit + +#endif + + +type ApiWSPath = "event" + + +#ifndef ghcjs_HOST_OS + +-- | WS API +type WebSocketAPI = ApiWSPath :> WebSocketSource Value + +#endif diff --git a/octopod-api/src/Octopod/PowerAPI.hs b/octopod-api/src/Octopod/PowerAPI.hs new file mode 100644 index 0000000..ae2c9db --- /dev/null +++ b/octopod-api/src/Octopod/PowerAPI.hs @@ -0,0 +1,71 @@ +{-| +Module : Octopod.PowerAPI +Description : Backend endpoints. + +This module contains backend endpoints. +-} + + +module Octopod.PowerAPI + ( module Octopod.API.WebSocket + , PowerAPI + ) where + +import Servant.API + +import Common.Types +import Octopod.API.WebSocket + +type CaptureName = Capture "name" DeploymentName + +type ListEndpoint = Get '[JSON] [DeploymentFullInfo] +type CreateEndpoint = + ReqBody '[JSON] Deployment :> Post '[JSON] CommandResponse +type ArchiveEndpoint c = c :> Delete '[JSON] CommandResponse +type UpdateEndpoint c = + c :> ReqBody '[JSON] DeploymentUpdate :> Put '[JSON] CommandResponse +type InfoEndpoint c = c :> "info" :> Get '[JSON] [DeploymentInfo] +type FullInfoEndpoint c = c :> "full_info" :> Get '[JSON] DeploymentFullInfo +type StatusEndpoint c = c :> "status" :> Get '[JSON] CurrentDeploymentStatus +type CleanupEndpoint c = + c :> "cleanup" :> Delete '[JSON] CommandResponse +type RestoreEndpoint c = + c :> "restore" :> Patch '[JSON] CommandResponse + +type GetActionInfoEndpoint = + "log" :> Capture "action_id" ActionId :> Get '[JSON] ActionInfo + +type CleanArchiveEndpoint = + "clean_archive" :> Delete '[JSON] CommandResponse + +type DeploymentAPI' c = + "api" :> "v1" :> + ( "deployments" :> + ( ListEndpoint + -- endpoint to get deployment list + :<|> CreateEndpoint + -- endpoint to create a new deployment + :<|> ArchiveEndpoint c + -- endpoint to archive an existing deployment + :<|> UpdateEndpoint c + -- endpoint to update an existing deployment + :<|> InfoEndpoint c + -- endpoint to get deployment info + :<|> FullInfoEndpoint c + -- endpoint to get full deployment info + :<|> StatusEndpoint c + -- endpoint to get deployment status + :<|> CleanupEndpoint c + -- endpoint to clean up resources of an archived deployment + :<|> RestoreEndpoint c + -- endpoint to restore an archived deployment + ) + :<|> GetActionInfoEndpoint + -- endpoint to get action logs + :<|> CleanArchiveEndpoint + -- endpoint to clean up resources of all archived deployments + -- according to the archive retention policy + ) + +-- | API for the octo CLI +type PowerAPI = DeploymentAPI' CaptureName diff --git a/octopod-backend/LICENSE b/octopod-backend/LICENSE new file mode 100644 index 0000000..e637cde --- /dev/null +++ b/octopod-backend/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2020 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/octopod-backend/Setup.hs b/octopod-backend/Setup.hs new file mode 100644 index 0000000..4467109 --- /dev/null +++ b/octopod-backend/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/octopod-backend/app/Main.hs b/octopod-backend/app/Main.hs new file mode 100644 index 0000000..c2e6588 --- /dev/null +++ b/octopod-backend/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import Octopod.Server + +main :: IO () +main = runOctopodServer diff --git a/octopod-backend/octopod-backend.cabal b/octopod-backend/octopod-backend.cabal new file mode 100644 index 0000000..f8e16fc --- /dev/null +++ b/octopod-backend/octopod-backend.cabal @@ -0,0 +1,189 @@ +cabal-version: 2.4 +name: octopod-backend +version: 0.1.0.0 +description: Please see the README on GitHub at +homepage: https://github.com/typeable/octopod +bug-reports: https://github.com/typeable/octopod/issues +author: Typeable +maintainer: octopod@typeable.io +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + ChangeLog.md + +source-repository head + type: git + location: https://github.com/typeable/octopod + +library + ghc-options: -Wall + -Werror + default-extensions: BlockArguments + , ConstraintKinds + , DataKinds + , DeriveFunctor + , DeriveGeneric + , DerivingVia + , DuplicateRecordFields + , FlexibleContexts + , GeneralizedNewtypeDeriving + , LambdaCase + , OverloadedStrings + , ScopedTypeVariables + , StandaloneDeriving + , TypeApplications + , TypeOperators + exposed-modules: + Octopod.Server + Octopod.Server.Args + Octopod.Server.ControlScriptUtils + Octopod.Server.Logger + Octopod.Server.Posix + Orphans + TLS + Types + hs-source-dirs: + src + build-depends: + aeson + , async + , base >=4.7 && <5 + , bytestring + , chronos + , conduit + , connection + , data-default-class + , deriving-aeson + , directory + , octopod-common + , fast-logger + , filepath + , generic-lens + , http-api-data + , http-client + , http-client-tls + , lens + , mtl + , network-uri + , octopod-api + , optparse-applicative + , optparse-generic + , postgresql-error-codes + , postgresql-simple + , resource-pool + , servant + , servant-client + , servant-client-core + , servant-server + , servant-websockets + , stm + , temporary + , text + , tls + , transformers + , transformers-base + , typed-process + , unix + , warp + , warp-tls + , x509 + , x509-store + , x509-validation + default-language: Haskell2010 + +executable octopod-exe + main-is: Main.hs + ghc-options: -Wall + -Werror + hs-source-dirs: app + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , async + , base >=4.7 && <5 + , bytestring + , chronos + , conduit + , connection + , data-default-class + , directory + , octopod-backend + , fast-logger + , http-client + , http-client-tls + , mtl + , network-uri + , optparse-applicative + , optparse-generic + , postgresql-error-codes + , postgresql-simple + , resource-pool + , servant + , servant-client + , servant-client-core + , servant-server + , servant-websockets + , stm + , temporary + , text + , tls + , transformers + , transformers-base + , typed-process + , unix + , warp + , warp-tls + , x509 + , x509-store + , x509-validation + default-language: Haskell2010 + +test-suite octopod-test + type: exitcode-stdio-1.0 + main-is: Spec.hs + hs-source-dirs: + test + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , async + , base >=4.7 && <5 + , bytestring + , chronos + , conduit + , connection + , data-default-class + , directory + , octopod-backend + , fast-logger + , filepath + , http-client + , http-client-tls + , mtl + , network-uri + , optparse-applicative + , optparse-generic + , postgresql-error-codes + , postgresql-simple + , resource-pool + , servant + , servant-client + , servant-client-core + , servant-server + , servant-websockets + , stm + , temporary + , text + , tls + , transformers + , transformers-base + , typed-process + , unix + , warp + , warp-tls + , x509 + , x509-store + , x509-validation + default-language: Haskell2010 diff --git a/octopod-backend/src/Octopod/Server.hs b/octopod-backend/src/Octopod/Server.hs new file mode 100644 index 0000000..e470916 --- /dev/null +++ b/octopod-backend/src/Octopod/Server.hs @@ -0,0 +1,1236 @@ +module Octopod.Server (runOctopodServer) where + + +import Chronos (Time, getTime, now) +import Control.Applicative +import Control.Concurrent (forkFinally, threadDelay) +import Control.Concurrent.Async (race_) +import Control.Concurrent.MVar +import Control.Concurrent.STM +import Control.Exception (Exception, throwIO, try) +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT) +import Data.Aeson (Value(..), encode, toJSON) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Lazy as BSL +import Data.Coerce +import Data.Conduit (ConduitT, yield) +import Data.Foldable (foldrM) +import Data.Functor ((<&>)) +import Data.Int (Int64) +import Data.IORef +import Data.Maybe +import Data.Pool +import Data.Text (lines, pack, unpack, unwords) +import Data.Text.IO (hGetContents) +import Data.Traversable +import Database.PostgreSQL.Simple +import Network.Wai.Handler.Warp +import Network.Wai.Handler.WarpTLS +import Options.Generic +import PostgreSQL.ErrorCodes (unique_violation) +import Prelude hiding (lines, log, unlines, unwords) +import Servant +import System.Environment (lookupEnv) +import System.Exit +import System.Log.FastLogger +import System.Posix.Signals (sigTERM) +import System.Process.Typed + + +import Common.Utils +import Common.Validation (isNameValid) +import Octopod.API +import Octopod.PowerAPI +import Octopod.Server.Args +import Octopod.Server.ControlScriptUtils +import Octopod.Server.Logger +import Octopod.Server.Posix +import Orphans () +import TLS (createTLSOpts) +import Types + +type PgPool = Pool Connection +type AppM = ReaderT AppState Handler + +-- | Octopod Server state definition. +data AppState = AppState + { pool :: PgPool + -- ^ postgres pool + , logger :: TimedFastLogger + -- ^ logger + , eventSink :: TChan WSEvent + -- ^ channel for WS events for the frontend + , bgWorkersCounter :: IORef Int + -- ^ background workers counter + , gracefulShutdownActivated :: IORef Bool + -- ^ flag of activating graceful shutdown + , shutdownSem :: MVar () + -- ^ semaphore for graceful shutdown + , projectName :: ProjectName + -- ^ project name + , baseDomain :: Domain + -- ^ base domain + , namespace :: Namespace + -- ^ namespace + , archiveRetention :: ArchiveRetention + -- ^ archive retention + , statusUpdateTimeout :: Timeout + -- ^ status update timeout + , creationCommand :: Command + -- ^ creation command path + , updateCommand :: Command + -- ^ update command path + , archiveCommand :: Command + -- ^ deletion command path + , checkingCommand :: Command + -- ^ checking command path + , cleanupCommand :: Command + -- ^ cleanup command path + , archiveCheckingCommand :: Command + -- ^ archive checking command path + , tagCheckingCommand :: Command + -- ^ tag checking command path + , infoCommand :: Command + } + +-- | Deployment exception definition. +data DeploymentException + = DeploymentFailed Int + | ThereIsActiveDeployment + deriving (Show) + +instance Exception DeploymentException + +-- | Deployment list type definition. +data DeploymentListType + = AllDeployments + | ArchivedOnlyDeployments + | ActiveOnlyDeployments + deriving (Show) + +-- | Full info list type definition. +data FullInfoListType + = FullInfoForAll + | FullInfoOnlyForOne DeploymentName + deriving (Show) + +-- | Deployment search errors definition. +data DeploymentNotFound + = DeploymentNotFound + | ArchivedDeploymentNotFound + | ActiveDeploymentNotFound + | DeploymentHasPendingStatus DeploymentStatus + +-- | Definition of a filter by pending statuses. +data FilterByPending + = AllowPending + | DenyPending + +runOctopodServer :: IO () +runOctopodServer = do + logger' <- newLogger + logInfo logger' "started" + bgWorkersC <- newIORef 0 + gracefulShutdownAct <- newIORef False + shutdownS <- newEmptyMVar + void $ do + let + termHandler = terminationHandler bgWorkersC gracefulShutdownAct shutdownS + installShutdownHandler logger' [sigTERM] termHandler + opts <- parseArgs + let + a ?! e = a >>= maybe (die e) pure + getEnvOrDie eName = lookupEnv eName ?! (eName <> " is not set") + projName <- coerce . pack <$> getEnvOrDie "PROJECT_NAME" + domain <- coerce . pack <$> getEnvOrDie "BASE_DOMAIN" + ns <- coerce . pack <$> getEnvOrDie "NAMESPACE" + archRetention <- coerce . read @Int <$> getEnvOrDie "ARCHIVE_RETENTION" + stUpdateTimeout <- coerce . read @Int <$> getEnvOrDie "STATUS_UPDATE_TIMEOUT" + creationCmd <- coerce . pack <$> getEnvOrDie "CREATION_COMMAND" + updateCmd <- coerce . pack <$> getEnvOrDie "UPDATE_COMMAND" + archiveCmd <- coerce . pack <$> getEnvOrDie "ARCHIVE_COMMAND" + checkingCmd <- coerce . pack <$> getEnvOrDie "CHECKING_COMMAND" + cleanupCmd <- coerce . pack <$> getEnvOrDie "CLEANUP_COMMAND" + archiveCheckingCmd <- coerce . pack <$> getEnvOrDie "ARCHIVE_CHECKING_COMMAND" + tagCheckingCmd <- coerce . pack <$> getEnvOrDie "TAG_CHECKING_COMMAND" + infoCmd <- coerce . pack <$> getEnvOrDie "INFO_COMMAND" + pgPool <- initConnectionPool + (unDBConnectionString $ octopodDB opts) + (unDBPoolSize $ octopodDBPoolSize opts) + channel <- liftIO . atomically $ newBroadcastTChan + let + appSt = + AppState + pgPool + logger' + channel + bgWorkersC + gracefulShutdownAct + shutdownS + projName + domain + ns + archRetention + stUpdateTimeout + creationCmd + updateCmd + archiveCmd + checkingCmd + cleanupCmd + archiveCheckingCmd + tagCheckingCmd + infoCmd + app' = app appSt + powerApp' = powerApp appSt + wsApp' = wsApp channel + serverPort = octopodPort opts + uiServerPort = unServerPort $ octopodUIPort opts + wsServerPort = unServerPort $ octopodWSPort opts + tlsOpts = + createTLSOpts (octopodTLSCertPath opts) (octopodTLSKeyPath opts) + (octopodTLSStorePath opts) serverPort + warpOpts = setPort (unServerPort serverPort) defaultSettings + in + (run uiServerPort app') + `race_` (runTLS tlsOpts warpOpts powerApp') + `race_` (run wsServerPort wsApp') + `race_` (runStatusUpdater appSt) + `race_` (runShutdownHandler appSt) + +-- | Initializes the connection pool. +initConnectionPool :: ByteString -> Int -> IO PgPool +initConnectionPool dbConnStr = + createPool (connectPostgreSQL dbConnStr) close 1 30 + +-- | Helper to run the server. +nt :: AppState -> AppM a -> Handler a +nt s x = runReaderT x s + +-- | Application with the Web UI API. +app :: AppState -> Application +app s = serve api $ hoistServer api (nt s) server + where + api = Proxy @API + +-- | Request handlers of the Web UI API application. +server :: ServerT API AppM +server = + ( listH :<|> createH :<|> archiveH :<|> updateH + :<|> infoH :<|> fullInfoH :<|> statusH :<|> restoreH + ) :<|> pingH :<|> projectNameH + +-- | Application with the octo CLI API. +powerApp :: AppState -> Application +powerApp s = serve api $ hoistServer api (nt s) powerServer + where + api = Proxy @PowerAPI + +-- | Request handlers of application with the octo CLI API. +powerServer :: ServerT PowerAPI AppM +powerServer = + ( powerListH :<|> createH :<|> archiveH :<|> updateH + :<|> powerInfoH :<|> powerFullInfoH :<|> statusH :<|> cleanupH :<|> restoreH + ) :<|> getActionInfoH :<|> cleanArchiveH + +-- | Application with the WS API. +wsApp :: TChan WSEvent -> Application +wsApp channel = serve api $ wsServer channel + where + api = Proxy @WebSocketAPI + +-- | Request handlers of the application with the WS API. +wsServer :: TChan WSEvent -> Server WebSocketAPI +wsServer = eventS + +-- | Handles WS events. +eventS :: MonadIO m => TChan WSEvent -> ConduitT () Value m () +eventS channel = do + dupChannel <- liftIO . atomically $ dupTChan channel + forever $ do + event <- liftIO . atomically . readTChan $ dupChannel + yield . toJSON $ event + +-- | Handles the 'list' request of the Web UI API. +listH :: AppM [DeploymentFullInfo] +listH = hidePrivateOverridesInFullInfos <$> getFullInfo FullInfoForAll + +-- | Handles the 'list' request of the octo CLI API. +powerListH :: AppM [DeploymentFullInfo] +powerListH = getFullInfo FullInfoForAll + +-- | Handles the 'full_info' request of the Web UI API. +fullInfoH :: DeploymentName -> AppM DeploymentFullInfo +fullInfoH dName = do + fullInfoList <- hidePrivateOverridesInFullInfos <$> + getFullInfo (FullInfoOnlyForOne dName) + case fullInfoList of + fullInfo : _ -> pure fullInfo + [] -> throwError err404 + { errBody = validationError ["Name not found"] [] } + +-- | Handles the 'full_info' request of the octo CLI API. +powerFullInfoH :: DeploymentName -> AppM DeploymentFullInfo +powerFullInfoH dName = do + fullInfoList <- getFullInfo $ FullInfoOnlyForOne dName + case fullInfoList of + fullInfo : _ -> pure fullInfo + [] -> throwError err404 + { errBody = validationError ["Name not found"] [] } + +-- | Hides private overrides in 'full_info' response. +hidePrivateOverridesInFullInfos :: [DeploymentFullInfo] -> [DeploymentFullInfo] +hidePrivateOverridesInFullInfos dFullInfos = do + dFullInfos <&> \(DeploymentFullInfo dep s a ct ut u) -> + let + hidePrivate (Deployment n t appOvs depOvs) = + Deployment n t (hideP appOvs) (hideP depOvs) + hideP o = + coerce o <&> \(Override k v vis) -> + let + v' = case vis of + Private -> "*" + Public -> v + in coerce $ Override k v' vis + in DeploymentFullInfo (hidePrivate dep) s a ct ut u + +-- | Helper to get full_info from the database. +getFullInfo :: FullInfoListType -> AppM [DeploymentFullInfo] +getFullInfo listType = do + AppState {pool = p, logger = l} <- ask + deployments <- liftIO $ withResource p $ \conn -> do + rows <- case listType of + FullInfoForAll -> query_ conn qAll + FullInfoOnlyForOne (dName) -> query conn qOne (Only dName) + for rows $ \(n, t, a, ct, ut, st) -> do + (appOvs, depOvs) <- selectOverrides conn n + dMeta <- selectDeploymentMetadata conn n + pure $ do + let dep = (Deployment n t appOvs depOvs) + DeploymentFullInfo dep (read st) a dMeta ct ut + liftIO . logInfo l $ "get deployments: " <> (pack . show $ deployments) + return deployments + where + qAll = + "SELECT name, tag, archived, extract(epoch from created_at)::int, \ + \extract(epoch from updated_at)::int, status::text \ + \FROM deployments ORDER BY name" + qOne = + "SELECT name, tag, archived, extract(epoch from created_at)::int, \ + \extract(epoch from updated_at)::int, status::text \ + \FROM deployments \ + \WHERE name = ?" + +-- | Handles the 'create' request. +createH :: Deployment -> AppM CommandResponse +createH dep = do + failIfGracefulShutdownActivated + unless (isNameValid $ name dep) $ do + let + badNameText = + "Deployment name length should be longer than 2 characters, \ + \under 17 characters and begin with a letter." + throwError err400 + { errBody = validationError [badNameText] [] } + t1 <- liftIO $ now + st <- ask + let + q = + "INSERT INTO deployments (name, tag, status) \ + \VALUES (?, ?, ?) RETURNING id" + pgPool = pool st + createDep :: PgPool -> Deployment -> IO [Only Int] + createDep p Deployment { name = n, tag = t } = + withResource p $ \conn -> + query conn q (n, t, show CreatePending) + failIfImageNotFound (name dep) (tag dep) + failIfGracefulShutdownActivated + res :: Either SqlError [Only Int] <- liftIO . try $ createDep pgPool dep + dId <- case res of + Right ((Only depId) : _) -> + pure . DeploymentId $ depId + Right [] -> + throwError err404 + { errBody = validationError ["Name not found"] [] } + Left (SqlError code _ _ _ _) | code == unique_violation -> + throwError err400 + { errBody = validationError ["Deployment already exists"] [] } + Left (SqlError _ _ _ _ _) -> + throwError err409 { errBody = appError "Some database error" } + liftIO . withResource pgPool $ \conn -> + upsertNewOverrides conn dId (appOverrides dep) (deploymentOverrides dep) + liftIO . runBgWorker st $ do + sendReloadEvent st + updateDeploymentInfo (name dep) st + (ec, out, err) <- createDeployment dep st + t2 <- now + let + arch = ArchivedFlag False + elTime = elapsedTime t2 t1 + createDeploymentLog pgPool dep "create" ec arch elTime out err + sendReloadEvent st + handleExitCode ec + pure Success + +-- | Updates deployment info. +updateDeploymentInfo :: DeploymentName -> AppState -> IO () +updateDeploymentInfo dName st = do + let + log = logWarning (logger st) + args = infoCommandArgs (projectName st) (baseDomain st) (namespace st) dName + cmd = coerce $ infoCommand st + liftIO $ do + (ec, out, err) <- runCommand (unpack cmd) (coerce args) + case ec of + ExitSuccess -> do + dMeta <- parseDeploymentMetadata (lines . unStdout $ out) + forM_ dMeta $ \meta -> + upsertDeploymentMetadata (pool st) dName meta + ExitFailure _ -> + log $ + "could not get deployment info, exit code: " <> (pack . show $ ec) + <> ", stdout: " <> coerce out <> "stderr: " <> coerce err + +-- | Helper to create a new deployment. +createDeployment :: Deployment -> AppState -> IO (ExitCode, Stdout, Stderr) +createDeployment dep st = do + let + log :: Text -> IO () + log = logInfo (logger st) + args = + [ "--project-name", coerce $ projectName st + , "--base-domain", coerce $ baseDomain st + , "--namespace", coerce $ namespace st + , "--name", coerce $ name dep + , "--tag", coerce $ tag dep + ] ++ applicationOverridesToArgs (appOverrides dep) + ++ deploymentOverridesToArgs (deploymentOverrides dep) + cmd = coerce $ creationCommand st + + liftIO $ do + log $ "call " <> unwords (cmd : args) + (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args) + log $ "deployment created, deployment: " <> (pack . show $ dep) + pure (ec, out, err) + +-- | Converts an application-level override list to command arguments. +applicationOverrideToArg :: ApplicationOverride -> [Text] +applicationOverrideToArg o = ["--app-env-override", overrideToArg . coerce $ o] + +-- | Helper to convert an application-level override to command arguments. +applicationOverridesToArgs :: ApplicationOverrides -> [Text] +applicationOverridesToArgs ovs = concat [applicationOverrideToArg o | o <- ovs ] + +-- | Converts a deployment-level override list to command arguments. +deploymentOverrideToArg :: DeploymentOverride -> [Text] +deploymentOverrideToArg o = + ["--deployment-override", overrideToArg . coerce $ o] + +-- | Helper to convert a deployment-level override to command arguments. +deploymentOverridesToArgs :: DeploymentOverrides -> [Text] +deploymentOverridesToArgs ovs = concat [deploymentOverrideToArg o | o <- ovs] + +-- | Helper to get deployment logs. +selectDeploymentLogs + :: PgPool + -> DeploymentId + -> IO [DeploymentLog] +selectDeploymentLogs pgPool dId = do + let + q = + "SELECT id, action::text, tag, exit_code, \ + \duration, extract(epoch from created_at)::int \ + \FROM deployment_logs \ + \WHERE deployment_id = ? \ + \ORDER BY created_at DESC \ + \LIMIT 20" + withResource pgPool $ \conn -> do + rows <- query conn q (Only . unDeploymentId $ dId) + -- FIXME: use FromRow instance instead + for rows $ \(ai, a, t, ec, d, ts) -> do + (appOvs, depOvs) <- selectLogOverrides conn (ActionId ai) + pure $ DeploymentLog (ActionId ai) a t appOvs depOvs ec (Duration d) ts + +-- | Helper to get a deployment. +selectDeployment + :: PgPool + -> DeploymentName + -> DeploymentListType + -> FilterByPending + -> AppM (Either DeploymentNotFound Deployment) +selectDeployment pgPool dName lType filterByPending = do + let + baseQuery = + "SELECT name, tag, status::text FROM deployments \ + \WHERE name = ?" + q AllDeployments = baseQuery + q ArchivedOnlyDeployments = baseQuery <> " AND archived = 't'" + q ActiveOnlyDeployments = baseQuery <> " AND archived = 'f'" + allowPending = + case filterByPending of + AllowPending -> True + DenyPending -> False + result <- liftIO . withResource pgPool $ \conn -> do + retrieved <- query conn (q lType) (Only dName) + for retrieved $ \(n, t, s) -> do + (appOvs, stOvs) <- selectOverrides conn n + pure $ (Deployment n t appOvs stOvs, read s) + pure $ + case result of + [(_, st)] | isPending st && not allowPending -> + Left $ DeploymentHasPendingStatus st + [(dep, _)] -> + Right dep + _ -> + Left $ + case lType of + AllDeployments -> DeploymentNotFound + ArchivedOnlyDeployments -> ArchivedDeploymentNotFound + ActiveOnlyDeployments -> ActiveDeploymentNotFound + +-- | Handles deployment search result. +unwrapOrValidateError + :: AppM (Either DeploymentNotFound Deployment) + -> AppM Deployment +unwrapOrValidateError deploymentSearchResult = do + deploymentSearchResult' <- deploymentSearchResult + case deploymentSearchResult' of + Right dep -> pure dep + Left DeploymentNotFound -> + throwError err404 + { errBody = validationError ["Deployment not found"] [] } + Left ArchivedDeploymentNotFound -> + throwError err404 + { errBody = validationError ["Archived deployment not found"] [] } + Left ActiveDeploymentNotFound -> + throwError err404 + { errBody = validationError ["Active deployment not found"] [] } + Left (DeploymentHasPendingStatus st) -> + let + err = + "You can not apply this operation \ + \on deployment with \"" <> (pack . show $ st) <> "\" status" + in throwError err405 { errBody = validationError [err] [] } + +-- | Handles the 'archive' request. +archiveH :: DeploymentName -> AppM CommandResponse +archiveH dName = do + failIfGracefulShutdownActivated + t1 <- liftIO $ now + st <- ask + let + log = logInfo (logger st) + pgPool = pool st + args = + [ "--project-name", coerce $ projectName st + , "--base-domain", coerce $ baseDomain st + , "--namespace", coerce $ namespace st + , "--name", coerce dName + ] + cmd = coerce $ archiveCommand st + arch = ArchivedFlag True + dep <- unwrapOrValidateError + $ selectDeployment pgPool dName ActiveOnlyDeployments DenyPending + liftIO . runBgWorker st $ do + log $ "call " <> unwords (cmd : args) + (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args) + void $ archiveDeployment pgPool dName + sendReloadEvent st + t2 <- now + let elTime = elapsedTime t2 t1 + void $ createDeploymentLog pgPool dep "archive" ec arch elTime out err + log $ "deployment archived, name: " <> coerce dName + sendReloadEvent st + handleExitCode ec + pure Success + +-- | Handles the 'archive' request. +archiveDeployment :: PgPool -> DeploymentName -> IO Int64 +archiveDeployment p dName = withResource p $ \conn -> do + let + q = + "UPDATE deployments \ + \SET archived = 't', archived_at = now(), \ + \status = ?, status_updated_at = now() \ + \WHERE name = ?" + execute conn q (show ArchivePending, dName) + +-- | Handles the 'update' request. +updateH :: DeploymentName -> DeploymentUpdate -> AppM CommandResponse +updateH dName dUpdate = do + failIfGracefulShutdownActivated + t1 <- liftIO $ now + st <- ask + let + DeploymentUpdate + { newTag = dTag + , newAppOverrides = newAppOvs + , oldAppOverrides = oldAppOvs + , newDeploymentOverrides = newDepOvs + , oldDeploymentOverrides = oldDepOvs + } = dUpdate + pgPool = pool st + log = logInfo (logger st) + + void $ unwrapOrValidateError + $ selectDeployment pgPool dName ActiveOnlyDeployments DenyPending + dId <- selectDeploymentId pgPool dName + failIfImageNotFound dName dTag + failIfGracefulShutdownActivated + liftIO . runBgWorker st $ do + (appOvs, depOvs) <- withResource pgPool $ \conn -> + withTransaction conn $ do + deleteOldOverrides conn dId oldAppOvs oldDepOvs + upsertNewOverrides conn dId newAppOvs newDepOvs + void $ updateDeployment conn dName dTag + selectOverrides conn dName + updateDeploymentInfo dName st + sendReloadEvent st + let + args = + [ "--project-name", coerce $ projectName st + , "--base-domain", coerce $ baseDomain st + , "--namespace", coerce $ namespace st + , "--name", coerce $ dName + , "--tag", coerce $ dTag + ] ++ applicationOverridesToArgs appOvs + ++ deploymentOverridesToArgs depOvs + cmd = coerce $ updateCommand st + log $ "call " <> unwords (cmd : args) + (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args) + log $ "deployment updated, name: " + <> coerce dName <> ", tag: " <> coerce dTag + void $ do + t2 <- now + let + dep = Deployment dName dTag appOvs depOvs + arch = ArchivedFlag False + elTime = elapsedTime t2 t1 + createDeploymentLog pgPool dep "update" ec arch elTime out err + sendReloadEvent st + handleExitCode ec + return Success + +-- | Helper to get overrides from the database. +selectOverrides + :: Connection + -> DeploymentName + -> IO (ApplicationOverrides, DeploymentOverrides) +selectOverrides conn dName = do + let + q = + "SELECT key, value, scope::text, visibility::text \ + \FROM deployment_overrides \ + \WHERE deployment_id = ( \ + \SELECT id FROM deployments WHERE name = ? \ + \)" + parseVis :: Text -> OverrideVisibility + parseVis = read . unpack + parseScope :: Text -> OverrideScope + parseScope = read . unpack + toOverrides (k, v, s, vis) (appOvs, depOvs) = + pure $ case parseScope s of + ApplicationScope -> + (ApplicationOverride (Override k v $ parseVis vis) : appOvs, depOvs) + DeploymentScope -> + (appOvs, DeploymentOverride (Override k v $ parseVis vis) : depOvs) + rows <- query conn q (Only dName) + foldrM toOverrides ([], []) rows + +-- | Helper to get override logs from the database. +selectLogOverrides + :: Connection + -> ActionId + -> IO (ApplicationOverrides, DeploymentOverrides) +selectLogOverrides conn aId = do + let + q = + "SELECT key, value, scope::text, visibility::text \ + \FROM deployment_log_overrides \ + \WHERE deployment_log_id = ?" + parseVis :: Text -> OverrideVisibility + parseVis = read . unpack + parseScope :: Text -> OverrideScope + parseScope = read . unpack + toOverrides (k, v, s, vis) (appOvs, depOvs) = + pure $ case parseScope s of + ApplicationScope -> + (ApplicationOverride (Override k v $ parseVis vis) : appOvs, depOvs) + DeploymentScope -> + (appOvs, DeploymentOverride (Override k v $ parseVis vis) : depOvs) + rows <- query conn q (Only . unActionId $ aId) + foldrM toOverrides ([], []) rows + +-- | Helper to delete overrides from the database. +deleteOldOverrides + :: Connection + -> DeploymentId + -> ApplicationOverrides + -> DeploymentOverrides + -> IO () +deleteOldOverrides conn dId appOvs depOvs = do + let + q = + "DELETE FROM deployment_overrides \ + \WHERE deployment_id = ? AND key = ? AND scope = ?" + dId' = unDeploymentId dId + void $ for appOvs $ \o -> do + let + oKey = overrideKey . unApplicationOverride $ o + oScope = show ApplicationScope + execute conn q (dId', oKey, oScope) + void $ for depOvs $ \o -> do + let + oKey = overrideKey . unDeploymentOverride $ o + oScope = show DeploymentScope + execute conn q (dId', oKey, oScope) + +-- | Helper to get the deployment id from the database. +selectDeploymentId :: PgPool -> DeploymentName -> AppM DeploymentId +selectDeploymentId pgPool dName = do + dIds :: [(Only Int)] <- liftIO $ withResource pgPool $ \conn -> + query conn "SELECT id FROM deployments WHERE name = ?" (Only dName) + case dIds of + [(Only dId)] -> pure . DeploymentId $ dId + [] -> throwError err404 + { errBody = validationError ["Name not found"] [] } + _ -> throwError err406 + { errBody = validationError ["More than one name found"] [] } + +-- | Helper to insert or update overrides. +upsertNewOverrides + :: Connection + -> DeploymentId + -> ApplicationOverrides + -> DeploymentOverrides + -> IO () +upsertNewOverrides conn dId appOvs depOvs = do + let + q = + "INSERT INTO deployment_overrides \ + \(key, value, deployment_id, scope, visibility) \ + \VALUES (?, ?, ?, ?, ?) \ + \ON CONFLICT (key, deployment_id, scope) \ + \DO \ + \UPDATE SET value = ?, visibility = ?, updated_at = now()" + dId' = unDeploymentId dId + void $ for appOvs $ \o -> do + let + oKey = overrideKey . unApplicationOverride $ o + oValue = overrideValue . unApplicationOverride $ o + oScope = show ApplicationScope + oVis = show . overrideVisibility . unApplicationOverride $ o + execute conn q (oKey, oValue, dId', oScope, oVis, oValue, oVis) + void $ for depOvs $ \o -> do + let + oKey = overrideKey . unDeploymentOverride $ o + oValue = overrideValue . unDeploymentOverride $ o + oScope = show DeploymentScope + oVis = show . overrideVisibility . unDeploymentOverride $ o + execute conn q (oKey, oValue, dId', oScope, oVis, oValue, oVis) + +-- | Helper to update a deployment. +updateDeployment + :: Connection + -> DeploymentName + -> DeploymentTag + -> IO Int64 +updateDeployment conn dName dTag = do + let + q = + "UPDATE deployments \ + \SET tag = ?, updated_at = now(), \ + \status = ?, status_updated_at = now() \ + \WHERE name = ?" + execute conn q (dTag, show UpdatePending, dName) + +-- | Handles the 'info' request of the Web UI API. +infoH :: DeploymentName -> AppM [DeploymentInfo] +infoH dName = do + st <- ask + dInfo <- getInfo dName + liftIO . logInfo (logger st) $ + "get deployment info: " <> (pack . show $ dInfo) + pure [hidePrivateOverridesInInfo dInfo] + +-- | Handles the 'info' request of the octo CLI API. +powerInfoH :: DeploymentName -> AppM [DeploymentInfo] +powerInfoH dName = do + st <- ask + dInfo <- getInfo dName + liftIO . logInfo (logger st) $ + "get deployment info: " <> (pack . show $ dInfo) + pure [dInfo] + +-- | Hides private overrides of 'info' response. +hidePrivateOverridesInInfo :: DeploymentInfo -> DeploymentInfo +hidePrivateOverridesInInfo (DeploymentInfo dep dMeta dLogs) = + let + dep' = + let (Deployment n t ao so) = dep + in Deployment n t (hideP ao) (hideP so) + dLogs' = dLogs <&> \(DeploymentLog ai a t ao so ec d ct) -> + DeploymentLog ai a t (hideP ao) (hideP so) ec d ct + hideP o = coerce o <&> \(Override k v vis) -> + let + v' = case vis of + Private -> "*" + Public -> v + in coerce $ Override k v' vis + in DeploymentInfo dep' dMeta dLogs' + +-- | Helper to get deployment info from the database. +getInfo :: DeploymentName -> AppM DeploymentInfo +getInfo dName = do + st <- ask + let pgPool = pool st + dep <- unwrapOrValidateError + $ selectDeployment pgPool dName AllDeployments AllowPending + dId <- selectDeploymentId pgPool dName + liftIO $ do + depLogs <- selectDeploymentLogs pgPool dId + dMeta <- withResource pgPool $ \conn -> + selectDeploymentMetadata conn dName + let depInfo = DeploymentInfo dep dMeta $ reverse depLogs + pure depInfo + +-- | Handles the 'ping' request. +pingH :: AppM NoContent +pingH = do + pgPool <- pool <$> ask + _ :: [Only Int] <- liftIO $ withResource pgPool $ \conn -> + query_ conn "SELECT 1" + pure NoContent + +-- | Handles the 'project_name' request. +projectNameH :: AppM ProjectName +projectNameH = projectName <$> ask + +-- | Handles the 'status' request. +statusH :: DeploymentName -> AppM CurrentDeploymentStatus +statusH dName = do + st <- ask + let + pgPool = pool st + log = logInfo (logger st) + cmd = checkingCommand st + args = + [ "--namespace", coerce $ namespace st + , "--name", coerce $ dName ] + void $ unwrapOrValidateError + $ selectDeployment pgPool dName AllDeployments AllowPending + liftIO $ log $ "call " <> unwords (coerce cmd : args) + ec <- liftIO $ runCommandWithoutPipes (unpack $ coerce cmd) (unpack <$> args) + pure . CurrentDeploymentStatus $ + case ec of + ExitSuccess -> Ok + _ -> Error + +-- | Handles the 'cleanup' request. +cleanupH :: DeploymentName -> AppM CommandResponse +cleanupH dName = do + failIfGracefulShutdownActivated + st <- ask + void $ unwrapOrValidateError + $ selectDeployment (pool st) dName ArchivedOnlyDeployments DenyPending + liftIO . runBgWorker st $ cleanupDeployment dName st + pure Success + +-- | Helper to cleanup deployment. +cleanupDeployment :: DeploymentName -> AppState -> IO () +cleanupDeployment dName st = do + let + log = logInfo (logger st) + pgPool = pool st + args = + [ "--project-name", coerce $ projectName st + , "--base-domain", coerce $ baseDomain st + , "--namespace", coerce $ namespace st + , "--name", coerce dName + ] + cmd = coerce $ cleanupCommand st + log $ "call " <> unwords (cmd : args) + (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args) + print out >> print err + void $ deleteDeploymentLogOverrides pgPool dName + void $ deleteDeploymentLogs pgPool dName + void $ deleteDeploymentMetadata pgPool dName + void $ deleteDeploymentOverrides pgPool dName + void $ deleteDeployment pgPool dName + log $ "deployment destroyed, name: " <> coerce dName + sendReloadEvent st + handleExitCode ec + +-- | Helper to delete deployment log overrides. +deleteDeploymentLogOverrides :: PgPool -> DeploymentName -> IO Int64 +deleteDeploymentLogOverrides p n = withResource p $ \conn -> execute + conn + "DELETE FROM deployment_log_overrides WHERE deployment_log_id in ( \ + \SELECT id FROM deployment_logs WHERE deployment_id in ( \ + \SELECT id FROM deployments where name = ? \ + \) \ + \)" + (Only n) + +-- | Helper to delete deployment logs. +deleteDeploymentLogs :: PgPool -> DeploymentName -> IO Int64 +deleteDeploymentLogs p n = withResource p $ \conn -> execute + conn + "DELETE FROM deployment_logs WHERE deployment_id in\ + \ (SELECT id FROM deployments where name = ?)" + (Only n) + +-- | Helper to delete deployment overrides. +deleteDeploymentOverrides :: PgPool -> DeploymentName -> IO Int64 +deleteDeploymentOverrides p n = withResource p $ \conn -> execute + conn + "DELETE FROM deployment_overrides WHERE deployment_id in\ + \ (SELECT id FROM deployments where name = ?)" + (Only n) + +-- | Helper to delete a deployment. +deleteDeployment :: PgPool -> DeploymentName -> IO Int64 +deleteDeployment p n = withResource p $ \conn -> + execute conn "DELETE FROM deployments WHERE name = ?" (Only n) + +-- | Handles the 'clean-archive' request. +cleanArchiveH :: AppM CommandResponse +cleanArchiveH = do + failIfGracefulShutdownActivated + st <- ask + let + pgPool = pool st + archRetention = unArchiveRetention . archiveRetention $ st + q = + "SELECT name FROM deployments \ + \WHERE archived = 't' AND archived_at + interval '?' second < now()" + retrieved :: [Only DeploymentName] <- liftIO $ + withResource pgPool $ \conn -> query conn q (Only archRetention) + liftIO . runBgWorker st . void $ + for retrieved $ \(Only dName) -> cleanupDeployment dName st + + pure Success + +-- | Handles the 'restore' request. +restoreH :: DeploymentName -> AppM CommandResponse +restoreH dName = do + failIfGracefulShutdownActivated + t1 <- liftIO $ now + st <- ask + let pgPool = pool st + dep <- unwrapOrValidateError + $ selectDeployment pgPool dName ArchivedOnlyDeployments DenyPending + failIfImageNotFound (name dep) (tag dep) + failIfGracefulShutdownActivated + liftIO . runBgWorker st $ do + updateDeploymentInfo dName st + let + q = + "UPDATE deployments \ + \SET archived = 'f', archived_at = null, status = 'CreatePending' \ + \WHERE name = ?" + void $ withResource pgPool $ \conn -> execute conn q (Only dName) + sendReloadEvent st + (ec, out, err) <- createDeployment dep st + t2 <- now + let + arch = ArchivedFlag False + elTime = elapsedTime t2 t1 + createDeploymentLog pgPool dep "restore" ec arch elTime out err + sendReloadEvent st + handleExitCode ec + pure Success + +-- | Helper to get action info. +getActionInfoH :: ActionId -> AppM ActionInfo +getActionInfoH aId = do + st <- ask + let + pgPool = pool st + aId' = Only . unActionId $ aId + q = "SELECT stdout, stderr FROM deployment_logs WHERE id = ?" + rows :: [(Text, Text)] <- liftIO $ + withResource pgPool $ \conn -> query conn q aId' + case rows of + (out, err) : _ -> pure $ ActionInfo out err + _ -> + throwError err400 { errBody = appError "Action not found" } + +-- | Helper to run command with pipes. +runCommand :: FilePath -> [String] -> IO (ExitCode, Stdout, Stderr) +runCommand cmd args = do + let proc' c a = setStdout createPipe . setStderr createPipe $ proc c a + withProcessWait (proc' cmd args) $ \p -> do + out <- hGetContents . getStdout $ p + err <- hGetContents . getStderr $ p + ec <- waitExitCode p + pure (ec, Stdout out, Stderr err) + +-- | Helper to run command without pipes. +runCommandWithoutPipes :: FilePath -> [String] -> IO ExitCode +runCommandWithoutPipes cmd args = do + withProcessWait (proc cmd args) $ \p -> do + ec <- waitExitCode p + pure ec + +-- | Helper to handle exit code. +handleExitCode :: ExitCode -> IO () +handleExitCode ExitSuccess = return () +handleExitCode (ExitFailure c) = throwIO $ DeploymentFailed c + +-- | Helper to log a deployment action. +createDeploymentLog + :: PgPool + -> Deployment + -> Action + -> ExitCode + -> ArchivedFlag + -> Duration + -> Stdout + -> Stderr + -> IO () +createDeploymentLog pgPool dep act ec arch dur out err = do + let + (Deployment dName dTag appOvs depOvs) = dep + exitCode' = + case ec of + ExitSuccess -> 0 + ExitFailure errCode -> errCode + arch' = unArchivedFlag arch + dur' = unDuration dur + out' = unStdout out + err' = unStderr err + qInsertLog = + "INSERT INTO deployment_logs \ + \(deployment_id, action, tag, exit_code, archived, \ + \duration, stdout, stderr) \ + \(\ + \SELECT id, ?, ?, ?, ?, ?, ?, ? \ + \FROM deployments \ + \WHERE name = ? \ + \) RETURNING id" + qInsertLogOverride = + "INSERT INTO deployment_log_overrides \ + \(key, value, deployment_log_id, scope, visibility) \ + \VALUES (?, ?, ?, ?, ?)" + + void $ withResource pgPool $ \conn -> + withTransaction conn $ do + aIds :: [Only Int] <- query conn qInsertLog + (act, dTag, exitCode', arch', dur', out', err', dName) + void $ for appOvs $ \o -> do + let + [Only aId] = aIds + oKey = overrideKey . unApplicationOverride $ o + oValue = overrideValue . unApplicationOverride $ o + oScope = show ApplicationScope + oVis = show . overrideVisibility . unApplicationOverride $ o + execute conn qInsertLogOverride (oKey, oValue, aId, oScope, oVis) + void $ for depOvs $ \o -> do + let + [Only aId] = aIds + oKey = overrideKey . unDeploymentOverride $ o + oValue = overrideValue . unDeploymentOverride $ o + oScope = show DeploymentScope + oVis = show . overrideVisibility . unDeploymentOverride $ o + execute conn qInsertLogOverride (oKey, oValue, aId, oScope, oVis) + +-- | Helper to get deployment metadata from the database. +selectDeploymentMetadata + :: Connection + -> DeploymentName + -> IO [DeploymentMetadata] +selectDeploymentMetadata conn dName = do + let + q = + "SELECT key, value FROM deployment_metadata \ + \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)" + rows <- query conn q (Only dName) + for rows $ \(k, v) -> pure $ DeploymentMetadata k v + +-- | Helper to delete deployment metadata. +deleteDeploymentMetadata + :: PgPool + -> DeploymentName + -> IO () +deleteDeploymentMetadata pgPool dName = do + let + q = + "DELETE FROM deployment_metadata \ + \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)" + void $ withResource pgPool $ \conn -> execute conn q (Only dName) + +-- | Helper to insert or update deployment metadata. +upsertDeploymentMetadata + :: PgPool + -> DeploymentName + -> DeploymentMetadata + -> IO () +upsertDeploymentMetadata pgPool dName dMetadata = do + let + q = + "INSERT INTO deployment_metadata \ + \(deployment_id, key, value, created_at, updated_at) \ + \(SELECT id, ?, ?, now(), now() FROM deployments WHERE name = ?) \ + \ON CONFLICT (deployment_id, key) \ + \DO \ + \UPDATE SET key = ?, value = ?, updated_at = now()" + key = deploymentMetadataKey dMetadata + value = deploymentMetadataValue dMetadata + void $ withResource pgPool $ \conn -> + execute conn q (key, value, dName, key, value) + +-- | Checks the existence of a deployment tag. +-- Returns 404 'Tag not found' response if the deployment tag doesn't exist. +failIfImageNotFound :: DeploymentName -> DeploymentTag -> AppM () +failIfImageNotFound dName dTag = do + st <- ask + let + log :: Text -> IO () + log = logInfo (logger st) + args = + [ "--project-name", coerce $ projectName st + , "--base-domain", coerce $ baseDomain st + , "--namespace", coerce $ namespace st + , "--name", coerce $ dName + , "--tag", coerce $ dTag + ] + cmd = coerce $ tagCheckingCommand st + + ec <- liftIO $ do + log $ "call " <> unwords (cmd : args) + runCommandWithoutPipes (unpack cmd) (unpack <$> args) + case ec of + ExitSuccess -> pure () + ExitFailure _ -> + throwError err400 { errBody = validationError [] ["Tag not found"] } + +-- | Helper to create an application-level error. +appError :: Text -> BSL.ByteString +appError = encode . AppError + +-- | Helper to create a validation-level error. +validationError :: [Text] -> [Text] -> BSL.ByteString +validationError nameErrors tagErrors = + encode $ ValidationError nameErrors tagErrors + +-- | Helper to send an event to the WS event channel. +sendReloadEvent :: AppState -> IO () +sendReloadEvent state = + atomically $ writeTChan (eventSink state) FrontendPleaseUpdateEverything + +-- | Returns time delta between 2 timestamps. +elapsedTime :: Time -> Time -> Duration +elapsedTime t1 t2 = + Duration . fromIntegral . (`div` 1000000) . abs $ getTime t2 - getTime t1 + +-- | Runs the status updater. +runStatusUpdater :: AppState -> IO () +runStatusUpdater state = do + let + pgPool = pool state + interval = 30 :: Int + selectDeps = + "SELECT name, status::text, \ + \extract(epoch from now())::int - \ + \extract(epoch from status_updated_at)::int \ + \FROM deployments \ + \WHERE checked_at < now() - interval '?' second AND status != 'Archived'" + updateStatus = + "UPDATE deployments \ + \SET status = ?, status_updated_at = now(), checked_at = now() \ + \WHERE name = ? and status = ?" + updateCheckedAt = + "UPDATE deployments SET checked_at = now() WHERE name = ? and status = ?" + + forever $ do + rows :: [(DeploymentName, Text, Int)] <- liftIO $ + withResource pgPool $ \conn -> query conn selectDeps (Only interval) + let + checkList :: [(DeploymentName, DeploymentStatus, Timestamp)] = + (\(n, s, t) -> (n, read . unpack $ s, coerce t)) <$> rows + checkResult <- for checkList $ \(dName, dStatus, ts) -> do + let + args = + [ "--project-name", unpack . coerce $ projectName state + , "--base-domain", unpack . coerce $ baseDomain state + , "--namespace", unpack . coerce $ namespace state + , "--name", unpack . coerce $ dName ] + cmd ArchivePending = unpack . coerce $ archiveCheckingCommand state + cmd _ = unpack . coerce $ checkingCommand state + timeout = statusUpdateTimeout state + ec <- runCommandWithoutPipes (cmd dStatus) args + pure (dName, newStatus ec dStatus ts timeout, ts) + void $ + for (zip checkList checkResult) $ \((dName, oldSt, _), (_, newSt, _)) -> + withResource pgPool $ \conn -> + if oldSt == newSt + then execute conn updateCheckedAt (dName, show oldSt) + else execute conn updateStatus (show newSt, dName, show oldSt) + if checkList == checkResult + then pure () + else sendReloadEvent state + threadDelay 5000000 + +-- | Returns the new deployment status. +newStatus + :: ExitCode + -> DeploymentStatus + -> Timestamp + -> Timeout + -> DeploymentStatus +newStatus ExitSuccess ArchivePending _ _ = Archived +newStatus ExitSuccess _ _ _ = Running +newStatus (ExitFailure _) Running _ _ = Failure +newStatus (ExitFailure _) CreatePending ts timeout | ts > coerce timeout = + Failure +newStatus (ExitFailure _) UpdatePending ts timeout | ts > coerce timeout = + Failure +newStatus (ExitFailure _) ArchivePending _ _ = ArchivePending +newStatus (ExitFailure _) oldStatus _ _ = oldStatus + +-- | Checks if graceful shutdown has been activated activated. +-- Returns 405 'Graceful shutdown activated' response +-- if graceful shutdown has been activated. +failIfGracefulShutdownActivated :: AppM () +failIfGracefulShutdownActivated = do + gracefulShutdownAct <- gracefulShutdownActivated <$> ask + gracefulShutdown <- liftIO . readIORef $ gracefulShutdownAct + if gracefulShutdown + then throwError err405 { errBody = appError "Graceful shutdown activated" } + else pure () + +-- | Handles the graceful shutdown signal. +-- Sends a signal to the 'shutdownSem' semaphore +-- if the background worker counter is 0. +terminationHandler :: IORef Int -> IORef Bool -> MVar () -> IO () +terminationHandler bgWorkersC gracefulShudownAct shutdownS = do + atomicWriteIORef gracefulShudownAct True + c <- readIORef bgWorkersC + if c == 0 + then putMVar shutdownS () + else pure () + +-- | Waits on the 'shutdownSem' semaphore to determine when the +-- server can shut down. +runShutdownHandler :: AppState -> IO () +runShutdownHandler = takeMVar . shutdownSem + +-- | Runs background the worker and increases the background worker counter. +-- Decreases background worker counter +-- after the background worker exits. +-- Sends a signal to 'shutdownSem' semaphore +-- if the background worker counter is 0 +-- and graceful shutdown is activated. +runBgWorker :: AppState -> IO () -> IO () +runBgWorker state act = void $ forkFinally act' cleanup + where + act' = + atomicModifyIORef' (bgWorkersCounter state) (\c -> (c + 1, c)) >> act + cleanup _ = do + c <- atomicModifyIORef' (bgWorkersCounter state) (\c -> (c - 1, c - 1)) + gracefulShutdown <- readIORef (gracefulShutdownActivated state) + if gracefulShutdown && c == 0 + then putMVar (shutdownSem state) () + else pure () diff --git a/octopod-backend/src/Octopod/Server/Args.hs b/octopod-backend/src/Octopod/Server/Args.hs new file mode 100644 index 0000000..19716de --- /dev/null +++ b/octopod-backend/src/Octopod/Server/Args.hs @@ -0,0 +1,72 @@ +{-| +Module : Octopod.Server.Args +Description : Octopod Server arguments parser utils. +-} + +module Octopod.Server.Args where + + +import Data.ByteString (ByteString) +import Data.Coerce +import Options.Generic + + +import Types + + +-- | Octopod Server arguments definition. +data Args = Args + { port :: Int + -- ^ port for octo CLI + , uiPort :: Int + -- ^ port for Web UI (HTTP) + , wsPort :: Int + -- ^ port for Web UI (WS) + , db :: ByteString + -- ^ database connection string + , dbPoolSize :: Int + -- ^ database pool size + , tlsCertPath :: ByteString + -- ^ TLS certificate file path + , tlsKeyPath :: ByteString + -- ^ TLS key file path + , tlsStorePath :: ByteString + -- ^ TLS store path + } deriving (Generic, Show) + +instance ParseRecord Args where + parseRecord = parseRecordWithModifiers lispCaseModifiers + +-- | Parsed Octopod Server arguments definition. +data OctopodOpts = OctopodOpts + { octopodPort :: ServerPort + -- ^ port for Octopod Server + , octopodUIPort :: ServerPort + -- ^ port for UI (HTTP) + , octopodWSPort :: ServerPort + -- ^ port for UI (WS) + , octopodDB :: DBConnectionString + -- ^ database connection string + , octopodDBPoolSize :: DBPoolSize + -- ^ database pool size + , octopodTLSCertPath :: TLSCertPath + -- ^ TLS certificate file path + , octopodTLSKeyPath :: TLSKeyPath + -- ^ TLS key file path + , octopodTLSStorePath :: TLSStorePath + -- ^ TLS store path + } deriving (Show) + +-- | Parses Octopod Server arguments. +parseArgs :: IO OctopodOpts +parseArgs = do + args <- getRecord "Octopod.Server" + pure $ OctopodOpts + (coerce $ port args) + (coerce $ uiPort args) + (coerce $ wsPort args) + (coerce $ db args) + (coerce $ dbPoolSize args) + (coerce $ tlsCertPath args) + (coerce $ tlsKeyPath args) + (coerce $ tlsStorePath args) diff --git a/octopod-backend/src/Octopod/Server/ControlScriptUtils.hs b/octopod-backend/src/Octopod/Server/ControlScriptUtils.hs new file mode 100644 index 0000000..548fb44 --- /dev/null +++ b/octopod-backend/src/Octopod/Server/ControlScriptUtils.hs @@ -0,0 +1,31 @@ +{-| +Module : Octopod.Server.ControlScriptUtils +Description : Control script utils. + +This module contains control script utils. +-} + + +module Octopod.Server.ControlScriptUtils where + + +import Data.Coerce +import Data.Text + + +import Types + + +-- | Creates command arguments for the 'info' deployment control script. +infoCommandArgs + :: ProjectName + -> Domain + -> Namespace + -> DeploymentName + -> ControlScriptArgs +infoCommandArgs pName domain ns dName = + ControlScriptArgs $ + [ "--project-name", unpack . coerce $ pName + , "--base-domain", unpack . coerce $ domain + , "--namespace", unpack . coerce $ ns + , "--name", unpack . coerce $ dName ] diff --git a/octopod-backend/src/Octopod/Server/Logger.hs b/octopod-backend/src/Octopod/Server/Logger.hs new file mode 100644 index 0000000..8ce66d1 --- /dev/null +++ b/octopod-backend/src/Octopod/Server/Logger.hs @@ -0,0 +1,34 @@ +{-| +Module : Octopod.Server.Logger +Description : Octopod Server logger. +-} + + +module Octopod.Server.Logger where + +import Data.ByteString +import Data.Text +import System.Log.FastLogger + +-- | Creates new logger. +newLogger :: IO TimedFastLogger +newLogger = do + timeCache <- newTimeCache "%Y-%m-%d %T%z" + (logger, _) <- newTimedFastLogger timeCache (LogStdout defaultBufSize) + pure logger + +-- | Logs a message with the 'INFO' severity. +logInfo :: TimedFastLogger -> Text -> IO () +logInfo l = logWithSeverity l "INFO" + +-- | Logs a message with the 'WARN' severity. +logWarning :: TimedFastLogger -> Text -> IO () +logWarning l = logWithSeverity l "WARN" + +-- | Logs a message with the specified severity. +logWithSeverity :: ToLogStr msg => TimedFastLogger -> ByteString -> msg -> IO () +logWithSeverity l severity msg = l $ \ft -> metadata ft <> message + where + metadata :: ByteString -> LogStr + metadata ft = foldMap toLogStr ["[", ft, " ", severity, "] "] + message = toLogStr msg <> toLogStr ("\n" :: ByteString) diff --git a/octopod-backend/src/Octopod/Server/Posix.hs b/octopod-backend/src/Octopod/Server/Posix.hs new file mode 100644 index 0000000..6a9d8d6 --- /dev/null +++ b/octopod-backend/src/Octopod/Server/Posix.hs @@ -0,0 +1,25 @@ +module Octopod.Server.Posix (installShutdownHandler) where + + +import Control.Monad +import Control.Monad.Base (liftBase) +import Data.Text +import System.Log.FastLogger +import System.Posix.Signals + + +import Octopod.Server.Logger + + +-- | Installs the given shutdown handler for the specified signals. +installShutdownHandler + :: TimedFastLogger + -> [Signal] + -> IO () + -> IO [Handler] +installShutdownHandler logger signals action = + forM signals $ \signal -> installHandler signal (handler signal) Nothing + where + handler signal = Catch $ do + logInfo logger $ "Shutdown initiated by signal " <> (pack . show $ signal) + liftBase action diff --git a/octopod-backend/src/Orphans.hs b/octopod-backend/src/Orphans.hs new file mode 100644 index 0000000..fedebfd --- /dev/null +++ b/octopod-backend/src/Orphans.hs @@ -0,0 +1,16 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Orphans where + +import Database.PostgreSQL.Simple.FromField (FromField) +import Database.PostgreSQL.Simple.ToField (ToField) + +import Common.Types + +deriving instance FromField Action +deriving instance ToField Action + +deriving instance FromField DeploymentTag +deriving instance ToField DeploymentTag + +deriving instance FromField DeploymentName +deriving instance ToField DeploymentName diff --git a/octopod-backend/src/TLS.hs b/octopod-backend/src/TLS.hs new file mode 100644 index 0000000..dc6a374 --- /dev/null +++ b/octopod-backend/src/TLS.hs @@ -0,0 +1,68 @@ +{-| +Module : TLS +Description : TLS utils. + +This module contains TLS utils. +-} + + +module TLS where + + +import qualified Data.ByteString.Char8 as BSC +import Data.Default.Class (def) +import Data.X509 (ExtKeyUsagePurpose(..), HashALG(HashSHA256)) +import Data.X509.CertificateStore (readCertificateStore) +import Data.X509.Validation + (ValidationChecks(..), ValidationHooks(..), checkLeafV3, validate) +import Network.TLS + (CertificateRejectReason(..), CertificateUsage(..), onClientCertificate) +import Network.Wai.Handler.WarpTLS hiding (TLSSettings, tlsSettings) +import qualified Network.Wai.Handler.WarpTLS as WTLS + + +import Types + + +-- | Creates TLS options for the HTTP server +createTLSOpts + :: TLSCertPath + -> TLSKeyPath + -> TLSStorePath + -> ServerPort + -> WTLS.TLSSettings +createTLSOpts tlsCert tlsKey tlsStore serverPort = + initTLSSettings + { tlsWantClientCert = True + , onInsecure = DenyInsecure + "This server only accepts secure HTTPS connections." + , tlsServerHooks = def { onClientCertificate = + fmap certificateUsageFromValidations . validateCertificate } + } + where + tlsCert' = unTLSCertPath tlsCert + tlsKey' = unTLSKeyPath tlsKey + tlsStore' = BSC.unpack . unTLSStorePath $ tlsStore + serverPort' = unServerPort serverPort + initTLSSettings = WTLS.tlsSettings + (BSC.unpack tlsCert') (BSC.unpack tlsKey') + certificateUsageFromValidations = maybe CertificateUsageAccept + (CertificateUsageReject . CertificateRejectOther) + serviceID = ("localhost", BSC.pack $ show serverPort') + validationHooks = def + { hookValidateName = \_ _ -> [] } + validationChecks = def + { checkStrictOrdering = True + , checkLeafKeyPurpose = [KeyUsagePurpose_ClientAuth] + , checkLeafV3 = False + } + validateCertificate cert = do + mstore <- readCertificateStore tlsStore' + maybe + (pure . Just $ "Cannot init a store") + (fmap fromX509FailedReasons . (\store -> validate HashSHA256 + validationHooks validationChecks store def serviceID cert)) + mstore + fromX509FailedReasons reasons = case reasons of + [] -> Nothing + _ -> Just (show reasons) diff --git a/octopod-backend/src/Types.hs b/octopod-backend/src/Types.hs new file mode 100644 index 0000000..6ccaf60 --- /dev/null +++ b/octopod-backend/src/Types.hs @@ -0,0 +1,83 @@ +{-| +Module : Types +Description : Backend types. + +This module contains backend types. +-} + + +module Types + ( module Common.Types + , parseDeploymentMetadata + , ServerPort(..) + , DBPoolSize(..) + , DBConnectionString(..) + , TLSStorePath(..) + , ProjectName(..) + , Domain(..) + , Namespace(..) + , Command(..) + , ArchiveRetention(..) + , Timeout(..) + , ControlScriptArgs(..)) where + +import Data.Bifunctor +import Data.ByteString (ByteString) +import Data.Text as T +import Data.Traversable + +import Common.Types + +-- | Parses deployment metadata. +parseDeploymentMetadata :: [Text] -> IO [DeploymentMetadata] +parseDeploymentMetadata texts = + for texts $ \t -> + case T.findIndex (== ',') t of + Just i -> do + let + (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t + pure $ DeploymentMetadata key value + Nothing -> error $ + "Malformed metadata key-value pair " <> T.unpack t <> + ", should be similar to foo,bar" + +-- | Server port. +newtype ServerPort = ServerPort { unServerPort :: Int } + deriving (Show) + +-- | Database pool size. +newtype DBPoolSize = DBPoolSize { unDBPoolSize :: Int } + deriving (Show) + +-- | Database connection string. +newtype DBConnectionString = DbConnectionString + { unDBConnectionString :: ByteString } + deriving (Show) + +-- | TLS store path. +newtype TLSStorePath = TLSStore { unTLSStorePath :: ByteString } + deriving (Show) + +newtype Domain = Domain { unDomain :: Text } + deriving (Show) + +-- | A Kubernetes namespace. +newtype Namespace = Namespace { unNamespace :: Text } + deriving (Show) + +-- | Archive retention. +newtype ArchiveRetention = ArchiveRetention { unArchiveRetention :: Int } + deriving (Show) + +-- | Timeout. +newtype Timeout = Timeout { unTimeout :: Int } + deriving (Show) + +-- | Path to a deployment control script. +newtype Command = Command { unCommand :: Text } + deriving (Show) + +-- | Control script arguments. +newtype ControlScriptArgs = ControlScriptArgs + { unControlScriptArgs :: [String] } + deriving (Show) diff --git a/octopod-backend/test/Spec.hs b/octopod-backend/test/Spec.hs new file mode 100644 index 0000000..cd4753f --- /dev/null +++ b/octopod-backend/test/Spec.hs @@ -0,0 +1,2 @@ +main :: IO () +main = putStrLn "Test suite not yet implemented" diff --git a/octopod-common/CHANGELOG.md b/octopod-common/CHANGELOG.md new file mode 100644 index 0000000..47b7089 --- /dev/null +++ b/octopod-common/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for common + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/octopod-common/LICENSE b/octopod-common/LICENSE new file mode 100644 index 0000000..2094028 --- /dev/null +++ b/octopod-common/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2020, Typeable LLC + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Typeable LLC nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/octopod-common/Setup.hs b/octopod-common/Setup.hs new file mode 100644 index 0000000..4467109 --- /dev/null +++ b/octopod-common/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/octopod-common/octopod-common.cabal b/octopod-common/octopod-common.cabal new file mode 100644 index 0000000..3c8ad05 --- /dev/null +++ b/octopod-common/octopod-common.cabal @@ -0,0 +1,48 @@ +-- Initial common.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/ + +name: octopod-common +version: 0.1.0.0 +-- synopsis: +-- description: +license: BSD3 +license-file: LICENSE +author: Typeable +maintainer: octopod@typeable.io +-- category: +build-type: Simple +extra-source-files: CHANGELOG.md +cabal-version: >=1.10 + +library + exposed-modules: Common.Types + , Common.Utils + , Common.Validation + -- other-modules: + -- other-extensions: + default-extensions: BlockArguments + , ConstraintKinds + , DataKinds + , DeriveFunctor + , DeriveGeneric + , DerivingVia + , DuplicateRecordFields + , FlexibleContexts + , GeneralizedNewtypeDeriving + , LambdaCase + , OverloadedStrings + , ScopedTypeVariables + , TypeApplications + , TypeOperators + build-depends: base >=4.12 && <4.13 + , aeson + , bytestring + , deriving-aeson + , generic-lens + , http-api-data + , lens + , regex-tdfa + , servant + , text + hs-source-dirs: src + default-language: Haskell2010 diff --git a/octopod-common/src/Common/Types.hs b/octopod-common/src/Common/Types.hs new file mode 100644 index 0000000..46051ab --- /dev/null +++ b/octopod-common/src/Common/Types.hs @@ -0,0 +1,272 @@ +{-| +Module : Common.Types +Description : Common types for backend and frontend. + +This module contains common types between the backend and the frontend. +-} + + +module Common.Types where + +import Data.Bifunctor +import Data.ByteString (ByteString) +import Data.Coerce +import Data.String +import Data.Text as T +import Data.Traversable +import Deriving.Aeson.Stock +import Web.HttpApiData + +-- | Deployment override. +data Override = Override + { overrideKey :: Text + , overrideValue :: Text + , overrideVisibility :: OverrideVisibility + } + deriving (Generic, Show, Eq) + deriving (FromJSON, ToJSON) via Snake Override + +type Overrides = [Override] + +-- | Deployment override scope. +data OverrideScope + = ApplicationScope + | DeploymentScope + deriving (Generic, Show, Read, Eq) + deriving (FromJSON, ToJSON) via Snake OverrideScope + +-- | Deployment override visibility. +data OverrideVisibility + = Private + | Public + deriving (Generic, Show, Read, Eq) + deriving (FromJSON, ToJSON) via Snake OverrideVisibility + +-- | Deployment application-level override. +newtype ApplicationOverride = + ApplicationOverride { unApplicationOverride :: Override } + deriving (Show, Eq, FromJSON, ToJSON) + +-- | Deployment application-level overrides. +type ApplicationOverrides = [ApplicationOverride] + +-- | Deployment-level override. +newtype DeploymentOverride = DeploymentOverride + { unDeploymentOverride :: Override } + deriving (Show, Eq, FromJSON, ToJSON) + +-- | Deployment-level overrides. +type DeploymentOverrides = [DeploymentOverride] + +newtype DeploymentId = DeploymentId { unDeploymentId :: Int } + deriving (Show) + +newtype DeploymentName = DeploymentName { unDeploymentName :: Text } + deriving + (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq, Ord) + +newtype DeploymentTag = DeploymentTag { unDeploymentTag :: Text } + deriving + (Show, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq) + +newtype Action = Action { unAction :: Text } + deriving (Show, FromJSON, ToJSON, IsString) + +newtype ArchivedFlag = ArchivedFlag { unArchivedFlag :: Bool } + deriving (Show, FromJSON, ToJSON) + +newtype Duration = Duration { unDuration :: Int } + deriving (Show, FromJSON, ToJSON) + +newtype Timestamp = Timestamp { unTimestamp :: Int } + deriving (Show, Eq, Ord, FromJSON, ToJSON) + +newtype ProjectName = ProjectName { uProjectName :: Text } + deriving (Show, FromJSON, ToJSON) + +data DeploymentStatus + = Running + | Failure + | CreatePending + | UpdatePending + | ArchivePending + | Archived + deriving (Generic, Read, Show, Eq) + deriving (FromJSON, ToJSON) via Snake DeploymentStatus + +data Deployment = Deployment + { name :: DeploymentName + , tag :: DeploymentTag + , appOverrides :: ApplicationOverrides + , deploymentOverrides :: DeploymentOverrides + } + deriving (Generic, Show, Eq) + deriving (FromJSON, ToJSON) via Snake Deployment + +data DeploymentLog = DeploymentLog + { actionId :: ActionId + , action :: Action + , deploymentTag :: DeploymentTag + , deploymentAppOverrides :: ApplicationOverrides + , deploymentDepOverrides :: DeploymentOverrides + , exitCode :: Int + , duration :: Duration + , createdAt :: Int + } + deriving (Generic, Show) + deriving (FromJSON, ToJSON) via Snake DeploymentLog + +data DeploymentMetadata = DeploymentMetadata + { deploymentMetadataKey :: Text + , deploymentMetadataValue :: Text + } + deriving (Generic, Show, Eq) + deriving (FromJSON, ToJSON) via Snake DeploymentMetadata + +data DeploymentInfo = DeploymentInfo + { deployment :: Deployment + , metadata :: [DeploymentMetadata] + , logs :: [DeploymentLog] + } + deriving (Generic, Show) + deriving (FromJSON, ToJSON) via Snake DeploymentInfo + +data DeploymentFullInfo = DeploymentFullInfo + { deployment :: Deployment + , status :: DeploymentStatus + , archived :: Bool + , metadata :: [DeploymentMetadata] + , createdAt :: Int + , updatedAt :: Int + } + deriving (Generic, Show, Eq) + deriving (FromJSON, ToJSON) via Snake DeploymentFullInfo + +data DeploymentUpdate = DeploymentUpdate + { newTag :: DeploymentTag + , newAppOverrides :: ApplicationOverrides + , oldAppOverrides :: ApplicationOverrides + , newDeploymentOverrides :: DeploymentOverrides + , oldDeploymentOverrides :: DeploymentOverrides + } + deriving (Generic, Show) + deriving (FromJSON, ToJSON) via Snake DeploymentUpdate + +data CurrentStatus + = Ok + | Error + deriving (Generic, Show, Eq) + deriving (FromJSON, ToJSON) via Snake CurrentStatus + +newtype CurrentDeploymentStatus = + CurrentDeploymentStatus { status :: CurrentStatus } + deriving (Generic, Show, Eq) + deriving (FromJSON, ToJSON) via Snake CurrentDeploymentStatus + +data CommandResponse + = Success + | ValidationError + { nameField :: [Text] + , tagField :: [Text] + } + | AppError + { errorMessage :: Text } + deriving (Generic, Show, Eq) + deriving (FromJSON, ToJSON) via Snake CommandResponse + +data WSEvent = FrontendPleaseUpdateEverything + deriving (Generic, Show) + deriving (FromJSON, ToJSON) via Snake WSEvent + +newtype ActionId = ActionId { unActionId :: Int } + deriving + (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq) + +newtype Stdout = Stdout { unStdout :: Text } + deriving (Generic, Show) + deriving (FromJSON, ToJSON) via Snake Stdout + +newtype Stderr = Stderr { unStderr :: Text } + deriving (Generic, Show) + deriving (FromJSON, ToJSON) via Snake Stderr + +data ActionInfo = ActionInfo + { stdout :: Text + , stderr :: Text + } + deriving (Generic, Show) + deriving (FromJSON, ToJSON) via Snake ActionInfo + +-- | TLS certificate path. +newtype TLSCertPath = TLSCertPath { unTLSCertPath :: ByteString } + deriving (Show) + +-- | TLS key path. +newtype TLSKeyPath = TLSKeyPath { unTLSKeyPath :: ByteString } + deriving (Show) + +-- | Parses setting application-level overrides. +parseSetApplicationOverrides + :: OverrideVisibility + -> [Text] + -> IO [ApplicationOverride] +parseSetApplicationOverrides visibility texts = + coerce <$> parseSetOverrides visibility texts + +-- | Parses setting deployment-level overrides. +parseSetDeploymentOverrides + :: OverrideVisibility + -> [Text] + -> IO [DeploymentOverride] +parseSetDeploymentOverrides visibility texts = + coerce <$> parseSetOverrides visibility texts + +-- | Parses setting overrides. +parseSetOverrides :: OverrideVisibility -> [Text] -> IO [Override] +parseSetOverrides visibility texts = + for texts $ \t -> + case T.findIndex (== '=') t of + Just i -> do + let + (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t + pure $ Override key value visibility + Nothing -> error $ + "Malformed override key-value pair " <> T.unpack t <> + ", should be similar to FOO=bar" + +-- | Parses unsetting application-level overrides. +parseUnsetApplicationOverrides + :: OverrideVisibility + -> [Text] + -> IO [ApplicationOverride] +parseUnsetApplicationOverrides visibility texts = + coerce <$> parseUnsetOverrides visibility texts + +-- | Parses unsetting deployment-level overrides. +parseUnsetDeploymentOverrides + :: OverrideVisibility + -> [Text] + -> IO [DeploymentOverride] +parseUnsetDeploymentOverrides visibility texts = + coerce <$> parseUnsetOverrides visibility texts + +-- | Parses unsetting overrides. +parseUnsetOverrides :: OverrideVisibility -> [Text] -> IO [Override] +parseUnsetOverrides visibility texts = + for texts $ \key -> + pure $ Override key "" visibility + +-- | Creates pretty-printed text from override. +formatOverride :: Override -> Text +formatOverride o@(Override _ _ vis) = + overrideToArg o <> case vis of + Private -> " (" <> pack (show vis) <> ")" + Public -> mempty + +-- | Creates pretty-printed texts from overrides. +formatOverrides :: Overrides -> Text +formatOverrides = T.unlines . fmap formatOverride + +-- | Creates a CLI argument from an override. +overrideToArg :: Override -> Text +overrideToArg (Override k v _) = k <> "=" <> v diff --git a/octopod-common/src/Common/Utils.hs b/octopod-common/src/Common/Utils.hs new file mode 100644 index 0000000..cfc5ac8 --- /dev/null +++ b/octopod-common/src/Common/Utils.hs @@ -0,0 +1,41 @@ +{-| +Module : Common.Utils +Description : Common utils for backend and frontend. + +This module contains common utils between the backend and the frontend. +-} + + +module Common.Utils where + +import Control.Lens +import Data.Generics.Product +import Data.Monoid + +import Common.Types + +-- Lens convenience helpers +(<^.>) :: Functor f => f a -> Getting b a b -> f b +(<^.>) fa l = fa <&> (^. l) + +(<^?>) :: Functor f => f a -> Getting (First b) a b -> f (Maybe b) +(<^?>) fa l = fa <&> (^? l) + +(<^..>) :: Functor f => f a -> Getting (Endo [b]) a b -> f [b] +(<^..>) fa t = fa <&> (^.. t) + +infixl 8 <^.>, <^..>, <^?> + +-- | Gets name from deployment full info. +dfiName :: Getter DeploymentFullInfo DeploymentName +dfiName = field @"deployment" . field @"name" + +-- | Checks that deployment status is pending. +isPending :: DeploymentStatus -> Bool +isPending = \case + Running -> False + Failure -> False + Archived -> False + CreatePending -> True + UpdatePending -> True + ArchivePending -> True diff --git a/octopod-common/src/Common/Validation.hs b/octopod-common/src/Common/Validation.hs new file mode 100644 index 0000000..e369a72 --- /dev/null +++ b/octopod-common/src/Common/Validation.hs @@ -0,0 +1,20 @@ +{-| +Module : Common.Validation +Description : Common validations for backend and frontend. + +This module contains common validations between the backend and the frontend. +-} + + +module Common.Validation where + +import Data.ByteString (ByteString) +import Data.Text.Encoding as T (encodeUtf8) +import Text.Regex.TDFA + +import Common.Types + +-- | Validates a deployment name. +isNameValid :: DeploymentName -> Bool +isNameValid (DeploymentName name) = + (T.encodeUtf8 name =~ ("^[a-z][a-z0-9\\-]{1,16}$" :: ByteString)) diff --git a/octopod-css/.stylelintrc.json b/octopod-css/.stylelintrc.json new file mode 100755 index 0000000..2682366 --- /dev/null +++ b/octopod-css/.stylelintrc.json @@ -0,0 +1,98 @@ +{ + "rules": { + "at-rule-name-case": "lower", + "at-rule-name-space-after": "always-single-line", + "at-rule-semicolon-newline-after": "always", + "block-closing-brace-empty-line-before": "never", + "block-closing-brace-newline-after": "always", + "block-closing-brace-newline-before": "always-multi-line", + "block-closing-brace-space-before": "always-single-line", + "block-opening-brace-newline-after": "always-multi-line", + "block-opening-brace-space-before": "always", + "block-opening-brace-space-after": "always-single-line", + "color-hex-case": "upper", + "color-hex-length": "long", + "color-no-invalid-hex": true, + "comment-no-empty": true, + "declaration-bang-space-after": "never", + "declaration-bang-space-before": "always", + "declaration-block-no-duplicate-properties": true, + "declaration-block-no-redundant-longhand-properties": true, + "declaration-block-no-shorthand-property-overrides": true, + "declaration-block-semicolon-newline-after": "always-multi-line", + "declaration-block-semicolon-space-after": "always-single-line", + "declaration-block-semicolon-space-before": "never", + "declaration-block-trailing-semicolon": "always", + "declaration-colon-space-before": "never", + "declaration-colon-space-after": "always-single-line", + "declaration-empty-line-before": [ "always", { + "except": [ + "after-declaration", + "first-nested" + ], + "ignore": [ + "after-comment", + "inside-single-line-block" + ] + } ], + "font-family-name-quotes": "always-unless-keyword", + "function-calc-no-unspaced-operator": true, + "function-comma-newline-after": "always-multi-line", + "function-comma-space-after": "always", + "function-comma-space-before": "never", + "function-linear-gradient-no-nonstandard-direction": true, + "function-max-empty-lines": 0, + "function-name-case": "lower", + "function-parentheses-newline-inside": "always-multi-line", + "function-parentheses-space-inside": "never-single-line", + "function-whitespace-after": "always", + "function-url-quotes": "always", + "keyframe-declaration-no-important": true, + "length-zero-no-unit": true, + "media-feature-colon-space-after": "always", + "media-feature-colon-space-before": "never", + "media-feature-name-case": "lower", + "media-feature-name-no-unknown": true, + "media-feature-parentheses-space-inside": "never", + "media-feature-range-operator-space-after": "always", + "media-feature-range-operator-space-before": "always", + "media-query-list-comma-newline-after": "always-multi-line", + "media-query-list-comma-space-after": "always-single-line", + "media-query-list-comma-space-before": "never", + "no-empty-source": true, + "no-eol-whitespace": true, + "no-extra-semicolons": true, + "no-invalid-double-slash-comments": true, + "number-leading-zero": "always", + "number-no-trailing-zeros": true, + "property-case": "lower", + "property-no-unknown": true, + "selector-attribute-brackets-space-inside": "never", + "selector-attribute-operator-space-after": "never", + "selector-attribute-operator-space-before": "never", + "selector-attribute-quotes": "always", + "selector-combinator-space-after": "always", + "selector-combinator-space-before": "always", + "selector-descendant-combinator-no-non-space": true, + "selector-list-comma-newline-after": "always", + "selector-list-comma-space-before": "never", + "selector-max-empty-lines": 0, + "selector-pseudo-class-case": "lower", + "selector-pseudo-class-no-unknown": true, + "selector-pseudo-class-parentheses-space-inside": "never", + "selector-pseudo-element-case": "lower", + "selector-pseudo-element-colon-notation": "double", + "selector-pseudo-element-no-unknown": true, + "selector-type-case": "lower", + "selector-type-no-unknown": true, + "shorthand-property-no-redundant-values": true, + "string-quotes": "double", + "string-no-newline": true, + "unit-case": "lower", + "unit-no-unknown": true, + "value-list-comma-newline-after": "always-multi-line", + "value-list-comma-space-after": "always-single-line", + "value-list-comma-space-before": "never", + "value-list-max-empty-lines": 0 + } +} diff --git a/octopod-css/development/images/dialog-delete.png b/octopod-css/development/images/dialog-delete.png new file mode 100644 index 0000000..fbfd43b Binary files /dev/null and b/octopod-css/development/images/dialog-delete.png differ diff --git a/octopod-css/development/images/dialog-delete@2x.png b/octopod-css/development/images/dialog-delete@2x.png new file mode 100644 index 0000000..daabfab Binary files /dev/null and b/octopod-css/development/images/dialog-delete@2x.png differ diff --git a/octopod-css/development/images/null-data.png b/octopod-css/development/images/null-data.png new file mode 100644 index 0000000..ec50af9 Binary files /dev/null and b/octopod-css/development/images/null-data.png differ diff --git a/octopod-css/development/images/null-data@2x.png b/octopod-css/development/images/null-data@2x.png new file mode 100644 index 0000000..34bd582 Binary files /dev/null and b/octopod-css/development/images/null-data@2x.png differ diff --git a/octopod-css/development/images/null-search.png b/octopod-css/development/images/null-search.png new file mode 100644 index 0000000..ea42d1f Binary files /dev/null and b/octopod-css/development/images/null-search.png differ diff --git a/octopod-css/development/images/null-search@2x.png b/octopod-css/development/images/null-search@2x.png new file mode 100644 index 0000000..022eec2 Binary files /dev/null and b/octopod-css/development/images/null-search@2x.png differ diff --git a/octopod-css/development/layouts/index.html b/octopod-css/development/layouts/index.html new file mode 100755 index 0000000..421705e --- /dev/null +++ b/octopod-css/development/layouts/index.html @@ -0,0 +1,885 @@ + + + + + + Index + + + + + + + + +
+
+ +
+ Project name +
+
+
+ +
+
+
+ System error. Сan not move the deployment to the archive right now. Please try later. + +
+
+

+ All deployments +

+
+ Updated 5 mins ago +
+ + + New deployment + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Links + + Tag + + App overrides + + + + + + + + Menu +
+ + +     или     + +
+ Cannot retrieve the data +
Try to reload page
+
+ +     или     + +
+ Loading... +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Feature name +
+ Archived +
+
+ ... + + fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b + +
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+ 15.05.2020 + + 01.06.2020 + +
+ +
+ +
+
+
+
+
+
+ +
+
+
+ + + + + + + + +
+
+
+
+ +
+
+ Are you sure you want to archive the +
‘Feature name’ deployment? +
+ +
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/layouts/staging.html b/octopod-css/development/layouts/staging.html new file mode 100755 index 0000000..84c64da --- /dev/null +++ b/octopod-css/development/layouts/staging.html @@ -0,0 +1,710 @@ + + + + + + Deployment + + + + + + + + +
+
+ +
+ Project name +
+
+
+ +
+
+
+ System error. Сan not move the deployment to the archive right now. Please try later. + +
+ + All deployments + +
+

+ Project name +

+ + Edit deployment + + + Move to archive + + +    или для уже архивных: + + + Recover from archive + +
+
+
+
+ Project successfully updated + +
+
+
+ + Status + +
+
Success
+
+
+
+ + Created + +
+ 15.05.2020 +
+
+
+ + Changed + +
+ 01.06.2020 +
+
+
+
+

+ Tag +

+
+ H97HF384GKERP39740223 +
+
+
+

+ Links +

+ +
+
+

+ App overrides +

+
+
+
+ ENVIRONENT: production +
+
+ app_api_url: http: //my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij +
+
+
+
+
+

+ Deployment overrides +

+
+
+
+ ENVIRONENT: production +
+
+ app_api_url: http: //my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij +
+
+
+
+
+

+ Actions +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Action type + + Image tag + + App overrides + + Deployment overrides + + Exit code + + + + Deployment duration +
+ Create + + fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b + +
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+ +
+
+
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+ +
+
+ 0 + + 2020-05-13 08:06:00 313811+00 + + 1m 95s +
+ Edit + + fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b + +
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+ 1 + + 2020-05-13 08:06:00 313811+00 + + 1m 95s +
+ Update + + fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b + +
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+ 0 + + 2020-05-13 08:06:00 313811+00 + + 1m 95s +
+ Delete + + fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b + +
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+
+
+ 1 + + 2020-05-13 08:06:00 313811+00 + + 1m 95s +
+
+ Cannot retrieve the data +
Try to reload page
+
+ +     или     + +
+ Loading... +
+
+
+
+
+
+ + или: + +
+
+
+ Cannot retrieve the data +
Try to reload page
+
+
+ или: +
+ Loading... +
+
+
+
+
+ + + + + + + + +
+
+
+
+ +
+
+ Are you sure you want to archive the +
‘Feature name’ deployment? +
+ +
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/layouts/standalone-loading.html b/octopod-css/development/layouts/standalone-loading.html new file mode 100755 index 0000000..0f1094b --- /dev/null +++ b/octopod-css/development/layouts/standalone-loading.html @@ -0,0 +1,43 @@ + + +
+ + + + Loading... +
diff --git a/octopod-css/development/markups/_action.html b/octopod-css/development/markups/_action.html new file mode 100755 index 0000000..f304024 --- /dev/null +++ b/octopod-css/development/markups/_action.html @@ -0,0 +1,109 @@ + + + + + + .action + + + + + + + + +
+
+
+
+
+

.action

+ + + + + +
+
+
+
+

.action--archive

+ + + + + +
+
+
+
+

.action--edit

+ + + + + +
+
+
+
+

.action--restore

+ + + + + + +
+
+
+
+

.action--disabled

+

+ Если экшн делается тегом <button> то хорошо бы добавлять атрибут disabled: +

+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_bar.html b/octopod-css/development/markups/_bar.html new file mode 100755 index 0000000..344d43e --- /dev/null +++ b/octopod-css/development/markups/_bar.html @@ -0,0 +1,58 @@ + + + + + + .bar + + + + + + + + +
+
+
+
+
+

.bar

+ + +
+ ENVIRONENT: production +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_break.html b/octopod-css/development/markups/_break.html new file mode 100755 index 0000000..d04d1af --- /dev/null +++ b/octopod-css/development/markups/_break.html @@ -0,0 +1,82 @@ + + + + + + .break + + + + + + + + + +
+
+
+

.break

+

+ Простой, вспомогательный класс, который переводит режим переносов. + По дефолту текст переносится там где пробелы, табы и любые текстовые разделители. + С этим классом переносится в любом месте. Т.е. слова печатаются до упора + и обрыв на новую строчку может проихойти после любой буквы. +

+
+
+
+ +
+ DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/libiserv/dist-install/build:/Users/dr/workspace/ghc/libraries/ghci/dist-install/build:/Users/dr/workspace/ghc/libraries/transformers/dist-install/build:/Users/dr/workspace/ghc/libraries/template-haskell/dist-install/build:/Users/dr/workspace/ghc/libraries/pretty/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-heap/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-boot/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-boot-th/dist-install/build:/Users/dr/workspace/ghc/libraries/directory/dist-install/build:/Users/dr/workspace/ghc/libraries/unix/dist-install/build:/Users/dr/workspace/ghc/libraries/time/dist-install/build:/Users/dr/workspace/ghc/libraries/filepath/dist-install/build:/Users/dr/workspace/ghc/libraries/binary/dist-install/build:/Users/dr/workspace/ghc/libraries/containers/containers/dist-install/build:/Users/dr/workspace/ghc/libraries/bytestring/dist-install/build:/Users/dr/workspace/ghc/libraries/deepseq/dist-install/build:/Users/dr/workspace/ghc/libraries/array/dist-install/build:/Users/dr/workspace/ghc/libraries/base/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-bignum/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-prim/dist-install/build:/Users/dr/workspace/ghc/rts/dist/build${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"' >> inplace/lib/bin/ghc-iserv-dyn +
+ +
+
+
+

+ Можно завертывать в <div class="break">, как в примере выше. + Можно просто добавлять класс .break куда угодно. Например к ячейке таблицы, + вот так вот: <td class="break">. +

+

+ Не работает с <pre>. Там скроллбар. Можно вешать этот класс на обычный <div>, + и давать ему моноширный шрифт. В нашем случае это: +
font-family: "Roboto Mono", monospace;. + И добавляем <br> перед первой стокой. + Таким образом будет получаться аналог <pre>. +

+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_button.html b/octopod-css/development/markups/_button.html new file mode 100755 index 0000000..04b299b --- /dev/null +++ b/octopod-css/development/markups/_button.html @@ -0,0 +1,134 @@ + + + + + + .button + + + + + + + + + +
+
+
+

.button

+ + + +
+
+
+
+

.button--secondary

+ + + + +
+
+
+
+

.button--add

+ + + + + +
+
+
+
+

.button--save

+ + + + +
+
+
+
+

.button--disabled

+

+ Хорошо бы еще добавить атрибут disabled +

+ + + + + +
+
+ + + + + + + + +
+
+
+
+

Теги

+ +
div.button
+ +

+ + span.button + +

+ + + +

+ + a.button + +

+ + + +

+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_dash.html b/octopod-css/development/markups/_dash.html new file mode 100755 index 0000000..85b6ce2 --- /dev/null +++ b/octopod-css/development/markups/_dash.html @@ -0,0 +1,127 @@ + + + + + + .dash + + + + + + + + + +
+
+
+

.dash

+ +
+
+
+
+

.dash--add

+ + + +
+
+
+
+

.dash--add

+ + + + + +
+
+
+
+

.dash--smaller

+ + + +
+
+
+
+

.dash--disabled

+

+ Хорошо бы добавлять атрибут disabled +

+ + + + +
+
+
+
+

Теги

+ +
Add an override
+ +

+ + Add an override + +

+ + + +

+ + Add an override + +

+ + + +

+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_drop.html b/octopod-css/development/markups/_drop.html new file mode 100755 index 0000000..41887ab --- /dev/null +++ b/octopod-css/development/markups/_drop.html @@ -0,0 +1,109 @@ + + + + + + .drop + + + + + + + + +
+
+
+
+
+

.drop

+ + +

+ Общий компонент для выпадаек что по клику. + Они могут отличаться внешне, но жаба-скрипт везде будет один. +

+

+ Смотрим базовую версию: +

+
+
+ +
+ +
+ dropdown +
+
+ + +
+
+
+
+

+ Дальше либо натягиваем скины, либо просто смиксовываем с другими блоками. +

+ + +
+
+
+
+

.drop--actions

+
+ + +
+ +
+ + +
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_expander.html b/octopod-css/development/markups/_expander.html new file mode 100755 index 0000000..d2626b1 --- /dev/null +++ b/octopod-css/development/markups/_expander.html @@ -0,0 +1,93 @@ + + + + + + .expander + + + + + + + + +
+
+
+
+
+

.expander

+ + + + + +
+
+
+
+

.expander--stand-alone

+

+ Название странное, так исторически сложилось +

+
+ + + +
+
+
+
+

.expander--open

+

+ Для расхлопывания добавляем .expander--open: +

+ + + +
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_external.html b/octopod-css/development/markups/_external.html new file mode 100755 index 0000000..5f29db9 --- /dev/null +++ b/octopod-css/development/markups/_external.html @@ -0,0 +1,58 @@ + + + + + + .external + + + + + + + + +
+
+
+
+
+

.external

+ + + + app.feature-name.project.name.com lorem ipsume dolor sit amen + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_input.html b/octopod-css/development/markups/_input.html new file mode 100755 index 0000000..4681464 --- /dev/null +++ b/octopod-css/development/markups/_input.html @@ -0,0 +1,108 @@ + + + + + + .input + + + + + + + + + +
+
+
+

.input

+ +
+ +
+ + +
+
+
+
+

Клеартайп

+

+ Здесь помимо самого клеартайпа еще нужно добавить модификатор + .input--has-clear-type -- он переиграет отступы и другие версточные штуки + (например выключит дефолтный ИЕшный клеартайп) +

+ +
+ + +
+ +
+
+
+
+

.input--search

+

+ Просто иконка и паддинг +

+ + + + +
+
+
+
+

.input--error и .input__output

+

+ Первое -- модификатор, что делает сам инпут красным. +
Второе -- просто элемент для вывода ошибки. +

+ +
+ + +
Error message
+
Another message
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_listing.html b/octopod-css/development/markups/_listing.html new file mode 100755 index 0000000..af395ec --- /dev/null +++ b/octopod-css/development/markups/_listing.html @@ -0,0 +1,97 @@ + + + + + + .listing + + + + + + + + +
+
+
+
+
+

.listing

+

+ Простая обертка выстраивающая элементы типа .bar + вертикально. Плюс небольшой отступ. +

+ + + + + +
+
+
+

.listing--for-text

+

+ В какой-то момент появилась аналогичная версия, + но уже без .bar а просто с голым текстом внутри. + Пришлось сюда переносить некоторые свойства типа бОльших отступов, + обрезания текста многоточием и т.д. Все это модификатор .listing--for-text: +

+
+
+ +
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_loading.html b/octopod-css/development/markups/_loading.html new file mode 100755 index 0000000..3dee2ba --- /dev/null +++ b/octopod-css/development/markups/_loading.html @@ -0,0 +1,133 @@ + + + + + + .loading + + + + + + + + +
+
+
+
+
+

.loading

+

+ Дефолтный кейс нигде не используется, но он подводят нас к другим нужным нам кейсам, + и, возможно, когда-нибудь подгоним его под нужный вариант: +

+ +
+ Loading... +
+ +
+
+
+
+

.loading--status-alike

+

+ Для использования в кейсах симметричных к .status +

+ +
+ Loading... +
+ +
+
+
+
+

.loading--alternate

+

+ Тоже нигде не используется, но здесь мы учимся делать индикатор синим +

+ +
+ Loading... +
+ + + +
+
+
+
+

.loading--enlarged

+

+ Большой и серый в чистом виде тоже нигде не встречался +

+ +
+ Loading... +
+ +
+
+

+ А вот этот уже используется на странице загрузки: +

+ +
+ Loading... +
+ + + + +
+
+
+
+

.loading--indicator-only

+

+ Бонус если вдург понадобится где-то в чистом виде: +

+ +
+ Loading... +
+ +
+ +
+ Loading... +
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_notification.html b/octopod-css/development/markups/_notification.html new file mode 100755 index 0000000..024d64e --- /dev/null +++ b/octopod-css/development/markups/_notification.html @@ -0,0 +1,85 @@ + + + + + + .notification + + + + + + + + +
+
+
+
+
+

.notification

+ +

+ Дефолтного кейса пока не было: +

+ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et eros justo. Vivamus scelerisque est sed congue aliquet. Pellentesque vitae sodales lectus. Mauris dignissim id tellus lacinia finibus. Sed egestas sapien urna, at rutrum diam mattis vitae. Duis a tempor enim. Curabitur at facilisis felis. Vestibulum imperdiet arcu a justo cursus mollis eu eget ipsum. + +
+ + +
+
+
+
+

.notification--success

+ +
+ Success. The deployment has moved to the archive + +
+ +
+
+
+
+

.notification--danger

+ +
+ System error. Сan not move the deployment to the archive right now. Please try later. + +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_null.html b/octopod-css/development/markups/_null.html new file mode 100755 index 0000000..bbeb8aa --- /dev/null +++ b/octopod-css/development/markups/_null.html @@ -0,0 +1,78 @@ + + + + + + .null + + + + + + + + +
+
+
+
+
+

.null

+ +
+
+
+
+

.null--data

+ +
+ Cannot retrieve the data +
Try to reload page
+
+ + + +
+
+
+
+

.null--search

+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_spot.html b/octopod-css/development/markups/_spot.html new file mode 100755 index 0000000..c321a3d --- /dev/null +++ b/octopod-css/development/markups/_spot.html @@ -0,0 +1,58 @@ + + + + + + .spot + + + + + + + + +
+
+
+
+
+

.spot

+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_status.html b/octopod-css/development/markups/_status.html new file mode 100755 index 0000000..a5b246c --- /dev/null +++ b/octopod-css/development/markups/_status.html @@ -0,0 +1,80 @@ + + + + + + .status + + + + + + + + +
+
+
+
+
+

.status

+ + +
+ Success +
+ +
+
+ +
+ Pending +
+ +
+
+ +
+ Failure +
+ +
+
+ +
+ Archived +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/markups/_title.html b/octopod-css/development/markups/_title.html new file mode 100755 index 0000000..8fa8a16 --- /dev/null +++ b/octopod-css/development/markups/_title.html @@ -0,0 +1,57 @@ + + + + + + .title + + + + + + + + +
+
+
+
+
+

.title

+ + + +

All deployments

+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/development/scripts/_classic-popup.js b/octopod-css/development/scripts/_classic-popup.js new file mode 100755 index 0000000..0f3e8b5 --- /dev/null +++ b/octopod-css/development/scripts/_classic-popup.js @@ -0,0 +1,92 @@ +(function($) { + $(function() { + + + + /* + * Page lock + */ + + var documentWidthWithScroll = 0; + var documentWidthWithoutScroll = 0; + var scrollWidth = 0; + + var $html = $('html'); + + + function lockPage() { + if ( ! $html.hasClass('html-lock') ) { + documentWidthWithScroll = $(window).width(); + $html.addClass('html-lock'); + documentWidthWithoutScroll = $(window).width(); + $html.css( 'padding-right', (documentWidthWithoutScroll - documentWidthWithScroll) + 'px' ); + } + } + + function unlockPage() { + if ( $html.hasClass('html-lock') ) { + $html.css( 'padding-right', '' ); + $html.removeClass('html-lock'); + } + } + + + + /* + * Popup + */ + + function popupShow(popup){ + lockPage(); + popup.fadeIn(150); + popup.scrollTop(0); + } + + function popupHide(popup){ + if( ! popup ) { //in case of Esc or something + popup = $('.classic-popup'); + } + + popup.fadeOut(150,function(){ //hide popup THAN unlock page + unlockPage(); + }); + } + + /* show popup by handler click */ + + $('.classic-popup-handler').on('click', function(event) { + event.preventDefault(); + var popupId = $(this).attr('href') ? $(this).attr('href') : $(this).attr('data-href') + popupShow( $(popupId) ); + event.stopPropagation(); + }); + + + /* hide popup by window close click */ + + $('.classic-popup__close').on('click', function(event){ + event.preventDefault(); + popupHide( $(this).parents('.classic-popup') ); + }); + + + /* hide popup by overlay click ( goo.gl/SJG2Hw ) */ + + $('.classic-popup').on('click', function(event) { + if (!$(event.target).closest('.classic-popup__slot').length) { + popupHide( $('.classic-popup') ); + } + }); + + + /* hide popup by Esc press */ + + $(document).on('keyup', function(event) { + if (event.keyCode == 27) { + popupHide(); + } + }); + + }); +})(jQuery); + diff --git a/octopod-css/development/scripts/_data.js b/octopod-css/development/scripts/_data.js new file mode 100755 index 0000000..7f5b3a6 --- /dev/null +++ b/octopod-css/development/scripts/_data.js @@ -0,0 +1,7 @@ +(function($) { + + $('.data__show-archive').on('click', function () { + $('.data__archive').toggleClass('data__archive--open'); + }); + +})(jQuery); diff --git a/octopod-css/development/scripts/_drop.js b/octopod-css/development/scripts/_drop.js new file mode 100755 index 0000000..d11a6ee --- /dev/null +++ b/octopod-css/development/scripts/_drop.js @@ -0,0 +1,26 @@ +(function($) { + + $('.drop__handler').on('click', function (event) { + $(this).parents('.drop').toggleClass('drop--expanded'); + event.stopPropagation(); + }); + + + /* Закрываем по клику вне ( goo.gl/SJG2Hw ) */ + + $(document).on('click', function(event) { + if (!$(event.target).closest('.drop').length) { + $('.drop--expanded').removeClass('drop--expanded'); + } + }); + + + /* Закрываем по Esc */ + + $(document).on('keyup', function(event) { + if (event.keyCode === 27) { + $('.drop--expanded').removeClass('drop--expanded'); + } + }); + +})(jQuery); diff --git a/octopod-css/development/scripts/_expander.js b/octopod-css/development/scripts/_expander.js new file mode 100755 index 0000000..e5d4c7c --- /dev/null +++ b/octopod-css/development/scripts/_expander.js @@ -0,0 +1,8 @@ +(function($) { + + $('.expander').on('click', function (event) { + $(this).toggleClass('expander--open'); + event.stopPropagation(); + }); + +})(jQuery); diff --git a/octopod-css/development/scripts/_popup.js b/octopod-css/development/scripts/_popup.js new file mode 100755 index 0000000..9ed0ec4 --- /dev/null +++ b/octopod-css/development/scripts/_popup.js @@ -0,0 +1,35 @@ +(function($) { + + $('.popup-handler').on('click', function (event) { + event.preventDefault(); + var popupId = $(this).attr('href') ? $(this).attr('href') : $(this).attr('data-href') + $( popupId ).addClass('popup--visible'); + event.stopPropagation(); + }); + + + /* Закрываем по крестику */ + + $('.popup__close').on('click', function(event){ + event.preventDefault(); + $(this).parents('.popup').removeClass('popup--visible'); + }); + + + /* Закрываем по клику вне ( goo.gl/SJG2Hw ) */ + + $('.popup__overlay').on('click', function(event) { + $('.popup--visible').removeClass('popup--visible'); + }); + + + /* Закрываем по Esc */ + + $(document).on('keyup', function(event) { + if (event.keyCode === 27) { + $('.popup--visible').removeClass('popup--visible'); + } + }); + + +})(jQuery); diff --git a/octopod-css/development/scripts/_sort.js b/octopod-css/development/scripts/_sort.js new file mode 100755 index 0000000..8407bd7 --- /dev/null +++ b/octopod-css/development/scripts/_sort.js @@ -0,0 +1,16 @@ +(function($) { + + $('.sort').on('click', function () { + if( ! $(this).hasClass('sort--active') ) { + $('.sort--active').removeClass('sort--active').removeClass('sort--desc').removeClass('sort--asc'); + $(this).addClass('sort--active').addClass('sort--desc'); + } else { + if( $(this).hasClass('sort--asc') ) { + $(this).removeClass('sort--asc').addClass('sort--desc'); + } else if( $(this).hasClass('sort--desc') ) { + $(this).removeClass('sort--desc').addClass('sort--asc'); + } + } + }); + +})(jQuery); diff --git a/octopod-css/development/styles/_action.css b/octopod-css/development/styles/_action.css new file mode 100755 index 0000000..e356cfa --- /dev/null +++ b/octopod-css/development/styles/_action.css @@ -0,0 +1,55 @@ +.action { + display: inline-block; + vertical-align: top; + width: 100%; + border: none; + padding: 16px 32px 16px 16px; + background-color: transparent; + background-repeat: no-repeat; + background-position: 16px center; + font-family: inherit; + font-size: 14px; + line-height: 16px; + color: #000000; + white-space: nowrap; + text-align: left; + text-decoration: none; + cursor: pointer; +} + +.action:not(.action--disabled):hover { + background-color: var(--grey); +} + +.action:not(.action--disabled):active { + background-color: var(--grey-active); +} + + + + +.action--archive { + padding-left: 50px; + background-image: url("../vectors/common-use-archive.svg"); +} + +.action--edit { + padding-left: 50px; + background-image: url("../vectors/common-use-edit.svg"); +} + +.action--restore { + padding-left: 50px; + background-image: url("../vectors/common-use-restore.svg"); +} + +.action--logs { + padding-left: 50px; + background-image: url("../vectors/common-use-logs.svg"); +} + + +.action--disabled { + opacity: 0.5; + cursor: default; +} diff --git a/octopod-css/development/styles/_bar.css b/octopod-css/development/styles/_bar.css new file mode 100755 index 0000000..0a6a200 --- /dev/null +++ b/octopod-css/development/styles/_bar.css @@ -0,0 +1,24 @@ +.bar { + display: inline-block; + vertical-align: top; + padding: 4px 6px; + background-color: #EEEEEE; + border-radius: 4px; + font-size: 12px; + line-height: 16px; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .bar b { + font-weight: 500; + } + + + +.bar--larger { + padding: 4px 8px; + font-size: 16px; + line-height: 24px; +} diff --git a/octopod-css/development/styles/_body.css b/octopod-css/development/styles/_body.css new file mode 100755 index 0000000..5109339 --- /dev/null +++ b/octopod-css/development/styles/_body.css @@ -0,0 +1,5 @@ +body { + height: 100%; + background-color: #FFFFFF; + font-family: "Roboto", sans-serif; +} diff --git a/octopod-css/development/styles/_break.css b/octopod-css/development/styles/_break.css new file mode 100755 index 0000000..7780f45 --- /dev/null +++ b/octopod-css/development/styles/_break.css @@ -0,0 +1,3 @@ +.break { + word-break: break-all; +} diff --git a/octopod-css/development/styles/_browserupgrade.css b/octopod-css/development/styles/_browserupgrade.css new file mode 100755 index 0000000..ca6eb03 --- /dev/null +++ b/octopod-css/development/styles/_browserupgrade.css @@ -0,0 +1,5 @@ +.browserupgrade { + background: #CCCCCC; + color: #000000; + padding: 4px 10px; +} \ No newline at end of file diff --git a/octopod-css/development/styles/_button.css b/octopod-css/development/styles/_button.css new file mode 100755 index 0000000..996de56 --- /dev/null +++ b/octopod-css/development/styles/_button.css @@ -0,0 +1,101 @@ +.button { + display: inline-block; + vertical-align: top; + height: 40px; + padding: 11px; + border: 1px solid transparent; + background-color: var(--primary); + border-radius: 4px; + font-family: inherit; + font-weight: 500; + font-size: 14px; + line-height: 16px; + letter-spacing: 0.6px; + color: #FFFFFF; + text-align: center; + text-transform: uppercase; + text-decoration: none; + white-space: nowrap; + cursor: pointer; +} + +.button:hover { + background-color: var(--primary-hover); +} + +.button:active { + background-color: var(--primary-active); +} + +.button--disabled, +.button--disabled:hover, +.button--disabled:active { + background-color: var(--primary-disabled); + cursor: default; +} + + +.button--secondary { + background-color: #FFFFFF; + border-color: var(--dark-grey); + color: #000000; +} + +.button--secondary:hover { + background-color: #EEEEEE; +} + +.button--secondary:active { + background-color: #E2E2E2; +} + + +.button--secondary.button--disabled, +.button--secondary.button--disabled:hover, +.button--secondary.button--disabled:active { + background-color: transparent; + opacity: 0.5; + cursor: default; +} + + + +.button--add { + padding-left: 40px; + background-image: url("../vectors/button-add.svg"); + background-size: 24px 24px; + background-position: 11px center; + background-repeat: no-repeat; +} + +.button--edit { + padding-left: 40px; + background-image: url("../vectors/common-use-edit-white.svg"); + background-size: 24px 24px; + background-position: 11px center; + background-repeat: no-repeat; +} + +.button--archive { + padding-left: 40px; + background-image: url("../vectors/common-use-archive.svg"); + background-size: 24px 24px; + background-position: 11px center; + background-repeat: no-repeat; +} + +.button--restore { + padding-left: 40px; + background-image: url("../vectors/common-use-restore.svg"); + background-size: 24px 24px; + background-position: 11px center; + background-repeat: no-repeat; +} + +.button--save { + padding-left: 40px; + background-image: url("../vectors/button-save.svg"); + background-size: 24px 24px; + background-position: 11px center; + background-repeat: no-repeat; +} diff --git a/octopod-css/development/styles/_classic-popup.css b/octopod-css/development/styles/_classic-popup.css new file mode 100755 index 0000000..0b4ad26 --- /dev/null +++ b/octopod-css/development/styles/_classic-popup.css @@ -0,0 +1,57 @@ +.html-lock { + overflow: hidden; +} + +.classic-popup { + position: fixed; + z-index: 800; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background: rgba(0, 0, 0, 0.5); + display: none; +} + + .classic-popup__container { + display: table; + table-layout: fixed; + width: 100%; + height: 100%; + } + + .classic-popup__viewport { + display: table-cell; + width: 100%; + height: 100%; + text-align: center; + vertical-align: middle; + } + + .classic-popup__slot { + position: relative; + display: inline-block; + vertical-align: top; + text-align: left; + margin: 20px; + } + + .classic-popup__close { + position: absolute; + right: 16px; + top: 16px; + width: 40px; + height: 40px; + background-color: transparent; + background-image: url("../vectors/classic-popup-close.svg"); + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center center; + padding: 0; + border: none; + text-indent: 200%; + white-space: nowrap; + overflow: hidden; + cursor: pointer; + } diff --git a/octopod-css/development/styles/_container.css b/octopod-css/development/styles/_container.css new file mode 100755 index 0000000..4bdfce0 --- /dev/null +++ b/octopod-css/development/styles/_container.css @@ -0,0 +1,4 @@ +.container { + padding-left: 24px; + padding-right: 24px; +} diff --git a/octopod-css/development/styles/_dash.css b/octopod-css/development/styles/_dash.css new file mode 100755 index 0000000..72aeab5 --- /dev/null +++ b/octopod-css/development/styles/_dash.css @@ -0,0 +1,57 @@ +.dash { + padding: 12px 0 12px 24px; + background-color: transparent; + background-size: 16px 16px; + background-repeat: no-repeat; + background-position: left center; + border: none; + font-family: inherit; + font-weight: 500; + font-size: 14px; + line-height: 16px; + letter-spacing: 0.4px; + text-decoration: none; + text-transform: uppercase; + color: var(--primary); + cursor: pointer; +} + +.dash:hover { + color: var(--primary-hover); +} + +.dash:active { + color: var(--primary-active); +} + + +.dash--smaller { + font-size: 12px; + padding-left: 20px; +} + +/* stylelint-disable block-opening-brace-space-before */ +.dash--add { background-image: url("../vectors/dash-add.svg"); } +.dash--add:not(.dash--disabled):hover { background-image: url("../vectors/dash-add-hover.svg"); } +.dash--add:not(.dash--disabled):active { background-image: url("../vectors/dash-add-active.svg"); } + +.dash--back { background-image: url("../vectors/dash-back.svg"); } +.dash--back:not(.dash--disabled):hover { background-image: url("../vectors/dash-back-hover.svg"); } +.dash--back:not(.dash--disabled):active { background-image: url("../vectors/dash-back-active.svg"); } + +.dash--next { + padding-left: 0; + padding-right: 20px; + background-position: right center; +} + +.dash--next { background-image: url("../vectors/dash-next.svg"); } +.dash--next:not(.dash--disabled):hover { background-image: url("../vectors/dash-next-hover.svg"); } +.dash--next:not(.dash--disabled):active { background-image: url("../vectors/dash-next-active.svg"); } +/* stylelint-enable */ + + +.dash--disabled { + opacity: 0.5; + cursor: default; +} diff --git a/octopod-css/development/styles/_data.css b/octopod-css/development/styles/_data.css new file mode 100755 index 0000000..8473ae5 --- /dev/null +++ b/octopod-css/development/styles/_data.css @@ -0,0 +1,19 @@ +.data { + flex-shrink: 0; +} + .data__primary { + + } + + .data__show-archive { + margin-top: 18px; + align-self: flex-start; + } + + .data__archive { + display: none; + } + + .data__archive--open { + display: block; + } diff --git a/octopod-css/development/styles/_deployment.css b/octopod-css/development/styles/_deployment.css new file mode 100755 index 0000000..d27ef05 --- /dev/null +++ b/octopod-css/development/styles/_deployment.css @@ -0,0 +1,59 @@ +.deployment { + padding: 24px; +} + +.page__body .deployment { + padding: 0; +} + + .deployment__output { + margin-bottom: 32px; + } + + .deployment__summary { + display: flex; + align-self: flex-start; + } + .deployment__stat { + min-width: 120px; + margin-left: 32px; + } + + .deployment__stat:first-child { + margin-left: 0; + } + .deployment__param { + display: block; + margin-top: 0; + margin-bottom: 0; + font-weight: 500; + font-size: 14px; + line-height: 16px; + } + + .deployment__value { + margin-top: 8px; + font-size: 16px; + line-height: 24px; + } + .deployment__value .status { + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + } + + .deployment__section { + margin-top: 32px; + } + .deployment__sub-heading { + margin-top: 0; + margin-bottom: 0; + font-weight: 500; + font-size: 14px; + line-height: 16px; + } + + .deployment__widget { + margin-top: 16px; + font-size: 16px; + line-height: 24px; + } diff --git a/octopod-css/development/styles/_dialog.css b/octopod-css/development/styles/_dialog.css new file mode 100755 index 0000000..7dab3e9 --- /dev/null +++ b/octopod-css/development/styles/_dialog.css @@ -0,0 +1,47 @@ +.dialog { + width: 400px; + padding: 40px 24px; + background-color: #FFFFFF; + border-radius: 16px; +} + +.dialog--archive::before { + content: ""; + display: block; + width: 185px; + height: 120px; + margin-left: auto; + margin-right: auto; + background-image: url("../images/dialog-delete.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center center; +} + +@media (min-resolution: 2dppx) { + + .dialog--archive::before { + background-image: url("../images/dialog-delete@2x.png"); + } +} + .dialog__content { + margin-top: 25px; + font-weight: bold; + font-size: 16px; + line-height: 20px; + text-align: center; + } + + .dialog__footer { + margin-top: 16px; + display: flex; + align-items: flex-start; + justify-content: center; + } + .dialog__action { + margin-left: 8px; + } + + .dialog__action:first-child { + margin-left: 0; + } diff --git a/octopod-css/development/styles/_drop.css b/octopod-css/development/styles/_drop.css new file mode 100755 index 0000000..fd6d31c --- /dev/null +++ b/octopod-css/development/styles/_drop.css @@ -0,0 +1,70 @@ +.drop { + position: relative; + display: inline-block; + vertical-align: top; +} + .drop__handler { + border: none; + padding: 0; + background-color: transparent; + cursor: pointer; + } + + .drop__dropdown { + position: absolute; + z-index: 500; + right: 0; + top: 100%; + min-width: 100%; + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-basic), + visibility var(--transition-basic); + } + + + +.drop--expanded { + +} + .drop--expanded .drop__dropdown { + opacity: 1; + visibility: visible; + } + + + + +.drop--actions { + +} + .drop--actions .drop__handler { + width: 40px; + height: 40px; + background-image: url("../vectors/drop-actions.svg"); + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center center; + text-indent: 200%; + white-space: nowrap; + overflow: hidden; + } + + .drop--actions .drop__handler:hover { + background-image: url("../vectors/drop-actions-hover.svg"); + } + + .drop--actions .drop__handler:active, + .drop--actions.drop--expanded .drop__handler { + background-image: url("../vectors/drop-actions-active.svg"); + } + + .drop--actions .drop__dropdown { + margin-top: 8px; + margin-right: -17px; + padding-top: 8px; + padding-bottom: 8px; + background-color: #FFFFFF; + box-shadow: var(--shadow_1); + border-radius: 2px; + } diff --git a/octopod-css/development/styles/_expander.css b/octopod-css/development/styles/_expander.css new file mode 100755 index 0000000..2bd62a2 --- /dev/null +++ b/octopod-css/development/styles/_expander.css @@ -0,0 +1,90 @@ +.expander { /* дефолтная версия расчитана на использование с .bar */ + position: relative; + border: none; + background-color: transparent; + padding: 0 17px 0 0; + font-family: inherit; + font-weight: 500; + font-size: 12px; + line-height: 16px; + color: var(--primary); + text-decoration: none; + cursor: pointer; +} + /* Увеличиваем область кликабельности */ + .expander::before { + content: ""; + position: absolute; + left: -4px; + right: -4px; + top: -10px; + bottom: -10px; + display: block; + } + + /* Стрелочка */ + .expander::after { + position: absolute; + right: 0; + top: 0; + content: ""; + display: block; + width: 16px; + height: 16px; + background-image: url("../vectors/expander.svg"); + background-repeat: no-repeat; + background-position: center center; + } + + +.expander:hover { + color: var(--primary-hover); +} + +.expander:active { + color: var(--primary-active); +} + + +.expander--open { + +} + .expander--open::after { + transform: scaleY(-1); + } + + +.expander--stand-alone { + padding: 9px 24px 9px 0; + background-color: transparent; + font-family: inherit; + font-size: 14px; + line-height: 16px; + color: var(--primary); + text-transform: uppercase; + letter-spacing: 0.6px; +} + .expander--stand-alone::after { + right: 0; + top: 7px; + width: 20px; + height: 20px; + background-image: url("../vectors/expander-larger.svg"); + background-size: contain; + } + +.expander--stand-alone:hover { + background-color: transparent; + color: var(--primary-hover); +} + .expander--stand-alone:hover::after { + background-image: url("../vectors/expander-larger-hover.svg"); + } + +.expander--stand-alone:active { + background-color: transparent; + color: var(--primary-active); +} + .expander--stand-alone:active::after { + background-image: url("../vectors/expander-larger-active.svg"); + } diff --git a/octopod-css/development/styles/_external.css b/octopod-css/development/styles/_external.css new file mode 100755 index 0000000..1d38c54 --- /dev/null +++ b/octopod-css/development/styles/_external.css @@ -0,0 +1,26 @@ +.external { + padding-right: 24px; + background-image: url("../vectors/external.svg"); + background-size: 12px 12px; + background-repeat: no-repeat; + background-position: right 6px center; + color: inherit; + text-decoration: none; + cursor: pointer; +} + +.external:hover { + color: var(--primary); + background-color: #E2E2E2; + background-image: url("../vectors/external-hover-active.svg"); +} + +.external:active { + color: var(--primary); + background-color: #D6D6D6; + background-image: url("../vectors/external-hover-active.svg"); +} + +.external.bar--larger { + background-position: right 8px center; +} diff --git a/octopod-css/development/styles/_header.css b/octopod-css/development/styles/_header.css new file mode 100755 index 0000000..bd59a9d --- /dev/null +++ b/octopod-css/development/styles/_header.css @@ -0,0 +1,31 @@ +.header { + padding-top: 24px; + padding-bottom: 24px; + background-color: #3671E3; + box-shadow: var(--shadow_1); + color: #FFFFFF; +} + .header__wrap { + display: flex; + align-items: center; + } + .header__logo { + margin-top: 0; + margin-bottom: 0; + border-right: 1px solid #709AEE; + padding-right: 15px; + font-weight: bold; + font-size: 20px; + line-height: 24px; + letter-spacing: 0.85px; + text-transform: uppercase; + } + + .header__project { + margin-left: -1px; + border-left: 1px solid #709AEE; + padding-left: 16px; + font-weight: 500; + font-size: 16px; + line-height: 24px; + } diff --git a/octopod-css/development/styles/_html.css b/octopod-css/development/styles/_html.css new file mode 100755 index 0000000..e2ea82e --- /dev/null +++ b/octopod-css/development/styles/_html.css @@ -0,0 +1,4 @@ +html { + height: 100%; + min-width: 1000px; +} diff --git a/octopod-css/development/styles/_input.css b/octopod-css/development/styles/_input.css new file mode 100755 index 0000000..2c31346 --- /dev/null +++ b/octopod-css/development/styles/_input.css @@ -0,0 +1,91 @@ +.input { + position: relative; +} + .input__widget { + width: 100%; + height: 40px; + vertical-align: top; + padding: 8px 0; + background-color: transparent; + border: none; + outline: none; + box-shadow: inset 0 -1px #B0B0B0; + background-clip: padding-box; /* reset iOS default shadow */ + border-radius: 0; /* reset iOS default border-radius */ + font-family: inherit; + font-size: 16px; + line-height: 24px; + color: rgba(0, 0, 0, 0.87); + } + + .input__widget:hover { + box-shadow: inset 0 -1px #326EE6; + } + + .input__widget:focus { + box-shadow: inset 0 -2px #326EE6; + } + + /* stylelint-disable block-opening-brace-space-before */ + .input__widget::-webkit-input-placeholder { font-style: normal; color: var(--dark-grey); } + .input__widget::-moz-placeholder { font-style: normal; color: var(--dark-grey); opacity: 1; } + .input__widget:-ms-input-placeholder { font-style: normal; color: var(--dark-grey); } + + .input__widget:hover::-webkit-input-placeholder { color: #B1B1B1; } + .input__widget:hover::-moz-placeholder { color: #B1B1B1; } + .input__widget:hover:-ms-input-placeholder { color: #B1B1B1; } + + .input__widget:focus::-webkit-input-placeholder { color: #B1B1B1; } + .input__widget:focus::-moz-placeholder { color: #B1B1B1; } + .input__widget:focus:-ms-input-placeholder { color: #B1B1B1; } + /* stylelint-enable */ + + + +.input--has-clear-type { + +} + .input--has-clear-type .input__widget { + padding-right: 30px; + } + + .input--has-clear-type .input__widget::-ms-clear { + display: none; + } + + .input__clear-type { + position: absolute; + right: -8px; + top: 0; + } + + +.input--search { + +} + .input--search .input__widget { + padding-left: 32px; + background-image: url("../vectors/input-search.svg"); + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: left center; + } + + +.input--error { + +} + .input--error .input__widget { + box-shadow: inset 0 -2px var(--red); + } + + .input__output { + margin-top: 7px; + font-size: 12px; + line-height: 16px; + color: var(--red); + } + + .input__output + .input__output { + margin-top: 3px; + } diff --git a/octopod-css/development/styles/_listing.css b/octopod-css/development/styles/_listing.css new file mode 100755 index 0000000..4b65197 --- /dev/null +++ b/octopod-css/development/styles/_listing.css @@ -0,0 +1,56 @@ +.listing { + display: flex; + flex-direction: column; + align-items: flex-start; +} + .listing__item.bar { + margin-top: 2px; + } + + .listing__item.bar--larger { + margin-top: 4px; + } + + .listing__item.bar:first-child { + margin-top: 0; + } + + .listing__more { + margin-top: 2px; + } + + +/* пока что не встречалось: */ +.listing--larger { + +} + + +.listing--for-text { + display: block; + font-size: 12px; /* поскольку тут нет .bar теперь размеры задаем вручную */ + line-height: 16px; +} + .listing--for-text .listing__item { + margin-top: 8px; + white-space: nowrap; /* поскольку тут нет .bar теперь обрезание текста задаем вручную */ + overflow: hidden; + text-overflow: ellipsis; + } + + .listing--for-text .listing__item:first-child { + margin-top: 0; + } + .listing--for-text b { + font-weight: 500; + } + + .listing--for-text .listing__more { + margin-top: 8px; + } + + +.listing--for-text.listing--larger { + font-size: 16px; + line-height: 24px; +} diff --git a/octopod-css/development/styles/_loading.css b/octopod-css/development/styles/_loading.css new file mode 100755 index 0000000..b36a297 --- /dev/null +++ b/octopod-css/development/styles/_loading.css @@ -0,0 +1,82 @@ +.loading { + position: relative; + display: inline-block; + vertical-align: top; + padding-left: 21px; + font-size: 14px; + line-height: 16px; + color: var(--dark-grey); +} + @keyframes loading { + to { transform: rotate(360deg); } + } + + .loading::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: auto; + margin: auto; + display: block; + width: 16px; + height: 16px; + background-image: url("../vectors/loading.svg"); + background-size: contain; + background-repeat: no-repeat; + background-position: center center; + animation: loading 2.2s linear infinite; + } + +.loading--status-alike { + font-weight: bold; + font-size: 12px; + letter-spacing: 0.4px; + text-transform: uppercase; +} + + +.loading--alternate { + +} + .loading--alternate::before { + background-image: url("../vectors/loading-alternate.svg"); + } + + +.loading--enlarged { + padding-left: 0; + padding-top: 66px; + padding-bottom: 8px; + width: 62px; + text-align: center; +} + .loading--enlarged::before { + left: 0; + top: 0; + bottom: auto; + right: 0; + width: 52px; + height: 52px; + background-image: url("../vectors/loading-enlarged.svg"); + } + + .loading--alternate.loading--enlarged::before { + background-image: url("../vectors/loading-enlarged-alternate.svg"); + } + + +.loading--indicator-only { + width: 16px; + height: 16px; + padding: 0; + text-indent: 200%; + white-space: nowrap; + overflow: hidden; +} + +.loading--indicator-only.loading--enlarged { + width: 52px; + height: 52px; +} diff --git a/octopod-css/development/styles/_log.css b/octopod-css/development/styles/_log.css new file mode 100755 index 0000000..f759ae4 --- /dev/null +++ b/octopod-css/development/styles/_log.css @@ -0,0 +1,10 @@ +.log { + margin-top: 0; + margin-bottom: 0; + display: block; + padding: 24px; + font-family: "Roboto Mono", monospace; + font-size: 12px; + line-height: 16px; + word-break: break-all; +} diff --git a/octopod-css/development/styles/_nav.css b/octopod-css/development/styles/_nav.css new file mode 100755 index 0000000..1a5c6d9 --- /dev/null +++ b/octopod-css/development/styles/_nav.css @@ -0,0 +1,39 @@ +.nav { + display: inline-flex; + align-items: flex-start; +} + .nav__link { + position: relative; + display: inline-block; + vertical-align: top; + background-color: transparent; + border: none; + padding: 21px 16px 19px; + font-family: inherit; + font-size: 14px; + line-height: 16px; + letter-spacing: 0.75px; + text-transform: uppercase; + cursor: pointer; + } + + .nav__link:hover { + color: var(--primary-active); + } + + .nav__link--current { + font-weight: 500; + color: var(--primary-active); + } + .nav__link--current::after { + content: ""; + position: absolute; + left: 16px; + right: 16px; + bottom: 0; + display: block; + height: 4px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + background-color: var(--primary-active); + } diff --git a/octopod-css/development/styles/_no-data.css b/octopod-css/development/styles/_no-data.css new file mode 100755 index 0000000..143ca7f --- /dev/null +++ b/octopod-css/development/styles/_no-data.css @@ -0,0 +1,8 @@ +.no-data { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + padding-top: 50px; + padding-bottom: 40px; +} diff --git a/octopod-css/development/styles/_no-deployment.css b/octopod-css/development/styles/_no-deployment.css new file mode 100755 index 0000000..a94f567 --- /dev/null +++ b/octopod-css/development/styles/_no-deployment.css @@ -0,0 +1,7 @@ +.no-deployment { + height: 100%; + padding-bottom: 72px; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/octopod-css/development/styles/_no-log.css b/octopod-css/development/styles/_no-log.css new file mode 100755 index 0000000..706c7b1 --- /dev/null +++ b/octopod-css/development/styles/_no-log.css @@ -0,0 +1,7 @@ +.no-log { + height: 100%; + padding-bottom: 72px; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/octopod-css/development/styles/_no-page.css b/octopod-css/development/styles/_no-page.css new file mode 100755 index 0000000..ddbe452 --- /dev/null +++ b/octopod-css/development/styles/_no-page.css @@ -0,0 +1,15 @@ +.no-page { + display: table; + vertical-align: top; + width: 100%; + height: calc(100% - 72px); /* minus header */ +} + .no-page__inner { + display: table-cell; + vertical-align: middle; + text-align: center; + width: 100%; + height: 100%; + padding-top: 40px; + padding-bottom: 40px; + } diff --git a/octopod-css/development/styles/_no-table.css b/octopod-css/development/styles/_no-table.css new file mode 100755 index 0000000..5233e53 --- /dev/null +++ b/octopod-css/development/styles/_no-table.css @@ -0,0 +1,16 @@ +.no-table { + background-color: transparent !important; + cursor: auto !important; +} + .no-table td { + vertical-align: middle; + text-align: center; + height: 352px; + padding-top: 50px; + padding-bottom: 50px; + } + .no-table .loading { + display: inline-block !important; /* конфликт со стилями лоадера */ + vertical-align: top !important; /* который вставляется в первый */ + margin-top: 0 !important; /* столбец всместо .status */ + } diff --git a/octopod-css/development/styles/_notification.css b/octopod-css/development/styles/_notification.css new file mode 100755 index 0000000..9343ee7 --- /dev/null +++ b/octopod-css/development/styles/_notification.css @@ -0,0 +1,54 @@ +.notification { + position: relative; + padding: 15px 46px 15px 16px; + border: 1px solid; + border-radius: 4px; + font-size: 14px; + line-height: 20px; +} + .notification__close { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 46px; + padding: 0; + border: none; + background-color: transparent; + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center center; + text-indent: 200%; + white-space: nowrap; + overflow: hidden; + cursor: pointer; + } + +.notification--success { + background-color: rgba(15, 127, 18, 0.1); + color: var(--green); +} + .notification--success .notification__close { + background-image: url("../vectors/notification-close-success.svg"); + } + .notification--success:hover .notification__close { + background-image: url("../vectors/notification-close-success-hover.svg"); + } + .notification--success:active .notification__close { + background-image: url("../vectors/notification-close-success-active.svg"); + } + +.notification--danger { + background-color: rgba(212, 42, 42, 0.1); + color: var(--red); +} + .notification--danger .notification__close { + background-image: url("../vectors/notification-close-danger.svg"); + } + .notification--danger:hover .notification__close { + background-image: url("../vectors/notification-close-danger-hover.svg"); + } + .notification--danger:active .notification__close { + background-image: url("../vectors/notification-close-danger-active.svg"); + } + diff --git a/octopod-css/development/styles/_null.css b/octopod-css/development/styles/_null.css new file mode 100755 index 0000000..674359a --- /dev/null +++ b/octopod-css/development/styles/_null.css @@ -0,0 +1,66 @@ +.null { + display: inline-block; + vertical-align: top; +} + .null::before { + content: ""; + display: block; + margin: 0 auto 22px; + background-size: contain; + background-repeat: no-repeat; + background-position: center center; + } + + .null__heading { + margin-top: 0; + margin-bottom: 0; + display: block; + font-weight: bold; + font-size: 16px; + line-height: 20px; + text-align: center; + } + + .null__message { + margin-top: 14px; + font-size: 14px; + line-height: 16px; + color: var(--dark-grey); + text-align: center; + } + + +.null--data { + padding-bottom: 8px; +} + .null--data::before { + width: 174px; + height: 120px; + background-image: url("../images/null-data.png"); + } + + @media (min-resolution: 2dppx) { + .null--data::before { + background-image: url("../images/null-data@2x.png"); + } + } + + +.null--search { + +} + .null--search::before { + width: 120px; + height: 120px; + background-image: url("../images/null-search.png"); + } + + @media (min-resolution: 2dppx) { + .null--search::before { + background-image: url("../images/null-search@2x.png"); + } + } + + .null--search .null__message { + margin-top: 6px; + } diff --git a/octopod-css/development/styles/_overrides.css b/octopod-css/development/styles/_overrides.css new file mode 100755 index 0000000..99a3d3d --- /dev/null +++ b/octopod-css/development/styles/_overrides.css @@ -0,0 +1,28 @@ +.overrides { + +} + .overrides__item { + margin-top: 8px; + display: flex; + align-self: flex-start; + } + + .overrides__item:first-child { + margin-top: 0; + } + .overrides__key { + width: 400px; + } + + .overrides__value { + margin-left: 16px; + width: 400px; + } + + .overrides__delete { + margin-left: 2px; + } + + .overrides__add { + margin-top: 4px; + } diff --git a/octopod-css/development/styles/_page.css b/octopod-css/development/styles/_page.css new file mode 100755 index 0000000..bbff6fc --- /dev/null +++ b/octopod-css/development/styles/_page.css @@ -0,0 +1,71 @@ +.page { + display: flex; + flex-direction: column; + height: calc(100% - 72px); /* minus header */ + padding-top: 24px; +} + .page__wrap { + display: flex; + flex-direction: column; + min-height: 100%; + } + .page__output { + flex-shrink: 0; + margin-bottom: 24px; + } + + .page__back { + align-self: flex-start; + margin-top: -8px; + margin-bottom: -8px; + } + + .page__head { + flex-shrink: 0; + display: flex; + align-items: flex-start; + margin-bottom: 16px; + } + .page__heading { + margin-top: 5px; + margin-bottom: 0; + } + + .page__note { + margin-left: 15px; + margin-top: 17px; + font-size: 14px; + line-height: 16px; + color: var(--dark-grey); + } + .page__action { + margin-left: auto; + } + + .page__action + .page__action { + margin-left: 8px; + } + + .page__action:first-child { + margin-left: 0; + } + + .page__action--search { + margin-right: 8px; + } + + .page__search { + margin-left: auto; + width: 240px; + } + + .page__add-deployment { + margin-left: 16px; + } + + .page__body { + flex-grow: 1; + display: flex; + flex-direction: column; + padding-bottom: 24px; + } diff --git a/octopod-css/development/styles/_popup.css b/octopod-css/development/styles/_popup.css new file mode 100755 index 0000000..68801e2 --- /dev/null +++ b/octopod-css/development/styles/_popup.css @@ -0,0 +1,139 @@ +.popup { + position: relative; + z-index: 550; /* выше .drop который может быть активным ПОД попапом (открытие logs из таблицы экшнов) */ + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, + visibility 0.3s ease; +} + .popup__overlay { + position: fixed; + z-index: 600; + left: 0; + top: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.3); + text-indent: 200%; + white-space: nowrap; + overflow: hidden; + } + + .popup__body { + position: fixed; + z-index: 800; + right: 0; + top: 0; + bottom: 0; + width: 1000px; + background-color: #FFFFFF; + } + .popup__head { + position: absolute; + z-index: 20; + left: 0; + top: 0; + right: 0; + height: 72px; + display: flex; + align-items: center; + background-color: #FFFFFF; + box-shadow: var(--shadow_1); + } + .popup__close { + display: inline-block; + vertical-align: top; + width: 72px; + height: 72px; + padding: 0; + border: none; + background-color: transparent; + background-image: url("../vectors/popup-close.svg"); + background-size: 24px 24px; + background-repeat: no-repeat; + background-position: center center; + text-indent: 100%; + white-space: nowrap; + overflow: hidden; + cursor: pointer; + } + + .popup__close:hover { + background-image: url("../vectors/popup-close-hover.svg"); + } + + .popup__close:active { + background-image: url("../vectors/popup-close-active.svg"); + } + + .popup__project { + margin-top: 0; + margin-bottom: 0; + margin-right: 32px; + display: block; + font-weight: inherit; + font-size: 24px; + line-height: 24px; + } + + .popup__operations { + margin-left: auto; + display: flex; + align-items: flex-start; + } + .popup__action { + margin-left: 8px; + } + + .popup__action:first-child { + margin-left: 0; + } + + .popup__menu { + margin-left: 8px; + margin-right: 10px; + } + + + .popup__sub-head { + position: absolute; + z-index: 10; + left: 0; + top: 72px; + right: 0; + height: 56px; + display: flex; + align-items: center; + background-color: #FFFFFF; + box-shadow: inset 0 -1px 0 #D9D9D9; + } + + .popup__head ~ .popup__content { + margin-top: 72px; + height: calc(100% - 72px); + overflow-y: auto; + } + + .popup__head ~ .popup__sub-head ~ .popup__content { + margin-top: 128px; + height: calc(100% - 128px); + overflow-y: auto; + } + .popup__sub-head .nav__link:first-child { + border-left: 8px solid transparent; + } + + +.popup--visible { + opacity: 1; + visibility: visible; +} + + @keyframes popup-body-slide { + from { transform: translateX(20px); } + to { transform: translateX(0); } + } + + .popup--visible .popup__body { + animation: popup-body-slide 0.3s ease; + } diff --git a/octopod-css/development/styles/_root.css b/octopod-css/development/styles/_root.css new file mode 100755 index 0000000..bb315cf --- /dev/null +++ b/octopod-css/development/styles/_root.css @@ -0,0 +1,25 @@ +/* stylelint-disable declaration-colon-space-after */ +:root { + --primary: #3671E3; + --primary-hover: #3A6CCC; + --primary-active: #3964B8; + --primary-disabled: #99B7F3; + + --grey: #EEEEEE; + --grey-hover: #E2E2E2; + --grey-active: #D6D6D6; + + --dark-grey: #757575; + --dark-grey-50pct: #757575; + + --orange: #FC5830; + + --red: #D42A2A; + --green: #0F7F12; + + --shadow_1: 0 2px 2px rgba(0, 0, 0, 0.24), + 0 0 2px rgba(0, 0, 0, 0.12); + + --transition-basic: 0.1s cubic-bezier(0.075, 0.82, 0.165, 1); +} +/* stylelint-enable declaration-colon-space-after */ diff --git a/octopod-css/development/styles/_sort.css b/octopod-css/development/styles/_sort.css new file mode 100755 index 0000000..bfc6f17 --- /dev/null +++ b/octopod-css/development/styles/_sort.css @@ -0,0 +1,73 @@ +.sort { + position: relative; + display: inline-block; + vertical-align: top; + border: none; + background: none; + margin: -8px; + padding: 8px; + font-family: inherit; + font-weight: 500; + font-size: 14px; + line-height: 16px; + text-decoration: underline; + color: #000000; + cursor: pointer; +} + .sort::after { + position: absolute; + right: 12px; + top: 14px; + content: ""; + display: block; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + opacity: 0; + } + + .sort:hover { + color: var(--primary); + } + + .sort:active { + + } + + +.sort--desc { + padding-right: 24px; +} + .sort--desc::after { + border-top: 4px solid; + } + +.sort--active.sort--desc:hover { + +} + .sort--active.sort--desc:hover::after { + + } + + +.sort--asc { + padding-right: 24px; +} + .sort--asc::after { + border-bottom: 4px solid; + } + +.sort--active.sort--asc:hover { + +} + .sort--active.sort--asc:hover::after { + + } + + + +.sort--active { + +} + .sort--active::after { + opacity: 1; + } diff --git a/octopod-css/development/styles/_spot.css b/octopod-css/development/styles/_spot.css new file mode 100755 index 0000000..1b45842 --- /dev/null +++ b/octopod-css/development/styles/_spot.css @@ -0,0 +1,25 @@ +.spot { + display: inline-block; + vertical-align: top; + border: none; + background-color: transparent; + background-repeat: no-repeat; + background-position: center center; + background-size: 24px 24px; + padding: 0; + width: 40px; + height: 40px; + cursor: pointer; + text-indent: 200%; + white-space: nowrap; + overflow: hidden; + opacity: 0.5; +} + +.spot--cancel { + background-image: url("../vectors/spot.svg"); +} + +.spot--cancel:hover { + opacity: 1; +} diff --git a/octopod-css/development/styles/_status.css b/octopod-css/development/styles/_status.css new file mode 100755 index 0000000..fbc2ae1 --- /dev/null +++ b/octopod-css/development/styles/_status.css @@ -0,0 +1,31 @@ +.status { + background-size: 16px 16px; + background-repeat: no-repeat; + background-position: left center; + padding-left: 21px; + font-weight: bold; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + text-transform: uppercase; +} + +.status--success { + color: #0F7F12; + background-image: url("../vectors/status-success.svg"); +} + +.status--pending { + color: var(--primary); + background-image: url("../vectors/status-pending.svg"); +} + +.status--failure { + color: var(--orange); + background-image: url("../vectors/status-failure.svg"); +} + +.status--archived { + color: var(--red); + background-image: url("../vectors/status-archived.svg"); +} diff --git a/octopod-css/development/styles/_table.css b/octopod-css/development/styles/_table.css new file mode 100755 index 0000000..f4d8231 --- /dev/null +++ b/octopod-css/development/styles/_table.css @@ -0,0 +1,224 @@ +.table { + background-color: #FFFFFF; + border: 1px solid #E0E0E0; + border-radius: 4px; +} + .table table { + width: 100%; + vertical-align: top; + border-collapse: collapse; + table-layout: fixed; + font-size: 14px; + line-height: 20px; + color: rgba(0, 0, 0, 0.87); + } + .table tr { + + } + + .table thead tr { + background-color: #EEEEEE; + } + + .table tbody tr { + border-top: 1px solid #E0E0E0; + } + .table th { + vertical-align: middle; + height: 48px; + padding: 16px 12px; + font-weight: 500; + font-size: 14px; + line-height: 16px; + color: #000000; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + } + + .table td { + vertical-align: top; + padding: 16px 12px; + } + + + /* Скругление углов всей таблицы */ + /* stylelint-disable block-opening-brace-space-before, selector-descendant-combinator-no-non-space */ + .table tr:first-child th:first-child { border-top-left-radius: 3px; } + .table tr:first-child th:last-child { border-top-right-radius: 3px; } + .table tr:last-child td:first-child { border-bottom-left-radius: 3px; } + .table tr:last-child td:last-child { border-bottom-right-radius: 3px; } + /* stylelint-enable */ + + + + +.table--clickable { + +} + .table--clickable tbody tr { + cursor: default; + } + + .table--clickable tbody tr:hover { + background-color: #F8F8F8; + } + + +.table--double-click { + +} + .table--double-click tbody tr { + + } + .table--double-click tbody tr { + user-select: none; /* При двойном клике происходит выделение */ + } + + + + +.table--deployments { + +} + /* В случае table-layout: fixed; ширины колонкам задаются по ширинам в первой строке */ + .table--deployments th:nth-child(1) { width: 16.1818%; } /* 178px */ + .table--deployments th:nth-child(2) { width: 16.1818%; } /* 178px */ + .table--deployments th:nth-child(3) { width: 8%; } /* 88px */ + .table--deployments th:nth-child(4) { width: 20.3636%; } /* 224px */ + .table--deployments th:nth-child(5) { width: 20.3636%; } /* 224px */ + .table--deployments th:nth-child(6) { width: 9.4546%; } /* 104px */ + .table--deployments th:nth-child(7) { width: 9.4546%; } /* 104px */ + .table--deployments th:nth-child(8) { width: 52px; } + + .table--deployments th:first-child, + .table--deployments td:first-child { + padding-left: 16px; + } + + .table--deployments th:last-child, + .table--deployments td:last-child { + padding-right: 16px; + } + + + /* Name */ + .table--deployments td:nth-child(1) { + font-weight: 500; + font-size: 16px; + line-height: 24px; + } + .table--deployments td:nth-child(1) .status, + .table--deployments td:nth-child(1) .loading { + margin-top: 8px; + display: block; + } + + /* Links */ + .table--deployments td:nth-child(2) { + + } + + /* Tag */ + .table--deployments td:nth-child(3) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + /* App overrides */ + .table--deployments td:nth-child(4) { + + } + + /* Deployment overrides */ + .table--deployments td:nth-child(5) { + + } + + /* Created */ + .table--deployments td:nth-child(6) { + white-space: nowrap; + } + + /* Changed */ + .table--deployments td:nth-child(7) { + white-space: nowrap; + } + + /* Dropdown */ + .table--deployments td:nth-child(8) { + + } + .table--deployments td:nth-child(8) .drop { + margin: -8px; + } + + + + +.table--actions { + +} + .table--actions table { + + } + + /* В случае table-layout: fixed; ширины колонкам задаются по ширинам в первой строке */ + .table--actions th:nth-child(1) { width: 8%; } + .table--actions th:nth-child(2) { width: 20%; } + .table--actions th:nth-child(3) { width: 21%; } + .table--actions th:nth-child(4) { width: 20%; } + .table--actions th:nth-child(5) { width: 7%; } + .table--actions th:nth-child(6) { width: 15%; } + .table--actions th:nth-child(7) { width: 9%; } + + .table--actions th:first-child, + .table--actions td:first-child { + padding-left: 16px; + } + + .table--actions th:last-child, + .table--actions td:last-child { + padding-right: 16px; + } + + /* Action type */ + .table--actions td:nth-child(1) { + font-weight: 500; + font-size: 16px; + line-height: 24px; + } + .table--actions td:nth-child(1) .status, + .table--actions td:nth-child(1) .loading { + margin-top: 8px; + display: block; + } + + /* Image tag */ + .table--actions td:nth-child(2) { + word-break: break-all; + } + + /* Overrides */ + .table--actions td:nth-child(3) { + + } + + /* Created */ + .table--actions td:nth-child(4) { + + } + + /* Deployment duration */ + .table--actions td:nth-child(5) { + white-space: nowrap; + } + + /* Dropdown */ + .table--actions td:nth-child(6) { + + } + .table--actions td:nth-child(6) .drop { + margin: -8px; + } diff --git a/octopod-css/development/styles/_tag.css b/octopod-css/development/styles/_tag.css new file mode 100755 index 0000000..cce9437 --- /dev/null +++ b/octopod-css/development/styles/_tag.css @@ -0,0 +1,3 @@ +.tag { + width: 400px; +} diff --git a/octopod-css/development/styles/_title.css b/octopod-css/development/styles/_title.css new file mode 100755 index 0000000..22d1a4d --- /dev/null +++ b/octopod-css/development/styles/_title.css @@ -0,0 +1,5 @@ +.title { + font-weight: inherit; + font-size: 28px; + line-height: 32px; +} diff --git a/octopod-css/development/styles/_universal.css b/octopod-css/development/styles/_universal.css new file mode 100755 index 0000000..cf03081 --- /dev/null +++ b/octopod-css/development/styles/_universal.css @@ -0,0 +1,11 @@ +* { + box-sizing: border-box; +} + +*::before { + box-sizing: border-box; +} + +*::after { + box-sizing: border-box; +} \ No newline at end of file diff --git a/octopod-css/development/styles/_visuallyhidden.css b/octopod-css/development/styles/_visuallyhidden.css new file mode 100755 index 0000000..f86db94 --- /dev/null +++ b/octopod-css/development/styles/_visuallyhidden.css @@ -0,0 +1,41 @@ +/* + * Hide only visually, but have it available for screen readers: + * https://snook.ca/archives/html_and_css/hiding-content-for-accessibility + * + * 1. For long content, line feeds are not interpreted as spaces and small width + * causes content to wrap 1 word per line: + * https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe + */ + +.visuallyhidden { + border: 0; + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; /* 1 */ +} + +/* + * Extends the .visuallyhidden class to allow the element + * to be focusable when navigated to via the keyboard: + * https://www.drupal.org/node/897638 + */ + +.visuallyhidden.focusable:active, +.visuallyhidden.focusable:focus { + clip: auto; + -webkit-clip-path: none; + clip-path: none; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; + white-space: inherit; +} \ No newline at end of file diff --git a/octopod-css/development/styles/style.css b/octopod-css/development/styles/style.css new file mode 100755 index 0000000..44723fd --- /dev/null +++ b/octopod-css/development/styles/style.css @@ -0,0 +1,42 @@ +@import url("../vendors/normalize/normalize.css"); +@import url("_body.css"); +@import url("_browserupgrade.css"); +@import url("_button.css"); +@import url("_root.css"); +@import url("_universal.css"); +@import url("_visuallyhidden.css"); +@import url("_container.css"); +@import url("_header.css"); +@import url("_status.css"); +@import url("_bar.css"); +@import url("_external.css"); +@import url("_expander.css"); +@import url("_action.css"); +@import url("_drop.css"); +@import url("_input.css"); +@import url("_spot.css"); +@import url("_title.css"); +@import url("_page.css"); +@import url("_table.css"); +@import url("_listing.css"); +@import url("_sort.css"); +@import url("_notification.css"); +@import url("_null.css"); +@import url("_html.css"); +@import url("_no-page.css"); +@import url("_popup.css"); +@import url("_deployment.css"); +@import url("_tag.css"); +@import url("_overrides.css"); +@import url("_dash.css"); +@import url("_log.css"); +@import url("_no-deployment.css"); +@import url("_data.css"); +@import url("_no-data.css"); +@import url("_classic-popup.css"); +@import url("_dialog.css"); +@import url("_no-table.css"); +@import url("_loading.css"); +@import url("_no-log.css"); +@import url("_nav.css"); +@import url("_break.css"); diff --git a/octopod-css/development/vectors/button-add.svg b/octopod-css/development/vectors/button-add.svg new file mode 100644 index 0000000..1a7caeb --- /dev/null +++ b/octopod-css/development/vectors/button-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/button-save.svg b/octopod-css/development/vectors/button-save.svg new file mode 100644 index 0000000..3180f56 --- /dev/null +++ b/octopod-css/development/vectors/button-save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/classic-popup-close.svg b/octopod-css/development/vectors/classic-popup-close.svg new file mode 100644 index 0000000..b536414 --- /dev/null +++ b/octopod-css/development/vectors/classic-popup-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/common-use-archive.svg b/octopod-css/development/vectors/common-use-archive.svg new file mode 100644 index 0000000..7758d3a --- /dev/null +++ b/octopod-css/development/vectors/common-use-archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/common-use-edit-white.svg b/octopod-css/development/vectors/common-use-edit-white.svg new file mode 100644 index 0000000..11591b8 --- /dev/null +++ b/octopod-css/development/vectors/common-use-edit-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/common-use-edit.svg b/octopod-css/development/vectors/common-use-edit.svg new file mode 100644 index 0000000..3185e3c --- /dev/null +++ b/octopod-css/development/vectors/common-use-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/common-use-logs.svg b/octopod-css/development/vectors/common-use-logs.svg new file mode 100644 index 0000000..02f7f73 --- /dev/null +++ b/octopod-css/development/vectors/common-use-logs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/common-use-restore.svg b/octopod-css/development/vectors/common-use-restore.svg new file mode 100644 index 0000000..db8310a --- /dev/null +++ b/octopod-css/development/vectors/common-use-restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-add-active.svg b/octopod-css/development/vectors/dash-add-active.svg new file mode 100644 index 0000000..a7685f6 --- /dev/null +++ b/octopod-css/development/vectors/dash-add-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-add-hover.svg b/octopod-css/development/vectors/dash-add-hover.svg new file mode 100644 index 0000000..f7b3bd6 --- /dev/null +++ b/octopod-css/development/vectors/dash-add-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-add.svg b/octopod-css/development/vectors/dash-add.svg new file mode 100644 index 0000000..74c4d36 --- /dev/null +++ b/octopod-css/development/vectors/dash-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-back-active.svg b/octopod-css/development/vectors/dash-back-active.svg new file mode 100644 index 0000000..f31ddaa --- /dev/null +++ b/octopod-css/development/vectors/dash-back-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-back-hover.svg b/octopod-css/development/vectors/dash-back-hover.svg new file mode 100644 index 0000000..02c6821 --- /dev/null +++ b/octopod-css/development/vectors/dash-back-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-back.svg b/octopod-css/development/vectors/dash-back.svg new file mode 100644 index 0000000..13ad7a6 --- /dev/null +++ b/octopod-css/development/vectors/dash-back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-next-active.svg b/octopod-css/development/vectors/dash-next-active.svg new file mode 100644 index 0000000..3eab99b --- /dev/null +++ b/octopod-css/development/vectors/dash-next-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-next-hover.svg b/octopod-css/development/vectors/dash-next-hover.svg new file mode 100644 index 0000000..d7e13fc --- /dev/null +++ b/octopod-css/development/vectors/dash-next-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/dash-next.svg b/octopod-css/development/vectors/dash-next.svg new file mode 100644 index 0000000..7207dc4 --- /dev/null +++ b/octopod-css/development/vectors/dash-next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/drop-actions-active.svg b/octopod-css/development/vectors/drop-actions-active.svg new file mode 100644 index 0000000..2567635 --- /dev/null +++ b/octopod-css/development/vectors/drop-actions-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/drop-actions-hover.svg b/octopod-css/development/vectors/drop-actions-hover.svg new file mode 100644 index 0000000..3ffc313 --- /dev/null +++ b/octopod-css/development/vectors/drop-actions-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/drop-actions.svg b/octopod-css/development/vectors/drop-actions.svg new file mode 100644 index 0000000..00aaa73 --- /dev/null +++ b/octopod-css/development/vectors/drop-actions.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/expander-active.svg b/octopod-css/development/vectors/expander-active.svg new file mode 100644 index 0000000..a586e39 --- /dev/null +++ b/octopod-css/development/vectors/expander-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/expander-hover.svg b/octopod-css/development/vectors/expander-hover.svg new file mode 100644 index 0000000..eb1e2dc --- /dev/null +++ b/octopod-css/development/vectors/expander-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/expander-larger-active.svg b/octopod-css/development/vectors/expander-larger-active.svg new file mode 100644 index 0000000..c97dc0b --- /dev/null +++ b/octopod-css/development/vectors/expander-larger-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/expander-larger-hover.svg b/octopod-css/development/vectors/expander-larger-hover.svg new file mode 100644 index 0000000..f0266d1 --- /dev/null +++ b/octopod-css/development/vectors/expander-larger-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/expander-larger.svg b/octopod-css/development/vectors/expander-larger.svg new file mode 100644 index 0000000..ad5e47e --- /dev/null +++ b/octopod-css/development/vectors/expander-larger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/expander.svg b/octopod-css/development/vectors/expander.svg new file mode 100644 index 0000000..6b20804 --- /dev/null +++ b/octopod-css/development/vectors/expander.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/external-hover-active.svg b/octopod-css/development/vectors/external-hover-active.svg new file mode 100644 index 0000000..88833b7 --- /dev/null +++ b/octopod-css/development/vectors/external-hover-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/external.svg b/octopod-css/development/vectors/external.svg new file mode 100644 index 0000000..d1ed724 --- /dev/null +++ b/octopod-css/development/vectors/external.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/input-search.svg b/octopod-css/development/vectors/input-search.svg new file mode 100644 index 0000000..5c485ad --- /dev/null +++ b/octopod-css/development/vectors/input-search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/loading-alternate.svg b/octopod-css/development/vectors/loading-alternate.svg new file mode 100644 index 0000000..96faddc --- /dev/null +++ b/octopod-css/development/vectors/loading-alternate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/loading-enlarged-alternate.svg b/octopod-css/development/vectors/loading-enlarged-alternate.svg new file mode 100644 index 0000000..11f5e45 --- /dev/null +++ b/octopod-css/development/vectors/loading-enlarged-alternate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/loading-enlarged.svg b/octopod-css/development/vectors/loading-enlarged.svg new file mode 100644 index 0000000..aac0397 --- /dev/null +++ b/octopod-css/development/vectors/loading-enlarged.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/loading.svg b/octopod-css/development/vectors/loading.svg new file mode 100644 index 0000000..1a8c39b --- /dev/null +++ b/octopod-css/development/vectors/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/notification-close-danger-active.svg b/octopod-css/development/vectors/notification-close-danger-active.svg new file mode 100644 index 0000000..dfe09ed --- /dev/null +++ b/octopod-css/development/vectors/notification-close-danger-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/notification-close-danger-hover.svg b/octopod-css/development/vectors/notification-close-danger-hover.svg new file mode 100644 index 0000000..84b98d3 --- /dev/null +++ b/octopod-css/development/vectors/notification-close-danger-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/notification-close-danger.svg b/octopod-css/development/vectors/notification-close-danger.svg new file mode 100644 index 0000000..aa064dc --- /dev/null +++ b/octopod-css/development/vectors/notification-close-danger.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/notification-close-success-active.svg b/octopod-css/development/vectors/notification-close-success-active.svg new file mode 100644 index 0000000..92e87a0 --- /dev/null +++ b/octopod-css/development/vectors/notification-close-success-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/notification-close-success-hover.svg b/octopod-css/development/vectors/notification-close-success-hover.svg new file mode 100644 index 0000000..f7e83ac --- /dev/null +++ b/octopod-css/development/vectors/notification-close-success-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/notification-close-success.svg b/octopod-css/development/vectors/notification-close-success.svg new file mode 100644 index 0000000..a15b8f6 --- /dev/null +++ b/octopod-css/development/vectors/notification-close-success.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/popup-close-active.svg b/octopod-css/development/vectors/popup-close-active.svg new file mode 100644 index 0000000..3a28bfb --- /dev/null +++ b/octopod-css/development/vectors/popup-close-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/popup-close-hover.svg b/octopod-css/development/vectors/popup-close-hover.svg new file mode 100644 index 0000000..927778c --- /dev/null +++ b/octopod-css/development/vectors/popup-close-hover.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/popup-close.svg b/octopod-css/development/vectors/popup-close.svg new file mode 100644 index 0000000..c5690e9 --- /dev/null +++ b/octopod-css/development/vectors/popup-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/octopod-css/development/vectors/spot.svg b/octopod-css/development/vectors/spot.svg new file mode 100644 index 0000000..481d062 --- /dev/null +++ b/octopod-css/development/vectors/spot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/status-archived.svg b/octopod-css/development/vectors/status-archived.svg new file mode 100644 index 0000000..3035bb5 --- /dev/null +++ b/octopod-css/development/vectors/status-archived.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/status-failure.svg b/octopod-css/development/vectors/status-failure.svg new file mode 100644 index 0000000..354fd96 --- /dev/null +++ b/octopod-css/development/vectors/status-failure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/status-pending.svg b/octopod-css/development/vectors/status-pending.svg new file mode 100644 index 0000000..bc97255 --- /dev/null +++ b/octopod-css/development/vectors/status-pending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vectors/status-success.svg b/octopod-css/development/vectors/status-success.svg new file mode 100644 index 0000000..451185d --- /dev/null +++ b/octopod-css/development/vectors/status-success.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/octopod-css/development/vendors/jquery/jquery-3.2.1.min.js b/octopod-css/development/vendors/jquery/jquery-3.2.1.min.js new file mode 100755 index 0000000..644d35e --- /dev/null +++ b/octopod-css/development/vendors/jquery/jquery-3.2.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + + +

Fork of outline.js

+ + Test link + +

+ + + +

+ + Test span + +

+ + + /must have it's own focus effect because otline is lost on click + +

+ + + + /need to make an outline for this thing using CSS and it should be possible to cancel it on click using this library + + diff --git a/octopod-css/development/vendors/outline/outline.js b/octopod-css/development/vendors/outline/outline.js new file mode 100755 index 0000000..76ec6eb --- /dev/null +++ b/octopod-css/development/vendors/outline/outline.js @@ -0,0 +1,37 @@ +// outline.js +// based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/ +// original: https://github.com/lindsayevans/outline.js +// this one is a fork: https://github.com/shchukin/outline.js + +(function(d){ + + var style_element = d.createElement('STYLE'), + dom_events = 'addEventListener' in d, + add_event_listener = function(type, callback){ + // Basic cross-browser event handling + if(dom_events){ + d.addEventListener(type, callback); + }else{ + d.attachEvent('on' + type, callback); + } + }, + set_css = function(css_text){ + // Handle setting of + +
+ + + + Loading... +
+ + + diff --git a/octopod-css/production/markups/_action.html b/octopod-css/production/markups/_action.html new file mode 100755 index 0000000..8265b39 --- /dev/null +++ b/octopod-css/production/markups/_action.html @@ -0,0 +1,112 @@ + + + + + + .action + + + + + + + + +
+
+
+
+
+

.action

+ + + + + +
+
+
+
+

.action--archive

+ + + + + +
+
+
+
+

.action--edit

+ + + + + +
+
+
+
+

.action--restore

+ + + + + + +
+
+
+
+

.action--disabled

+

+ Если экшн делается тегом <button> то хорошо бы добавлять атрибут disabled: +

+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_bar.html b/octopod-css/production/markups/_bar.html new file mode 100755 index 0000000..71a88cc --- /dev/null +++ b/octopod-css/production/markups/_bar.html @@ -0,0 +1,61 @@ + + + + + + .bar + + + + + + + + +
+
+
+
+
+

.bar

+ + +
+ ENVIRONENT: production +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_break.html b/octopod-css/production/markups/_break.html new file mode 100755 index 0000000..c95a23d --- /dev/null +++ b/octopod-css/production/markups/_break.html @@ -0,0 +1,85 @@ + + + + + + .break + + + + + + + + + +
+
+
+

.break

+

+ Простой, вспомогательный класс, который переводит режим переносов. + По дефолту текст переносится там где пробелы, табы и любые текстовые разделители. + С этим классом переносится в любом месте. Т.е. слова печатаются до упора + и обрыв на новую строчку может проихойти после любой буквы. +

+
+
+
+ +
+ DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/libiserv/dist-install/build:/Users/dr/workspace/ghc/libraries/ghci/dist-install/build:/Users/dr/workspace/ghc/libraries/transformers/dist-install/build:/Users/dr/workspace/ghc/libraries/template-haskell/dist-install/build:/Users/dr/workspace/ghc/libraries/pretty/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-heap/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-boot/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-boot-th/dist-install/build:/Users/dr/workspace/ghc/libraries/directory/dist-install/build:/Users/dr/workspace/ghc/libraries/unix/dist-install/build:/Users/dr/workspace/ghc/libraries/time/dist-install/build:/Users/dr/workspace/ghc/libraries/filepath/dist-install/build:/Users/dr/workspace/ghc/libraries/binary/dist-install/build:/Users/dr/workspace/ghc/libraries/containers/containers/dist-install/build:/Users/dr/workspace/ghc/libraries/bytestring/dist-install/build:/Users/dr/workspace/ghc/libraries/deepseq/dist-install/build:/Users/dr/workspace/ghc/libraries/array/dist-install/build:/Users/dr/workspace/ghc/libraries/base/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-bignum/dist-install/build:/Users/dr/workspace/ghc/libraries/ghc-prim/dist-install/build:/Users/dr/workspace/ghc/rts/dist/build${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"' >> inplace/lib/bin/ghc-iserv-dyn +
+ +
+
+
+

+ Можно завертывать в <div class="break">, как в примере выше. + Можно просто добавлять класс .break куда угодно. Например к ячейке таблицы, + вот так вот: <td class="break">. +

+

+ Не работает с <pre>. Там скроллбар. Можно вешать этот класс на обычный <div>, + и давать ему моноширный шрифт. В нашем случае это: +
font-family: "Roboto Mono", monospace;. + И добавляем <br> перед первой стокой. + Таким образом будет получаться аналог <pre>. +

+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_button.html b/octopod-css/production/markups/_button.html new file mode 100755 index 0000000..eadb953 --- /dev/null +++ b/octopod-css/production/markups/_button.html @@ -0,0 +1,137 @@ + + + + + + .button + + + + + + + + + +
+
+
+

.button

+ + + +
+
+
+
+

.button--secondary

+ + + + +
+
+
+
+

.button--add

+ + + + + +
+
+
+
+

.button--save

+ + + + +
+
+
+
+

.button--disabled

+

+ Хорошо бы еще добавить атрибут disabled +

+ + + + + +
+
+ + + + + + + + +
+
+
+
+

Теги

+ +
div.button
+ +

+ + span.button + +

+ + + +

+ + a.button + +

+ + + +

+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_dash.html b/octopod-css/production/markups/_dash.html new file mode 100755 index 0000000..ce54d41 --- /dev/null +++ b/octopod-css/production/markups/_dash.html @@ -0,0 +1,130 @@ + + + + + + .dash + + + + + + + + + +
+
+
+

.dash

+ +
+
+
+
+

.dash--add

+ + + +
+
+
+
+

.dash--add

+ + + + + +
+
+
+
+

.dash--smaller

+ + + +
+
+
+
+

.dash--disabled

+

+ Хорошо бы добавлять атрибут disabled +

+ + + + +
+
+
+
+

Теги

+ +
Add an override
+ +

+ + Add an override + +

+ + + +

+ + Add an override + +

+ + + +

+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_drop.html b/octopod-css/production/markups/_drop.html new file mode 100755 index 0000000..a164843 --- /dev/null +++ b/octopod-css/production/markups/_drop.html @@ -0,0 +1,112 @@ + + + + + + .drop + + + + + + + + +
+
+
+
+
+

.drop

+ + +

+ Общий компонент для выпадаек что по клику. + Они могут отличаться внешне, но жаба-скрипт везде будет один. +

+

+ Смотрим базовую версию: +

+
+
+ +
+ +
+ dropdown +
+
+ + +
+
+
+
+

+ Дальше либо натягиваем скины, либо просто смиксовываем с другими блоками. +

+ + +
+
+
+
+

.drop--actions

+
+ + +
+ +
+ + +
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_expander.html b/octopod-css/production/markups/_expander.html new file mode 100755 index 0000000..3a570ef --- /dev/null +++ b/octopod-css/production/markups/_expander.html @@ -0,0 +1,96 @@ + + + + + + .expander + + + + + + + + +
+
+
+
+
+

.expander

+ + + + + +
+
+
+
+

.expander--stand-alone

+

+ Название странное, так исторически сложилось +

+
+ + + +
+
+
+
+

.expander--open

+

+ Для расхлопывания добавляем .expander--open: +

+ + + +
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_external.html b/octopod-css/production/markups/_external.html new file mode 100755 index 0000000..74a2e61 --- /dev/null +++ b/octopod-css/production/markups/_external.html @@ -0,0 +1,61 @@ + + + + + + .external + + + + + + + + +
+
+
+
+
+

.external

+ + + + app.feature-name.project.name.com lorem ipsume dolor sit amen + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_input.html b/octopod-css/production/markups/_input.html new file mode 100755 index 0000000..0e511a0 --- /dev/null +++ b/octopod-css/production/markups/_input.html @@ -0,0 +1,111 @@ + + + + + + .input + + + + + + + + + +
+
+
+

.input

+ +
+ +
+ + +
+
+
+
+

Клеартайп

+

+ Здесь помимо самого клеартайпа еще нужно добавить модификатор + .input--has-clear-type -- он переиграет отступы и другие версточные штуки + (например выключит дефолтный ИЕшный клеартайп) +

+ +
+ + +
+ +
+
+
+
+

.input--search

+

+ Просто иконка и паддинг +

+ + + + +
+
+
+
+

.input--error и .input__output

+

+ Первое -- модификатор, что делает сам инпут красным. +
Второе -- просто элемент для вывода ошибки. +

+ +
+ + +
Error message
+
Another message
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_listing.html b/octopod-css/production/markups/_listing.html new file mode 100755 index 0000000..fd34aaf --- /dev/null +++ b/octopod-css/production/markups/_listing.html @@ -0,0 +1,100 @@ + + + + + + .listing + + + + + + + + +
+
+
+
+
+

.listing

+

+ Простая обертка выстраивающая элементы типа .bar + вертикально. Плюс небольшой отступ. +

+ + + + + +
+
+
+

.listing--for-text

+

+ В какой-то момент появилась аналогичная версия, + но уже без .bar а просто с голым текстом внутри. + Пришлось сюда переносить некоторые свойства типа бОльших отступов, + обрезания текста многоточием и т.д. Все это модификатор .listing--for-text: +

+
+
+ +
+
+ ENVIRONENT: production +
+
+ app_api_url: http://my-api.my-domain.com +
+
+ SECRET_CODE: asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_loading.html b/octopod-css/production/markups/_loading.html new file mode 100755 index 0000000..a779ff8 --- /dev/null +++ b/octopod-css/production/markups/_loading.html @@ -0,0 +1,136 @@ + + + + + + .loading + + + + + + + + +
+
+
+
+
+

.loading

+

+ Дефолтный кейс нигде не используется, но он подводят нас к другим нужным нам кейсам, + и, возможно, когда-нибудь подгоним его под нужный вариант: +

+ +
+ Loading... +
+ +
+
+
+
+

.loading--status-alike

+

+ Для использования в кейсах симметричных к .status +

+ +
+ Loading... +
+ +
+
+
+
+

.loading--alternate

+

+ Тоже нигде не используется, но здесь мы учимся делать индикатор синим +

+ +
+ Loading... +
+ + + +
+
+
+
+

.loading--enlarged

+

+ Большой и серый в чистом виде тоже нигде не встречался +

+ +
+ Loading... +
+ +
+
+

+ А вот этот уже используется на странице загрузки: +

+ +
+ Loading... +
+ + + + +
+
+
+
+

.loading--indicator-only

+

+ Бонус если вдург понадобится где-то в чистом виде: +

+ +
+ Loading... +
+ +
+ +
+ Loading... +
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_notification.html b/octopod-css/production/markups/_notification.html new file mode 100755 index 0000000..d635980 --- /dev/null +++ b/octopod-css/production/markups/_notification.html @@ -0,0 +1,88 @@ + + + + + + .notification + + + + + + + + +
+
+
+
+
+

.notification

+ +

+ Дефолтного кейса пока не было: +

+ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed et eros justo. Vivamus scelerisque est sed congue aliquet. Pellentesque vitae sodales lectus. Mauris dignissim id tellus lacinia finibus. Sed egestas sapien urna, at rutrum diam mattis vitae. Duis a tempor enim. Curabitur at facilisis felis. Vestibulum imperdiet arcu a justo cursus mollis eu eget ipsum. + +
+ + +
+
+
+
+

.notification--success

+ +
+ Success. The deployment has moved to the archive + +
+ +
+
+
+
+

.notification--danger

+ +
+ System error. Сan not move the deployment to the archive right now. Please try later. + +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_null.html b/octopod-css/production/markups/_null.html new file mode 100755 index 0000000..593b641 --- /dev/null +++ b/octopod-css/production/markups/_null.html @@ -0,0 +1,81 @@ + + + + + + .null + + + + + + + + +
+
+
+
+
+

.null

+ +
+
+
+
+

.null--data

+ +
+ Cannot retrieve the data +
Try to reload page
+
+ + + +
+
+
+
+

.null--search

+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_spot.html b/octopod-css/production/markups/_spot.html new file mode 100755 index 0000000..5c14772 --- /dev/null +++ b/octopod-css/production/markups/_spot.html @@ -0,0 +1,61 @@ + + + + + + .spot + + + + + + + + +
+
+
+
+
+

.spot

+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_status.html b/octopod-css/production/markups/_status.html new file mode 100755 index 0000000..ef249f4 --- /dev/null +++ b/octopod-css/production/markups/_status.html @@ -0,0 +1,83 @@ + + + + + + .status + + + + + + + + +
+
+
+
+
+

.status

+ + +
+ Success +
+ +
+
+ +
+ Pending +
+ +
+
+ +
+ Failure +
+ +
+
+ +
+ Archived +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/markups/_title.html b/octopod-css/production/markups/_title.html new file mode 100755 index 0000000..53e5c97 --- /dev/null +++ b/octopod-css/production/markups/_title.html @@ -0,0 +1,60 @@ + + + + + + .title + + + + + + + + +
+
+
+
+
+

.title

+ + + +

All deployments

+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/octopod-css/production/scripts/_classic-popup.js b/octopod-css/production/scripts/_classic-popup.js new file mode 100755 index 0000000..0f3e8b5 --- /dev/null +++ b/octopod-css/production/scripts/_classic-popup.js @@ -0,0 +1,92 @@ +(function($) { + $(function() { + + + + /* + * Page lock + */ + + var documentWidthWithScroll = 0; + var documentWidthWithoutScroll = 0; + var scrollWidth = 0; + + var $html = $('html'); + + + function lockPage() { + if ( ! $html.hasClass('html-lock') ) { + documentWidthWithScroll = $(window).width(); + $html.addClass('html-lock'); + documentWidthWithoutScroll = $(window).width(); + $html.css( 'padding-right', (documentWidthWithoutScroll - documentWidthWithScroll) + 'px' ); + } + } + + function unlockPage() { + if ( $html.hasClass('html-lock') ) { + $html.css( 'padding-right', '' ); + $html.removeClass('html-lock'); + } + } + + + + /* + * Popup + */ + + function popupShow(popup){ + lockPage(); + popup.fadeIn(150); + popup.scrollTop(0); + } + + function popupHide(popup){ + if( ! popup ) { //in case of Esc or something + popup = $('.classic-popup'); + } + + popup.fadeOut(150,function(){ //hide popup THAN unlock page + unlockPage(); + }); + } + + /* show popup by handler click */ + + $('.classic-popup-handler').on('click', function(event) { + event.preventDefault(); + var popupId = $(this).attr('href') ? $(this).attr('href') : $(this).attr('data-href') + popupShow( $(popupId) ); + event.stopPropagation(); + }); + + + /* hide popup by window close click */ + + $('.classic-popup__close').on('click', function(event){ + event.preventDefault(); + popupHide( $(this).parents('.classic-popup') ); + }); + + + /* hide popup by overlay click ( goo.gl/SJG2Hw ) */ + + $('.classic-popup').on('click', function(event) { + if (!$(event.target).closest('.classic-popup__slot').length) { + popupHide( $('.classic-popup') ); + } + }); + + + /* hide popup by Esc press */ + + $(document).on('keyup', function(event) { + if (event.keyCode == 27) { + popupHide(); + } + }); + + }); +})(jQuery); + diff --git a/octopod-css/production/scripts/_data.js b/octopod-css/production/scripts/_data.js new file mode 100755 index 0000000..7f5b3a6 --- /dev/null +++ b/octopod-css/production/scripts/_data.js @@ -0,0 +1,7 @@ +(function($) { + + $('.data__show-archive').on('click', function () { + $('.data__archive').toggleClass('data__archive--open'); + }); + +})(jQuery); diff --git a/octopod-css/production/scripts/_drop.js b/octopod-css/production/scripts/_drop.js new file mode 100755 index 0000000..d11a6ee --- /dev/null +++ b/octopod-css/production/scripts/_drop.js @@ -0,0 +1,26 @@ +(function($) { + + $('.drop__handler').on('click', function (event) { + $(this).parents('.drop').toggleClass('drop--expanded'); + event.stopPropagation(); + }); + + + /* Закрываем по клику вне ( goo.gl/SJG2Hw ) */ + + $(document).on('click', function(event) { + if (!$(event.target).closest('.drop').length) { + $('.drop--expanded').removeClass('drop--expanded'); + } + }); + + + /* Закрываем по Esc */ + + $(document).on('keyup', function(event) { + if (event.keyCode === 27) { + $('.drop--expanded').removeClass('drop--expanded'); + } + }); + +})(jQuery); diff --git a/octopod-css/production/scripts/_expander.js b/octopod-css/production/scripts/_expander.js new file mode 100755 index 0000000..e5d4c7c --- /dev/null +++ b/octopod-css/production/scripts/_expander.js @@ -0,0 +1,8 @@ +(function($) { + + $('.expander').on('click', function (event) { + $(this).toggleClass('expander--open'); + event.stopPropagation(); + }); + +})(jQuery); diff --git a/octopod-css/production/scripts/_popup.js b/octopod-css/production/scripts/_popup.js new file mode 100755 index 0000000..9ed0ec4 --- /dev/null +++ b/octopod-css/production/scripts/_popup.js @@ -0,0 +1,35 @@ +(function($) { + + $('.popup-handler').on('click', function (event) { + event.preventDefault(); + var popupId = $(this).attr('href') ? $(this).attr('href') : $(this).attr('data-href') + $( popupId ).addClass('popup--visible'); + event.stopPropagation(); + }); + + + /* Закрываем по крестику */ + + $('.popup__close').on('click', function(event){ + event.preventDefault(); + $(this).parents('.popup').removeClass('popup--visible'); + }); + + + /* Закрываем по клику вне ( goo.gl/SJG2Hw ) */ + + $('.popup__overlay').on('click', function(event) { + $('.popup--visible').removeClass('popup--visible'); + }); + + + /* Закрываем по Esc */ + + $(document).on('keyup', function(event) { + if (event.keyCode === 27) { + $('.popup--visible').removeClass('popup--visible'); + } + }); + + +})(jQuery); diff --git a/octopod-css/production/scripts/_sort.js b/octopod-css/production/scripts/_sort.js new file mode 100755 index 0000000..8407bd7 --- /dev/null +++ b/octopod-css/production/scripts/_sort.js @@ -0,0 +1,16 @@ +(function($) { + + $('.sort').on('click', function () { + if( ! $(this).hasClass('sort--active') ) { + $('.sort--active').removeClass('sort--active').removeClass('sort--desc').removeClass('sort--asc'); + $(this).addClass('sort--active').addClass('sort--desc'); + } else { + if( $(this).hasClass('sort--asc') ) { + $(this).removeClass('sort--asc').addClass('sort--desc'); + } else if( $(this).hasClass('sort--desc') ) { + $(this).removeClass('sort--desc').addClass('sort--asc'); + } + } + }); + +})(jQuery); diff --git a/octopod-css/production/styles/style.css b/octopod-css/production/styles/style.css new file mode 100755 index 0000000..ef908a0 --- /dev/null +++ b/octopod-css/production/styles/style.css @@ -0,0 +1 @@ +html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}body{height:100%;background-color:#FFF;font-family:Roboto,sans-serif}.browserupgrade{background:#CCC;color:#000;padding:4px 10px}.button{display:inline-block;vertical-align:top;height:40px;padding:11px;border:1px solid transparent;background-color:#3671E3;border-radius:4px;font-family:inherit;font-weight:500;font-size:14px;line-height:16px;letter-spacing:.6px;color:#FFF;text-align:center;text-transform:uppercase;text-decoration:none;white-space:nowrap;cursor:pointer}.button:hover{background-color:#3A6CCC}.button:active{background-color:#3964B8}.button--disabled,.button--disabled:active,.button--disabled:hover{background-color:#99B7F3;cursor:default}.button--secondary{background-color:#FFF;border-color:#757575;color:#000}.button--secondary:hover{background-color:#EEE}.button--secondary:active{background-color:#E2E2E2}.button--secondary.button--disabled,.button--secondary.button--disabled:active,.button--secondary.button--disabled:hover{background-color:transparent;opacity:.5;cursor:default}.button--add{padding-left:40px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDEzSDEzVjE5SDExVjEzSDVWMTFIMTFWNUgxM1YxMUgxOVYxM1oiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=);background-size:24px 24px;background-position:11px center;background-repeat:no-repeat}.button--edit{padding-left:40px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMTcuMjUwNVYyMS4wMDA1SDYuNzVMMTcuODEgOS45NDA1NUwxNC4wNiA2LjE5MDU1TDMgMTcuMjUwNVpNMjAuNzEgNy4wNDA1NUMyMS4xIDYuNjUwNTUgMjEuMSA2LjAyMDU1IDIwLjcxIDUuNjMwNTVMMTguMzcgMy4yOTA1NUMxNy45OCAyLjkwMDU1IDE3LjM1IDIuOTAwNTUgMTYuOTYgMy4yOTA1NUwxNS4xMyA1LjEyMDU1TDE4Ljg4IDguODcwNTVMMjAuNzEgNy4wNDA1NVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=);background-size:24px 24px;background-position:11px center;background-repeat:no-repeat}.button--archive{padding-left:40px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIwLjU0IDUuMjNMMTkuMTUgMy41NUMxOC44OCAzLjIxIDE4LjQ3IDMgMTggM0g2QzUuNTMgMyA1LjEyIDMuMjEgNC44NCAzLjU1TDMuNDYgNS4yM0MzLjE3IDUuNTcgMyA2LjAyIDMgNi41VjE5QzMgMjAuMSAzLjkgMjEgNSAyMUgxOUMyMC4xIDIxIDIxIDIwLjEgMjEgMTlWNi41QzIxIDYuMDIgMjAuODMgNS41NyAyMC41NCA1LjIzWk0xMiAxNy41TDYuNSAxMkgxMFYxMEgxNFYxMkgxNy41TDEyIDE3LjVaTTUuMTIgNUw1LjkzIDRIMTcuOTNMMTguODcgNUg1LjEyWiIgZmlsbD0iIzc1NzU3NSIvPgo8L3N2Zz4K);background-size:24px 24px;background-position:11px center;background-repeat:no-repeat}.button--restore{padding-left:40px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIwLjU1IDUuMjJMMTkuMTYgMy41NEMxOC44OCAzLjIxIDE4LjQ3IDMgMTggM0g2QzUuNTMgMyA1LjEyIDMuMjEgNC44NSAzLjU1TDMuNDYgNS4yMkMzLjE3IDUuNTcgMyA2LjAxIDMgNi41VjE5QzMgMjAuMSAzLjg5IDIxIDUgMjFIMTlDMjAuMSAyMSAyMSAyMC4xIDIxIDE5VjYuNUMyMSA2LjAxIDIwLjgzIDUuNTcgMjAuNTUgNS4yMlpNMTIgOS41TDE3LjUgMTVIMTRWMTdIMTBWMTVINi41TDEyIDkuNVpNNS4xMiA1TDUuOTQgNEgxNy45NEwxOC44NyA1SDUuMTJaIiBmaWxsPSIjNzU3NTc1Ii8+Cjwvc3ZnPgo=);background-size:24px 24px;background-position:11px center;background-repeat:no-repeat}.button--save{padding-left:40px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguOTk5OTcgMTYuMTY5OEw0LjgyOTk3IDExLjk5OThMMy40MDk5NyAxMy40MDk4TDguOTk5OTcgMTguOTk5OEwyMSA2Ljk5OTg0TDE5LjU5IDUuNTg5ODRMOC45OTk5NyAxNi4xNjk4WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==);background-size:24px 24px;background-position:11px center;background-repeat:no-repeat}*{box-sizing:border-box}:before{box-sizing:border-box}:after{box-sizing:border-box}.visuallyhidden{border:0;clip:rect(0 0 0 0);-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;white-space:nowrap}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;-webkit-clip-path:none;clip-path:none;height:auto;margin:0;overflow:visible;position:static;width:auto;white-space:inherit}.container{padding-left:24px;padding-right:24px}.header{padding-top:24px;padding-bottom:24px;background-color:#3671E3;box-shadow:0 2px 2px rgba(0, 0, 0, 0.24),0 0 2px rgba(0, 0, 0, 0.12);color:#FFF}.header__wrap{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.header__logo{margin-top:0;margin-bottom:0;border-right:1px solid #709AEE;padding-right:15px;font-weight:700;font-size:20px;line-height:24px;letter-spacing:.85px;text-transform:uppercase}.header__project{margin-left:-1px;border-left:1px solid #709AEE;padding-left:16px;font-weight:500;font-size:16px;line-height:24px}.status{background-size:16px 16px;background-repeat:no-repeat;background-position:left center;padding-left:21px;font-weight:700;font-size:12px;line-height:16px;letter-spacing:.4px;text-transform:uppercase}.status--success{color:#0F7F12;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuOTk5OTIgMS4zMzMwMUM0LjMxOTkyIDEuMzMzMDEgMS4zMzMyNSA0LjMxOTY3IDEuMzMzMjUgNy45OTk2OEMxLjMzMzI1IDExLjY3OTcgNC4zMTk5MiAxNC42NjYzIDcuOTk5OTIgMTQuNjY2M0MxMS42Nzk5IDE0LjY2NjMgMTQuNjY2NiAxMS42Nzk3IDE0LjY2NjYgNy45OTk2OEMxNC42NjY2IDQuMzE5NjcgMTEuNjc5OSAxLjMzMzAxIDcuOTk5OTIgMS4zMzMwMVpNNi42NjY1OSAxMS4zMzNMMy4zMzMyNSA3Ljk5OTY4TDQuMjczMjUgNy4wNTk2OEw2LjY2NjU5IDkuNDQ2MzRMMTEuNzI2NiA0LjM4NjM0TDEyLjY2NjYgNS4zMzMwMUw2LjY2NjU5IDExLjMzM1oiIGZpbGw9IiMwRjdGMTIiLz4KPC9zdmc+Cg==)}.status--pending{color:#3671E3;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjgyNjYgNS4xNzMwMUMxMC4wNDY2IDQuMzkzMDEgOS4wMjY1OSAzLjk5OTY3IDcuOTk5OTIgMy45OTk2N1Y3Ljk5OTY4TDUuMTczMjUgMTAuODI2M0M2LjczMzI1IDEyLjM4NjMgOS4yNjY1OSAxMi4zODYzIDEwLjgzMzMgMTAuODI2M0MxMi4zOTMzIDkuMjY2MzQgMTIuMzkzMyA2LjczMzAxIDEwLjgyNjYgNS4xNzMwMVpNNy45OTk5MiAxLjMzMzAxQzQuMzE5OTIgMS4zMzMwMSAxLjMzMzI1IDQuMzE5NjcgMS4zMzMyNSA3Ljk5OTY4QzEuMzMzMjUgMTEuNjc5NyA0LjMxOTkyIDE0LjY2NjMgNy45OTk5MiAxNC42NjYzQzExLjY3OTkgMTQuNjY2MyAxNC42NjY2IDExLjY3OTcgMTQuNjY2NiA3Ljk5OTY4QzE0LjY2NjYgNC4zMTk2NyAxMS42Nzk5IDEuMzMzMDEgNy45OTk5MiAxLjMzMzAxWk03Ljk5OTkyIDEzLjMzM0M1LjA1MzI1IDEzLjMzMyAyLjY2NjU5IDEwLjk0NjMgMi42NjY1OSA3Ljk5OTY4QzIuNjY2NTkgNS4wNTMwMSA1LjA1MzI1IDIuNjY2MzQgNy45OTk5MiAyLjY2NjM0QzEwLjk0NjYgMi42NjYzNCAxMy4zMzMzIDUuMDUzMDEgMTMuMzMzMyA3Ljk5OTY4QzEzLjMzMzMgMTAuOTQ2MyAxMC45NDY2IDEzLjMzMyA3Ljk5OTkyIDEzLjMzM1oiIGZpbGw9IiMzNjcxRTMiLz4KPC9zdmc+Cg==)}.status--failure{color:#FC5830;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAuNjY2NzQ4IDEzLjk5OTdIMTUuMzMzNEw4LjAwMDA4IDEuMzMzMDFMMC42NjY3NDggMTMuOTk5N1pNOC42NjY3NSAxMS45OTk3SDcuMzMzNDFWMTAuNjY2M0g4LjY2Njc1VjExLjk5OTdaTTguNjY2NzUgOS4zMzMwMUg3LjMzMzQxVjYuNjY2MzRIOC42NjY3NVY5LjMzMzAxWiIgZmlsbD0iI0ZDNTgzMCIvPgo8L3N2Zz4K)}.status--archived{color:#D42A2A;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjY5MzMgMy40ODY2N0wxMi43NjY3IDIuMzY2NjdDMTIuNTg2NyAyLjE0IDEyLjMxMzMgMiAxMiAySDRDMy42ODY2NyAyIDMuNDEzMzMgMi4xNCAzLjIyNjY3IDIuMzY2NjdMMi4zMDY2NyAzLjQ4NjY3QzIuMTEzMzMgMy43MTMzMyAyIDQuMDEzMzMgMiA0LjMzMzMzVjEyLjY2NjdDMiAxMy40IDIuNiAxNCAzLjMzMzMzIDE0SDEyLjY2NjdDMTMuNCAxNCAxNCAxMy40IDE0IDEyLjY2NjdWNC4zMzMzM0MxNCA0LjAxMzMzIDEzLjg4NjcgMy43MTMzMyAxMy42OTMzIDMuNDg2NjdaTTggMTEuNjY2N0w0LjMzMzMzIDhINi42NjY2N1Y2LjY2NjY3SDkuMzMzMzNWOEgxMS42NjY3TDggMTEuNjY2N1pNMy40MTMzMyAzLjMzMzMzTDMuOTUzMzMgMi42NjY2N0gxMS45NTMzTDEyLjU4IDMuMzMzMzNIMy40MTMzM1oiIGZpbGw9IiNENDJBMkEiLz4KPC9zdmc+Cg==)}.bar{display:inline-block;vertical-align:top;padding:4px 6px;background-color:#EEE;border-radius:4px;font-size:12px;line-height:16px;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar b{font-weight:500}.bar--larger{padding:4px 8px;font-size:16px;line-height:24px}.external{padding-right:24px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkuMTQyODEgOS4xNDM0MkgyLjg1NzA5VjIuODU3N0g1LjE0MjgxVjEuNzE0ODRIMi44NTcwOUMyLjIyMjgxIDEuNzE0ODQgMS43MTQyMyAyLjIyOTEzIDEuNzE0MjMgMi44NTc3VjkuMTQzNDJDMS43MTQyMyA5Ljc3MTk5IDIuMjIyODEgMTAuMjg2MyAyLjg1NzA5IDEwLjI4NjNIOS4xNDI4MUM5Ljc3MTM4IDEwLjI4NjMgMTAuMjg1NyA5Ljc3MTk5IDEwLjI4NTcgOS4xNDM0MlY2Ljg1NzdIOS4xNDI4MVY5LjE0MzQyWk02LjI4NTY2IDEuNzE0ODRWMi44NTc3SDguMzM3MDlMNC40MzQyMyA2Ljc2MDU2TDUuMjM5OTUgNy41NjYyN0w5LjE0MjgxIDMuNjYzNDJWNS43MTQ4NEgxMC4yODU3VjEuNzE0ODRINi4yODU2NloiIGZpbGw9IiMyMTIxMjEiLz4KPC9zdmc+Cg==);background-size:12px 12px;background-repeat:no-repeat;background-position:right 6px center;color:inherit;text-decoration:none;cursor:pointer}.external:hover{color:#3671E3;background-color:#E2E2E2;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkuMTQyODEgOS4xNDM0MkgyLjg1NzA5VjIuODU3N0g1LjE0MjgxVjEuNzE0ODRIMi44NTcwOUMyLjIyMjgxIDEuNzE0ODQgMS43MTQyMyAyLjIyOTEzIDEuNzE0MjMgMi44NTc3VjkuMTQzNDJDMS43MTQyMyA5Ljc3MTk5IDIuMjIyODEgMTAuMjg2MyAyLjg1NzA5IDEwLjI4NjNIOS4xNDI4MUM5Ljc3MTM4IDEwLjI4NjMgMTAuMjg1NyA5Ljc3MTk5IDEwLjI4NTcgOS4xNDM0MlY2Ljg1NzdIOS4xNDI4MVY5LjE0MzQyWk02LjI4NTY2IDEuNzE0ODRWMi44NTc3SDguMzM3MDlMNC40MzQyMyA2Ljc2MDU2TDUuMjM5OTUgNy41NjYyN0w5LjE0MjgxIDMuNjYzNDJWNS43MTQ4NEgxMC4yODU3VjEuNzE0ODRINi4yODU2NloiIGZpbGw9IiMzNjcxRTMgICIvPgo8L3N2Zz4K)}.external:active{color:#3671E3;background-color:#D6D6D6;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkuMTQyODEgOS4xNDM0MkgyLjg1NzA5VjIuODU3N0g1LjE0MjgxVjEuNzE0ODRIMi44NTcwOUMyLjIyMjgxIDEuNzE0ODQgMS43MTQyMyAyLjIyOTEzIDEuNzE0MjMgMi44NTc3VjkuMTQzNDJDMS43MTQyMyA5Ljc3MTk5IDIuMjIyODEgMTAuMjg2MyAyLjg1NzA5IDEwLjI4NjNIOS4xNDI4MUM5Ljc3MTM4IDEwLjI4NjMgMTAuMjg1NyA5Ljc3MTk5IDEwLjI4NTcgOS4xNDM0MlY2Ljg1NzdIOS4xNDI4MVY5LjE0MzQyWk02LjI4NTY2IDEuNzE0ODRWMi44NTc3SDguMzM3MDlMNC40MzQyMyA2Ljc2MDU2TDUuMjM5OTUgNy41NjYyN0w5LjE0MjgxIDMuNjYzNDJWNS43MTQ4NEgxMC4yODU3VjEuNzE0ODRINi4yODU2NloiIGZpbGw9IiMzNjcxRTMgICIvPgo8L3N2Zz4K)}.external.bar--larger{background-position:right 8px center}.expander{position:relative;border:none;background-color:transparent;padding:0 17px 0 0;font-family:inherit;font-weight:500;font-size:12px;line-height:16px;color:#3671E3;text-decoration:none;cursor:pointer}.expander:before{content:"";position:absolute;left:-4px;right:-4px;top:-10px;bottom:-10px;display:block}.expander:after{position:absolute;right:0;top:0;content:"";display:block;width:16px;height:16px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTQgNS43MjY1Nkw4IDguNzc5OUwxMS4wNiA1LjcyNjU2TDEyIDYuNjY2NTZMOCAxMC42NjY2TDQgNi42NjY1Nkw0Ljk0IDUuNzI2NTZaIiBmaWxsPSIjMzY3MUUzIi8+Cjwvc3ZnPgo=);background-repeat:no-repeat;background-position:center center}.expander:hover{color:#3A6CCC}.expander:active{color:#3964B8}.expander--open:after{-ms-transform:scaleY(-1);transform:scaleY(-1)}.expander--stand-alone{padding:9px 24px 9px 0;background-color:transparent;font-family:inherit;font-size:14px;line-height:16px;color:#3671E3;text-transform:uppercase;letter-spacing:.6px}.expander--stand-alone:after{right:0;top:7px;width:20px;height:20px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuMTc1IDcuMTU4MkwxMCAxMC45NzQ5TDEzLjgyNSA3LjE1ODJMMTUgOC4zMzMyTDEwIDEzLjMzMzJMNSA4LjMzMzJMNi4xNzUgNy4xNTgyWiIgZmlsbD0iIzM2NzFFMyIvPgo8L3N2Zz4K);background-size:contain}.expander--stand-alone:hover{background-color:transparent;color:#3A6CCC}.expander--stand-alone:hover:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuMTc1IDcuMTU4MkwxMCAxMC45NzQ5TDEzLjgyNSA3LjE1ODJMMTUgOC4zMzMyTDEwIDEzLjMzMzJMNSA4LjMzMzJMNi4xNzUgNy4xNTgyWiIgZmlsbD0iIzNBNkNDQyIvPgo8L3N2Zz4K)}.expander--stand-alone:active{background-color:transparent;color:#3964B8}.expander--stand-alone:active:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuMTc1IDcuMTU4MkwxMCAxMC45NzQ5TDEzLjgyNSA3LjE1ODJMMTUgOC4zMzMyTDEwIDEzLjMzMzJMNSA4LjMzMzJMNi4xNzUgNy4xNTgyWiIgZmlsbD0iIzM5NjRCOCIvPgo8L3N2Zz4K)}.action{display:inline-block;vertical-align:top;width:100%;border:none;padding:16px 32px 16px 16px;background-color:transparent;background-repeat:no-repeat;background-position:16px center;font-family:inherit;font-size:14px;line-height:16px;color:#000;white-space:nowrap;text-align:left;text-decoration:none;cursor:pointer}.action:not(.action--disabled):hover{background-color:#EEEEEE}.action:not(.action--disabled):active{background-color:#D6D6D6}.action--archive{padding-left:50px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIwLjU0IDUuMjNMMTkuMTUgMy41NUMxOC44OCAzLjIxIDE4LjQ3IDMgMTggM0g2QzUuNTMgMyA1LjEyIDMuMjEgNC44NCAzLjU1TDMuNDYgNS4yM0MzLjE3IDUuNTcgMyA2LjAyIDMgNi41VjE5QzMgMjAuMSAzLjkgMjEgNSAyMUgxOUMyMC4xIDIxIDIxIDIwLjEgMjEgMTlWNi41QzIxIDYuMDIgMjAuODMgNS41NyAyMC41NCA1LjIzWk0xMiAxNy41TDYuNSAxMkgxMFYxMEgxNFYxMkgxNy41TDEyIDE3LjVaTTUuMTIgNUw1LjkzIDRIMTcuOTNMMTguODcgNUg1LjEyWiIgZmlsbD0iIzc1NzU3NSIvPgo8L3N2Zz4K)}.action--edit{padding-left:50px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMTcuMjQ5NlYyMC45OTk2SDYuNzVMMTcuODEgOS45Mzk1N0wxNC4wNiA2LjE4OTU3TDMgMTcuMjQ5NlpNMjAuNzEgNy4wMzk1N0MyMS4xIDYuNjQ5NTcgMjEuMSA2LjAxOTU3IDIwLjcxIDUuNjI5NTdMMTguMzcgMy4yODk1N0MxNy45OCAyLjg5OTU3IDE3LjM1IDIuODk5NTcgMTYuOTYgMy4yODk1N0wxNS4xMyA1LjExOTU3TDE4Ljg4IDguODY5NTdMMjAuNzEgNy4wMzk1N1oiIGZpbGw9IiM3NTc1NzUiLz4KPC9zdmc+Cg==)}.action--restore{padding-left:50px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIwLjU1IDUuMjJMMTkuMTYgMy41NEMxOC44OCAzLjIxIDE4LjQ3IDMgMTggM0g2QzUuNTMgMyA1LjEyIDMuMjEgNC44NSAzLjU1TDMuNDYgNS4yMkMzLjE3IDUuNTcgMyA2LjAxIDMgNi41VjE5QzMgMjAuMSAzLjg5IDIxIDUgMjFIMTlDMjAuMSAyMSAyMSAyMC4xIDIxIDE5VjYuNUMyMSA2LjAxIDIwLjgzIDUuNTcgMjAuNTUgNS4yMlpNMTIgOS41TDE3LjUgMTVIMTRWMTdIMTBWMTVINi41TDEyIDkuNVpNNS4xMiA1TDUuOTQgNEgxNy45NEwxOC44NyA1SDUuMTJaIiBmaWxsPSIjNzU3NTc1Ii8+Cjwvc3ZnPgo=)}.action--logs{padding-left:50px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDE3SDRWMTlIMTRWMTdaTTIwIDlINFYxMUgyMFY5Wk00IDE1SDIwVjEzSDRWMTVaTTQgNVY3SDIwVjVINFoiIGZpbGw9IiM3NTc1NzUiLz4KPC9zdmc+Cg==)}.action--disabled{opacity:.5;cursor:default}.drop{position:relative;display:inline-block;vertical-align:top}.drop__handler{border:none;padding:0;background-color:transparent;cursor:pointer}.drop__dropdown{position:absolute;z-index:500;right:0;top:100%;min-width:100%;opacity:0;visibility:hidden;transition:opacity 0.1s cubic-bezier(0.075, 0.82, 0.165, 1),visibility 0.1s cubic-bezier(0.075, 0.82, 0.165, 1)}.drop--expanded .drop__dropdown{opacity:1;visibility:visible}.drop--actions .drop__handler{width:40px;height:40px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDhDMTMuMSA4IDE0IDcuMSAxNCA2QzE0IDQuOSAxMy4xIDQgMTIgNEMxMC45IDQgMTAgNC45IDEwIDZDMTAgNy4xIDEwLjkgOCAxMiA4Wk0xMiAxMEMxMC45IDEwIDEwIDEwLjkgMTAgMTJDMTAgMTMuMSAxMC45IDE0IDEyIDE0QzEzLjEgMTQgMTQgMTMuMSAxNCAxMkMxNCAxMC45IDEzLjEgMTAgMTIgMTBaTTEyIDE2QzEwLjkgMTYgMTAgMTYuOSAxMCAxOEMxMCAxOS4xIDEwLjkgMjAgMTIgMjBDMTMuMSAyMCAxNCAxOS4xIDE0IDE4QzE0IDE2LjkgMTMuMSAxNiAxMiAxNloiIGZpbGw9IiM3NTc1NzUiLz4KPC9zdmc+Cg==);background-size:24px 24px;background-repeat:no-repeat;background-position:center center;text-indent:200%;white-space:nowrap;overflow:hidden}.drop--actions .drop__handler:hover{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDhDMTMuMSA4IDE0IDcuMSAxNCA2QzE0IDQuOSAxMy4xIDQgMTIgNEMxMC45IDQgMTAgNC45IDEwIDZDMTAgNy4xIDEwLjkgOCAxMiA4Wk0xMiAxMEMxMC45IDEwIDEwIDEwLjkgMTAgMTJDMTAgMTMuMSAxMC45IDE0IDEyIDE0QzEzLjEgMTQgMTQgMTMuMSAxNCAxMkMxNCAxMC45IDEzLjEgMTAgMTIgMTBaTTEyIDE2QzEwLjkgMTYgMTAgMTYuOSAxMCAxOEMxMCAxOS4xIDEwLjkgMjAgMTIgMjBDMTMuMSAyMCAxNCAxOS4xIDE0IDE4QzE0IDE2LjkgMTMuMSAxNiAxMiAxNloiIGZpbGw9IiM0QzRDNEMiLz4KPC9zdmc+Cg==)}.drop--actions .drop__handler:active,.drop--actions.drop--expanded .drop__handler{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDhDMTMuMSA4IDE0IDcuMSAxNCA2QzE0IDQuOSAxMy4xIDQgMTIgNEMxMC45IDQgMTAgNC45IDEwIDZDMTAgNy4xIDEwLjkgOCAxMiA4Wk0xMiAxMEMxMC45IDEwIDEwIDEwLjkgMTAgMTJDMTAgMTMuMSAxMC45IDE0IDEyIDE0QzEzLjEgMTQgMTQgMTMuMSAxNCAxMkMxNCAxMC45IDEzLjEgMTAgMTIgMTBaTTEyIDE2QzEwLjkgMTYgMTAgMTYuOSAxMCAxOEMxMCAxOS4xIDEwLjkgMjAgMTIgMjBDMTMuMSAyMCAxNCAxOS4xIDE0IDE4QzE0IDE2LjkgMTMuMSAxNiAxMiAxNloiIGZpbGw9IiMyMTIxMjEiLz4KPC9zdmc+Cg==)}.drop--actions .drop__dropdown{margin-top:8px;margin-right:-17px;padding-top:8px;padding-bottom:8px;background-color:#FFF;box-shadow:0 2px 2px rgba(0, 0, 0, 0.24),0 0 2px rgba(0, 0, 0, 0.12);border-radius:2px}.input{position:relative}.input__widget{width:100%;height:40px;vertical-align:top;padding:8px 0;background-color:transparent;border:none;outline:0;box-shadow:inset 0 -1px #B0B0B0;background-clip:padding-box;border-radius:0;font-family:inherit;font-size:16px;line-height:24px;color:#000000;color:rgba(0,0,0,.87)}.input__widget:hover{box-shadow:inset 0 -1px #326EE6}.input__widget:focus{box-shadow:inset 0 -2px #326EE6}.input__widget::-webkit-input-placeholder{font-style:normal;color:#757575}.input__widget::-moz-placeholder{font-style:normal;color:#757575;opacity:1}.input__widget:-ms-input-placeholder{font-style:normal;color:#757575}.input__widget:hover::-webkit-input-placeholder{color:#B1B1B1}.input__widget:hover::-moz-placeholder{color:#B1B1B1}.input__widget:hover:-ms-input-placeholder{color:#B1B1B1}.input__widget:focus::-webkit-input-placeholder{color:#B1B1B1}.input__widget:focus::-moz-placeholder{color:#B1B1B1}.input__widget:focus:-ms-input-placeholder{color:#B1B1B1}.input--has-clear-type .input__widget{padding-right:30px}.input--has-clear-type .input__widget::-ms-clear{display:none}.input__clear-type{position:absolute;right:-8px;top:0}.input--search .input__widget{padding-left:32px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1LjUgMTRIMTQuNzFMMTQuNDMgMTMuNzNDMTUuNDEgMTIuNTkgMTYgMTEuMTEgMTYgOS41QzE2IDUuOTEgMTMuMDkgMyA5LjUgM0M1LjkxIDMgMyA1LjkxIDMgOS41QzMgMTMuMDkgNS45MSAxNiA5LjUgMTZDMTEuMTEgMTYgMTIuNTkgMTUuNDEgMTMuNzMgMTQuNDNMMTQgMTQuNzFWMTUuNUwxOSAyMC40OUwyMC40OSAxOUwxNS41IDE0Wk05LjUgMTRDNy4wMSAxNCA1IDExLjk5IDUgOS41QzUgNy4wMSA3LjAxIDUgOS41IDVDMTEuOTkgNSAxNCA3LjAxIDE0IDkuNUMxNCAxMS45OSAxMS45OSAxNCA5LjUgMTRaIiBmaWxsPSIjNzU3NTc1Ii8+Cjwvc3ZnPgo=);background-size:24px 24px;background-repeat:no-repeat;background-position:left center}.input--error .input__widget{box-shadow:inset 0 -2px #D42A2A}.input__output{margin-top:7px;font-size:12px;line-height:16px;color:#D42A2A}.input__output+.input__output{margin-top:3px}.spot{display:inline-block;vertical-align:top;border:none;background-color:transparent;background-repeat:no-repeat;background-position:center center;background-size:24px 24px;padding:0;width:40px;height:40px;cursor:pointer;text-indent:200%;white-space:nowrap;overflow:hidden;opacity:.5}.spot--cancel{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExLjk5OTggMkM2LjQ2OTgyIDIgMS45OTk4MiA2LjQ3IDEuOTk5ODIgMTJDMS45OTk4MiAxNy41MyA2LjQ2OTgyIDIyIDExLjk5OTggMjJDMTcuNTI5OCAyMiAyMS45OTk4IDE3LjUzIDIxLjk5OTggMTJDMjEuOTk5OCA2LjQ3IDE3LjUyOTggMiAxMS45OTk4IDJaTTE2Ljk5OTggMTUuNTlMMTUuNTg5OCAxN0wxMS45OTk4IDEzLjQxTDguNDA5ODIgMTdMNi45OTk4MiAxNS41OUwxMC41ODk4IDEyTDYuOTk5ODIgOC40MUw4LjQwOTgyIDdMMTEuOTk5OCAxMC41OUwxNS41ODk4IDdMMTYuOTk5OCA4LjQxTDEzLjQwOTggMTJMMTYuOTk5OCAxNS41OVoiIGZpbGw9IiM3NTc1NzUiLz4KPC9zdmc+Cg==)}.spot--cancel:hover{opacity:1}.title{font-weight:inherit;font-size:28px;line-height:32px}.page{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:calc(100% - 72px);padding-top:24px}.page__wrap{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:100%}.page__output{-ms-flex-negative:0;flex-shrink:0;margin-bottom:24px}.page__back{-ms-flex-item-align:start;align-self:flex-start;margin-top:-8px;margin-bottom:-8px}.page__head{-ms-flex-negative:0;flex-shrink:0;display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;margin-bottom:16px}.page__heading{margin-top:5px;margin-bottom:0}.page__note{margin-left:15px;margin-top:17px;font-size:14px;line-height:16px;color:#757575}.page__action{margin-left:auto}.page__action+.page__action{margin-left:8px}.page__action:first-child{margin-left:0}.page__action--search{margin-right:8px}.page__search{margin-left:auto;width:240px}.page__add-deployment{margin-left:16px}.page__body{-ms-flex-positive:1;flex-grow:1;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-bottom:24px}.table{background-color:#FFF;border:1px solid #E0E0E0;border-radius:4px}.table table{width:100%;vertical-align:top;border-collapse:collapse;table-layout:fixed;font-size:14px;line-height:20px;color:#000000;color:rgba(0,0,0,.87)}.table thead tr{background-color:#EEE}.table tbody tr{border-top:1px solid #E0E0E0}.table th{vertical-align:middle;height:48px;padding:16px 12px;font-weight:500;font-size:14px;line-height:16px;color:#000;text-align:left;overflow:hidden;text-overflow:ellipsis}.table td{vertical-align:top;padding:16px 12px}.table tr:first-child th:first-child{border-top-left-radius:3px}.table tr:first-child th:last-child{border-top-right-radius:3px}.table tr:last-child td:first-child{border-bottom-left-radius:3px}.table tr:last-child td:last-child{border-bottom-right-radius:3px}.table--clickable tbody tr{cursor:default}.table--clickable tbody tr:hover{background-color:#F8F8F8}.table--double-click tbody tr{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.table--deployments th:nth-child(1){width:16.1818%}.table--deployments th:nth-child(2){width:16.1818%}.table--deployments th:nth-child(3){width:8%}.table--deployments th:nth-child(4){width:20.3636%}.table--deployments th:nth-child(5){width:20.3636%}.table--deployments th:nth-child(6){width:9.4546%}.table--deployments th:nth-child(7){width:9.4546%}.table--deployments th:nth-child(8){width:52px}.table--deployments td:first-child,.table--deployments th:first-child{padding-left:16px}.table--deployments td:last-child,.table--deployments th:last-child{padding-right:16px}.table--deployments td:nth-child(1){font-weight:500;font-size:16px;line-height:24px}.table--deployments td:nth-child(1) .loading,.table--deployments td:nth-child(1) .status{margin-top:8px;display:block}.table--deployments td:nth-child(3){white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.table--deployments td:nth-child(6){white-space:nowrap}.table--deployments td:nth-child(7){white-space:nowrap}.table--deployments td:nth-child(8) .drop{margin:-8px}.table--actions th:nth-child(1){width:8%}.table--actions th:nth-child(2){width:20%}.table--actions th:nth-child(3){width:21%}.table--actions th:nth-child(4){width:20%}.table--actions th:nth-child(5){width:7%}.table--actions th:nth-child(6){width:15%}.table--actions th:nth-child(7){width:9%}.table--actions td:first-child,.table--actions th:first-child{padding-left:16px}.table--actions td:last-child,.table--actions th:last-child{padding-right:16px}.table--actions td:nth-child(1){font-weight:500;font-size:16px;line-height:24px}.table--actions td:nth-child(1) .loading,.table--actions td:nth-child(1) .status{margin-top:8px;display:block}.table--actions td:nth-child(2){word-break:break-all}.table--actions td:nth-child(5){white-space:nowrap}.table--actions td:nth-child(6) .drop{margin:-8px}.listing{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start}.listing__item.bar{margin-top:2px}.listing__item.bar--larger{margin-top:4px}.listing__item.bar:first-child{margin-top:0}.listing__more{margin-top:2px}.listing--for-text{display:block;font-size:12px;line-height:16px}.listing--for-text .listing__item{margin-top:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.listing--for-text .listing__item:first-child{margin-top:0}.listing--for-text b{font-weight:500}.listing--for-text .listing__more{margin-top:8px}.listing--for-text.listing--larger{font-size:16px;line-height:24px}.sort{position:relative;display:inline-block;vertical-align:top;border:none;background:0 0;margin:-8px;padding:8px;font-family:inherit;font-weight:500;font-size:14px;line-height:16px;text-decoration:underline;color:#000;cursor:pointer}.sort:after{position:absolute;right:12px;top:14px;content:"";display:block;border-left:4px solid transparent;border-right:4px solid transparent;opacity:0}.sort:hover{color:#3671E3}.sort--desc{padding-right:24px}.sort--desc:after{border-top:4px solid}.sort--asc{padding-right:24px}.sort--asc:after{border-bottom:4px solid}.sort--active:after{opacity:1}.notification{position:relative;padding:15px 46px 15px 16px;border:1px solid;border-radius:4px;font-size:14px;line-height:20px}.notification__close{position:absolute;right:0;top:0;bottom:0;width:46px;padding:0;border:none;background-color:transparent;background-size:24px 24px;background-repeat:no-repeat;background-position:center center;text-indent:200%;white-space:nowrap;overflow:hidden;cursor:pointer}.notification--success{background-color:#0f7f12;background-color:rgba(15,127,18,.1);color:#0F7F12}.notification--success .notification__close{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iIzBGN0YxMiIvPgo8L3N2Zz4K)}.notification--success:hover .notification__close{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iIzBDNjcwRSIvPgo8L3N2Zz4K)}.notification--success:active .notification__close{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iIzA4NTMwQSIvPgo8L3N2Zz4K)}.notification--danger{background-color:#d42a2a;background-color:rgba(212,42,42,.1);color:#D42A2A}.notification--danger .notification__close{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iI0Q0MkEyQSIvPgo8L3N2Zz4K)}.notification--danger:hover .notification__close{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iI0IxMUIxQiIvPgo8L3N2Zz4K)}.notification--danger:active .notification__close{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iIzhCMTAxMCIvPgo8L3N2Zz4K)}.null{display:inline-block;vertical-align:top}.null:before{content:"";display:block;margin:0 auto 22px;background-size:contain;background-repeat:no-repeat;background-position:center center}.null__heading{margin-top:0;margin-bottom:0;display:block;font-weight:700;font-size:16px;line-height:20px;text-align:center}.null__message{margin-top:14px;font-size:14px;line-height:16px;color:#757575;text-align:center}.null--data{padding-bottom:8px}.null--data:before{width:174px;height:120px;background-image:url(../images/null-data.png)}@media (-webkit-min-device-pixel-ratio:2), (min-resolution:2dppx){.null--data:before{background-image:url(../images/null-data@2x.png)}}.null--search:before{width:120px;height:120px;background-image:url(../images/null-search.png)}@media (-webkit-min-device-pixel-ratio:2), (min-resolution:2dppx){.null--search:before{background-image:url(../images/null-search@2x.png)}}.null--search .null__message{margin-top:6px}html{height:100%;min-width:1000px}.no-page{display:table;vertical-align:top;width:100%;height:calc(100% - 72px)}.no-page__inner{display:table-cell;vertical-align:middle;text-align:center;width:100%;height:100%;padding-top:40px;padding-bottom:40px}.popup{position:relative;z-index:550;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}.popup__overlay{position:fixed;z-index:600;left:0;top:0;right:0;bottom:0;background-color:#000000;background-color:rgba(0,0,0,.3);text-indent:200%;white-space:nowrap;overflow:hidden}.popup__body{position:fixed;z-index:800;right:0;top:0;bottom:0;width:1000px;background-color:#FFF}.popup__head{position:absolute;z-index:20;left:0;top:0;right:0;height:72px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;background-color:#FFF;box-shadow:0 2px 2px rgba(0, 0, 0, 0.24),0 0 2px rgba(0, 0, 0, 0.12)}.popup__close{display:inline-block;vertical-align:top;width:72px;height:72px;padding:0;border:none;background-color:transparent;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iIzc1NzU3NSIvPgo8L3N2Zz4K);background-size:24px 24px;background-repeat:no-repeat;background-position:center center;text-indent:100%;white-space:nowrap;overflow:hidden;cursor:pointer}.popup__close:hover{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iIzRDNEM0QyIvPgo8L3N2Zz4K)}.popup__close:active{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iIzIxMjEyMSIvPgo8L3N2Zz4K)}.popup__project{margin-top:0;margin-bottom:0;margin-right:32px;display:block;font-weight:inherit;font-size:24px;line-height:24px}.popup__operations{margin-left:auto;display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.popup__action{margin-left:8px}.popup__action:first-child{margin-left:0}.popup__menu{margin-left:8px;margin-right:10px}.popup__sub-head{position:absolute;z-index:10;left:0;top:72px;right:0;height:56px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;background-color:#FFF;box-shadow:inset 0 -1px 0 #D9D9D9}.popup__head~.popup__content{margin-top:72px;height:calc(100% - 72px);overflow-y:auto}.popup__head~.popup__sub-head~.popup__content{margin-top:128px;height:calc(100% - 128px);overflow-y:auto}.popup__sub-head .nav__link:first-child{border-left:8px solid transparent}.popup--visible{opacity:1;visibility:visible}@keyframes popup-body-slide{from{transform:translateX(20px)}to{transform:translateX(0)}}.popup--visible .popup__body{animation:popup-body-slide .3s ease}.deployment{padding:24px}.page__body .deployment{padding:0}.deployment__output{margin-bottom:32px}.deployment__summary{display:-ms-flexbox;display:flex;-ms-flex-item-align:start;align-self:flex-start}.deployment__stat{min-width:120px;margin-left:32px}.deployment__stat:first-child{margin-left:0}.deployment__param{display:block;margin-top:0;margin-bottom:0;font-weight:500;font-size:14px;line-height:16px}.deployment__value{margin-top:8px;font-size:16px;line-height:24px}.deployment__value .status{border-top:4px solid transparent;border-bottom:4px solid transparent}.deployment__section{margin-top:32px}.deployment__sub-heading{margin-top:0;margin-bottom:0;font-weight:500;font-size:14px;line-height:16px}.deployment__widget{margin-top:16px;font-size:16px;line-height:24px}.tag{width:400px}.overrides__item{margin-top:8px;display:-ms-flexbox;display:flex;-ms-flex-item-align:start;align-self:flex-start}.overrides__item:first-child{margin-top:0}.overrides__key{width:400px}.overrides__value{margin-left:16px;width:400px}.overrides__delete{margin-left:2px}.overrides__add{margin-top:4px}.dash{padding:12px 0 12px 24px;background-color:transparent;background-size:16px 16px;background-repeat:no-repeat;background-position:left center;border:none;font-family:inherit;font-weight:500;font-size:14px;line-height:16px;letter-spacing:.4px;text-decoration:none;text-transform:uppercase;color:#3671E3;cursor:pointer}.dash:hover{color:#3A6CCC}.dash:active{color:#3964B8}.dash--smaller{font-size:12px;padding-left:20px}.dash--add{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iOCIgZmlsbD0iIzM2NzFFMyIvPgo8cGF0aCBkPSJNMTEuNSA4LjVIOC40OTk5OFYxMS41SDcuNDk5OThWOC41SDQuNDk5OThWNy41SDcuNDk5OThWNC41SDguNDk5OThWNy41SDExLjVWOC41WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==)}.dash--add:not(.dash--disabled):hover{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iOCIgZmlsbD0iIzNBNkNDQyIvPgo8cGF0aCBkPSJNMTEuNSA4LjVIOC40OTk5OFYxMS41SDcuNDk5OThWOC41SDQuNDk5OThWNy41SDcuNDk5OThWNC41SDguNDk5OThWNy41SDExLjVWOC41WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==)}.dash--add:not(.dash--disabled):active{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iOCIgZmlsbD0iIzM5NjRCOCIvPgo8cGF0aCBkPSJNMTEuNSA4LjVIOC40OTk5OFYxMS41SDcuNDk5OThWOC41SDQuNDk5OThWNy41SDcuNDk5OThWNC41SDguNDk5OThWNy41SDExLjVWOC41WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==)}.dash--back{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTciIHZpZXdCb3g9IjAgMCAxNiAxNyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjMzMzMgNy43NDI4NEg1LjIyTDguOTQ2NjYgNC4wMTYxN0w4IDMuMDc2MTdMMi42NjY2NiA4LjQwOTVMOCAxMy43NDI4TDguOTQgMTIuODAyOEw1LjIyIDkuMDc2MTdIMTMuMzMzM1Y3Ljc0Mjg0WiIgZmlsbD0iIzM2NzFFMyIvPgo8L3N2Zz4K)}.dash--back:not(.dash--disabled):hover{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTciIHZpZXdCb3g9IjAgMCAxNiAxNyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbD0iIzNBNkNDQyIgZD0iTTEzLjMzMzMgNy43NDI4NEg1LjIyTDguOTQ2NjYgNC4wMTYxN0w4IDMuMDc2MTdMMi42NjY2NiA4LjQwOTVMOCAxMy43NDI4TDguOTQgMTIuODAyOEw1LjIyIDkuMDc2MTdIMTMuMzMzM1Y3Ljc0Mjg0WiIvPgo8L3N2Zz4K)}.dash--back:not(.dash--disabled):active{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTciIHZpZXdCb3g9IjAgMCAxNiAxNyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbD0iIzM5NjRCOCIgZD0iTTEzLjMzMzMgNy43NDI4NEg1LjIyTDguOTQ2NjYgNC4wMTYxN0w4IDMuMDc2MTdMMi42NjY2NiA4LjQwOTVMOCAxMy43NDI4TDguOTQgMTIuODAyOEw1LjIyIDkuMDc2MTdIMTMuMzMzM1Y3Ljc0Mjg0WiIvPgo8L3N2Zz4K)}.dash--next{padding-left:0;padding-right:20px;background-position:right center}.dash--next{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICAgIDxwb2x5Z29uIGZpbGw9IiMzNjcxRTMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgcG9pbnRzPSI0LjYgMy40IDkuMiA4IDQuNiAxMi42IDYgMTQgMTIgOCA2IDIiLz4KPC9zdmc+Cg==)}.dash--next:not(.dash--disabled):hover{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICAgIDxwb2x5Z29uIGZpbGw9IiMzQTZDQ0MiIGZpbGwtcnVsZT0iZXZlbm9kZCIgcG9pbnRzPSI0LjYgMy40IDkuMiA4IDQuNiAxMi42IDYgMTQgMTIgOCA2IDIiLz4KPC9zdmc+Cg==)}.dash--next:not(.dash--disabled):active{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICAgIDxwb2x5Z29uIGZpbGw9IiMzOTY0QjgiIGZpbGwtcnVsZT0iZXZlbm9kZCIgcG9pbnRzPSI0LjYgMy40IDkuMiA4IDQuNiAxMi42IDYgMTQgMTIgOCA2IDIiLz4KPC9zdmc+Cg==)}.dash--disabled{opacity:.5;cursor:default}.log{margin-top:0;margin-bottom:0;display:block;padding:24px;font-family:"Roboto Mono",monospace;font-size:12px;line-height:16px;word-break:break-all}.no-deployment{height:100%;padding-bottom:72px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.data{-ms-flex-negative:0;flex-shrink:0}.data__show-archive{margin-top:18px;-ms-flex-item-align:start;align-self:flex-start}.data__archive{display:none}.data__archive--open{display:block}.no-data{-ms-flex-positive:1;flex-grow:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;padding-top:50px;padding-bottom:40px}.html-lock{overflow:hidden}.classic-popup{position:fixed;z-index:800;left:0;top:0;width:100%;height:100%;overflow:auto;background:#000000;background:rgba(0,0,0,.5);display:none}.classic-popup__container{display:table;table-layout:fixed;width:100%;height:100%}.classic-popup__viewport{display:table-cell;width:100%;height:100%;text-align:center;vertical-align:middle}.classic-popup__slot{position:relative;display:inline-block;vertical-align:top;text-align:left;margin:20px}.classic-popup__close{position:absolute;right:16px;top:16px;width:40px;height:40px;background-color:transparent;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDYuNDFMMTcuNTkgNUwxMiAxMC41OUw2LjQxIDVMNSA2LjQxTDEwLjU5IDEyTDUgMTcuNTlMNi40MSAxOUwxMiAxMy40MUwxNy41OSAxOUwxOSAxNy41OUwxMy40MSAxMkwxOSA2LjQxWiIgZmlsbD0iYmxhY2siLz4KPC9zdmc+Cg==);background-size:24px 24px;background-repeat:no-repeat;background-position:center center;padding:0;border:none;text-indent:200%;white-space:nowrap;overflow:hidden;cursor:pointer}.dialog{width:400px;padding:40px 24px;background-color:#FFF;border-radius:16px}.dialog--archive:before{content:"";display:block;width:185px;height:120px;margin-left:auto;margin-right:auto;background-image:url(../images/dialog-delete.png);background-size:contain;background-repeat:no-repeat;background-position:center center}@media (-webkit-min-device-pixel-ratio:2), (min-resolution:2dppx){.dialog--archive:before{background-image:url(../images/dialog-delete@2x.png)}}.dialog__content{margin-top:25px;font-weight:700;font-size:16px;line-height:20px;text-align:center}.dialog__footer{margin-top:16px;display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.dialog__action{margin-left:8px}.dialog__action:first-child{margin-left:0}.no-table{background-color:transparent!important;cursor:auto!important}.no-table td{vertical-align:middle;text-align:center;height:352px;padding-top:50px;padding-bottom:50px}.no-table .loading{display:inline-block!important;vertical-align:top!important;margin-top:0!important}.loading{position:relative;display:inline-block;vertical-align:top;padding-left:21px;font-size:14px;line-height:16px;color:#757575}@keyframes loading{to{transform:rotate(360deg)}}.loading:before{content:"";position:absolute;left:0;top:0;bottom:0;right:auto;margin:auto;display:block;width:16px;height:16px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgOEMyIDExLjMxMzcgNC42ODYyOSAxNCA4IDE0QzExLjMxMzcgMTQgMTQgMTEuMzEzNyAxNCA4QzE0IDQuNjg2MjkgMTEuMzEzNyAyIDggMiIgc3Ryb2tlPSIjNzU3NTc1IiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==);background-size:contain;background-repeat:no-repeat;background-position:center center;animation:loading 2.2s linear infinite}.loading--status-alike{font-weight:700;font-size:12px;letter-spacing:.4px;text-transform:uppercase}.loading--alternate:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgOEMyIDExLjMxMzcgNC42ODYyOSAxNCA4IDE0QzExLjMxMzcgMTQgMTQgMTEuMzEzNyAxNCA4QzE0IDQuNjg2MjkgMTEuMzEzNyAyIDggMiIgc3Ryb2tlPSIjMzY3MUUzIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==)}.loading--enlarged{padding-left:0;padding-top:66px;padding-bottom:8px;width:62px;text-align:center}.loading--enlarged:before{left:0;top:0;bottom:auto;right:0;width:52px;height:52px;background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTIiIGhlaWdodD0iNTIiIHZpZXdCb3g9IjAgMCA1MiA1MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgMjZDMiAzOS4yNTQ4IDEyLjc0NTIgNTAgMjYgNTBDMzkuMjU0OCA1MCA1MCAzOS4yNTQ4IDUwIDI2QzUwIDEyLjc0NTIgMzkuMjU0OCAyIDI2IDIiIHN0cm9rZT0iIzc1NzU3NSIgc3Ryb2tlLXdpZHRoPSI0Ii8+Cjwvc3ZnPgo=)}.loading--alternate.loading--enlarged:before{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTIiIGhlaWdodD0iNTIiIHZpZXdCb3g9IjAgMCA1MiA1MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIgMjZDMiAzOS4yNTQ4IDEyLjc0NTIgNTAgMjYgNTBDMzkuMjU0OCA1MCA1MCAzOS4yNTQ4IDUwIDI2QzUwIDEyLjc0NTIgMzkuMjU0OCAyIDI2IDIiIHN0cm9rZT0iIzM2NzFFMyIgc3Ryb2tlLXdpZHRoPSI0Ii8+Cjwvc3ZnPgo=)}.loading--indicator-only{width:16px;height:16px;padding:0;text-indent:200%;white-space:nowrap;overflow:hidden}.loading--indicator-only.loading--enlarged{width:52px;height:52px}.no-log{height:100%;padding-bottom:72px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.nav{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:start;align-items:flex-start}.nav__link{position:relative;display:inline-block;vertical-align:top;background-color:transparent;border:none;padding:21px 16px 19px;font-family:inherit;font-size:14px;line-height:16px;letter-spacing:.75px;text-transform:uppercase;cursor:pointer}.nav__link:hover{color:#3964B8}.nav__link--current{font-weight:500;color:#3964B8}.nav__link--current:after{content:"";position:absolute;left:16px;right:16px;bottom:0;display:block;height:4px;border-top-left-radius:4px;border-top-right-radius:4px;background-color:#3964B8}.break{word-break:break-all} \ No newline at end of file diff --git a/octopod-css/production/vendors/jquery/jquery-3.2.1.min.js b/octopod-css/production/vendors/jquery/jquery-3.2.1.min.js new file mode 100755 index 0000000..644d35e --- /dev/null +++ b/octopod-css/production/vendors/jquery/jquery-3.2.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + + +

Fork of outline.js

+ + Test link + +

+ + + +

+ + Test span + +

+ + + /must have it's own focus effect because otline is lost on click + +

+ + + + /need to make an outline for this thing using CSS and it should be possible to cancel it on click using this library + + diff --git a/octopod-css/production/vendors/outline/outline.js b/octopod-css/production/vendors/outline/outline.js new file mode 100755 index 0000000..76ec6eb --- /dev/null +++ b/octopod-css/production/vendors/outline/outline.js @@ -0,0 +1,37 @@ +// outline.js +// based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/ +// original: https://github.com/lindsayevans/outline.js +// this one is a fork: https://github.com/shchukin/outline.js + +(function(d){ + + var style_element = d.createElement('STYLE'), + dom_events = 'addEventListener' in d, + add_event_listener = function(type, callback){ + // Basic cross-browser event handling + if(dom_events){ + d.addEventListener(type, callback); + }else{ + d.attachEvent('on' + type, callback); + } + }, + set_css = function(css_text){ + // Handle setting of