git.iko.soy/typeable/octopod
on master help sign in
229 files changed+8470−4877
since splitsnapshots
unifiedsplit
.github/actions/common_setup/action.yaml+40 .github/actions/push_docker_images/action.yaml+26 .github/workflows/build.yaml+65−84 .github/workflows/check_documentation.yaml+1−1 .gitignore+1 .hlint.yaml+55 .nova/Configuration.json+5 .vscode/settings.json+6−1 Development_guide.md+5−12 Makefile+3 README.md+77−15 Release_checklist.md+5−18 build.sh−60 cabal.project+6 charts/octopod/Chart.yaml+3−3 charts/octopod/README.md+28−11 charts/octopod/index.yaml+37 charts/octopod/templates/NOTES.txt+9−5 charts/octopod/templates/ingress-app.yaml+11−5 charts/octopod/templates/ingress-powerapp.yaml+11−5 charts/octopod/templates/ingress-ui.yaml+11−5 charts/octopod/templates/ingress-ws.yaml+11−5 charts/octopod/templates/nginx-configmap.yaml+5−5 charts/octopod/templates/octopod-configmap.yaml+6−1 charts/octopod/values.yaml+6−6 default.nix+28−5 dev/default.nix+72−10 docs/Makefile−9 docs/README.md+6−6 docs/diagrams/images/app-architecture.png docs/diagrams/images/technical-architecture-archive-via-cli.png docs/diagrams/images/technical-architecture-archive-via-ui.png docs/diagrams/images/technical-architecture-archive.png docs/diagrams/images/technical-architecture-cleanup-via-cli.png docs/diagrams/images/technical-architecture-cleanup-via-ui.png docs/diagrams/images/technical-architecture-cleanup.png docs/diagrams/images/technical-architecture-create-via-cli.png docs/diagrams/images/technical-architecture-create-via-ui.png docs/diagrams/images/technical-architecture-create.png docs/diagrams/images/technical-architecture-deployment-states-fsm.png docs/diagrams/images/technical-architecture-restore-via-cli.png docs/diagrams/images/technical-architecture-restore-via-ui.png docs/diagrams/images/technical-architecture-restore.png docs/diagrams/images/technical-architecture-update-via-cli.png docs/diagrams/images/technical-architecture-update-via-ui.png docs/diagrams/images/technical-architecture-update.png docs/diagrams/src/technical-architecture-archive-via-cli.mmd−37 docs/diagrams/src/technical-architecture-archive-via-ui.mmd−36 docs/diagrams/src/technical-architecture-archive.mmd+31 docs/diagrams/src/technical-architecture-cleanup-via-cli.mmd−20 docs/diagrams/src/technical-architecture-cleanup-via-ui.mmd−19 docs/diagrams/src/technical-architecture-cleanup.mmd+14 docs/diagrams/src/technical-architecture-create-via-cli.mmd−41 docs/diagrams/src/technical-architecture-create-via-ui.mmd−40 docs/diagrams/src/technical-architecture-create.mmd+41 docs/diagrams/src/technical-architecture-deployment-states-fsm.mmd+1 docs/diagrams/src/technical-architecture-restore-via-cli.mmd−41 docs/diagrams/src/technical-architecture-restore-via-ui.mmd−40 docs/diagrams/src/technical-architecture-restore.mmd+42 docs/diagrams/src/technical-architecture-update-via-cli.mmd−41 docs/diagrams/src/technical-architecture-update-via-ui.mmd−40 docs/diagrams/src/technical-architecture-update.mmd+46 docs/en/Control_scripts.md+141−167 docs/en/Helm-based_deployment_guide.md+29−24 docs/en/Integration.md+9−8 docs/en/Octo_user_guide.md+31−76 docs/en/Octopod_deployment_guide.md+50−9 docs/en/Overview.md+12−22 docs/en/PM_case_study.md+9−1 docs/en/README.md+1−1 docs/en/Security_model.md+10−13 docs/en/Tech_case_study.md+9−16 docs/en/Technical_architecture.md+18−169 docs/images/dev2.png docs/images/octopod_app_keys_list.png docs/images/octopod_archive.png docs/images/octopod_blank.png docs/images/octopod_deployment_blank.png docs/images/octopod_deployment_filled.png docs/images/octopod_in_octopod_deployment.png docs/images/wordpress_blank.png docs/ru/Control_scripts.md−241 docs/ru/Helm-based_deployment_guide.md−73 docs/ru/Octo_user_guide.md−173 docs/ru/Octopod_deployment_with_K8S.md−30 docs/ru/README.md−8 docs/ru/Security_model.md−120 docs/ru/Technical_architecture.md−275 helm-control-scripts/Cargo.lock+459−168 helm-control-scripts/Cargo.toml+28−4 helm-control-scripts/Dockerfile+6−5 helm-control-scripts/README.md+40−7 helm-control-scripts/src/app_keys.rs+31 helm-control-scripts/src/app_overrides.rs+11 helm-control-scripts/src/archive.rs+33−12 helm-control-scripts/src/archive_check.rs+39−17 helm-control-scripts/src/check.rs+11−17 helm-control-scripts/src/cleanup.rs+57−7 helm-control-scripts/src/tag_check.rs → helm-control-scripts/src/config_check.rs+25−20 helm-control-scripts/src/create_update.rs+8−13 helm-control-scripts/src/deployment_keys.rs+11 helm-control-scripts/src/deployment_overrides.rs+11 helm-control-scripts/src/info.rs+11−17 helm-control-scripts/src/lib.rs+583−187 helm-control-scripts/src/unarchive.rs+53 img/demo.gif img/logo.svg+1−1 img/release_workflow.png img/typeable.png img/typeable_logo.svg+1 migrations/deploy/move_tag_to_overrides.sql+18 migrations/revert/move_tag_to_overrides.sql+62 migrations/sqitch.plan+1 migrations/verify/move_tag_to_overrides.sql+7 nix/ci.nix+6 nix/default.nix+1−1 nix/sources.json+11−11 octo-cli/Setup.hs+1 octo-cli/octo-cli.cabal+1−1 octo-cli/src/Octopod/CLI.hs+113−102 octo-cli/src/Octopod/CLI/Args.hs+23−34 octo-cli/src/Text/Layout/Table/Extras.hs+11−9 octopod-api/Setup.hs+1 octopod-api/src/Octopod/API.hs+1−1 octopod-api/src/Octopod/API/WebSocket.hs+1 octopod-api/src/Octopod/PowerAPI.hs+26−27 octopod-backend/Setup.hs+2−1 octopod-backend/app/Main.hs+5−1 octopod-backend/octopod-backend.cabal+11−3 octopod-backend/src/Control/CacheMap.hs+89 octopod-backend/src/Control/Octopod/DeploymentLock.hs+42−47 octopod-backend/src/Octopod/Schema.hs−4 octopod-backend/src/Octopod/Server.hs+478−445 octopod-backend/src/Octopod/Server/Args.hs+2−1 octopod-backend/src/Octopod/Server/ControlScriptUtils.hs+94−69 octopod-backend/src/Octopod/Server/Logger.hs−42 octopod-backend/src/Octopod/Server/Logging.hs+121 octopod-backend/src/Octopod/Server/Posix.hs+22−17 octopod-backend/src/Orphans.hs−3 octopod-backend/src/Types.hs+3−5 octopod-common/Setup.hs+2−1 octopod-common/octopod-common.cabal+13−1 octopod-common/src/Common/Orphans.hs+20 octopod-common/src/Common/Types.hs+128−74 octopod-common/src/Common/Utils.hs+1 octopod-common/src/Control/Searchable.hs+66 octopod-common/src/Data/ConfigTree.hs+165 octopod-common/src/Data/Map/Ordered/Strict/Extra.hs−15 octopod-css/default.nix+2 octopod-css/development/layouts/index.html+321−174 octopod-css/development/layouts/staging.html+687−168 octopod-css/development/markups/_action.html+2−1 octopod-css/development/markups/_bar.html+2−1 octopod-css/development/markups/_break.html+2−1 octopod-css/development/markups/_button.html+2−1 octopod-css/development/markups/_collapse.html+277 octopod-css/development/markups/_dash.html+3−2 octopod-css/development/markups/_drop.html+2−1 octopod-css/development/markups/_expander.html+2−1 octopod-css/development/markups/_external.html+2−1 octopod-css/development/markups/_input.html+36−1 octopod-css/development/markups/_keys-and-values.html+373 octopod-css/development/markups/_listing.html+2−1 octopod-css/development/markups/_loading.html+2−1 octopod-css/development/markups/_notification.html+2−1 octopod-css/development/markups/_null.html+2−1 octopod-css/development/markups/_spot.html+2−1 octopod-css/development/markups/_status.html+2−1 octopod-css/development/markups/_title.html+2−1 octopod-css/development/scripts/_collapse.js+18 octopod-css/development/styles/_button.css+27 octopod-css/development/styles/_collapse.css+95 octopod-css/development/styles/_deployment.css+4 octopod-css/development/styles/_editable-row.css+49 octopod-css/development/styles/_has-changes.css+3 octopod-css/development/styles/_input.css+80−2 octopod-css/development/styles/_key-custom-edited.css+4 octopod-css/development/styles/_key-default-edited.css+5 octopod-css/development/styles/_key-default-pristine.css+4 octopod-css/development/styles/_key-deleted.css+6 octopod-css/development/styles/_listing.css+73−1 octopod-css/development/styles/_overrides.css+7−20 octopod-css/development/styles/_padded.css+3 octopod-css/development/styles/_root.css+2 octopod-css/development/styles/_row.css+3 octopod-css/development/styles/_spot.css+11 octopod-css/development/styles/_table.css+21−26 octopod-css/development/styles/_value-deleted.css+5 octopod-css/development/styles/_value-edited.css+4 octopod-css/development/styles/_value-pristine.css+3 octopod-css/development/styles/_value-unknown.css+12 octopod-css/development/styles/style.css+15 octopod-css/development/vectors/button-loader.svg+5 octopod-css/development/vectors/collapse-project.svg+3 octopod-css/development/vectors/spot-loader.svg+3 octopod-css/development/vectors/spot-undo.svg+3 octopod-css/package.json+2−2 octopod-css/yarn.lock+67−67 octopod-frontend/Setup.hs+2−1 octopod-frontend/index.html+1 octopod-frontend/octopod-frontend.cabal+28 octopod-frontend/src/Data/Text/Search.hs+136−37 octopod-frontend/src/Data/WorkingOverrides.hs+118−70 octopod-frontend/src/Frontend/API.hs+1−6 octopod-frontend/src/Frontend/Classes.hs+18 octopod-frontend/src/Frontend/Route.hs+1−1 octopod-frontend/src/Frontend/UIKit.hs+555 octopod-frontend/src/Frontend/UIKit/Button/Action.hs+63 octopod-frontend/src/Frontend/UIKit/Button/Common.hs+101 octopod-frontend/src/Frontend/UIKit/Button/Dash.hs+71 octopod-frontend/src/Frontend/UIKit/Button/Expander.hs+86 octopod-frontend/src/Frontend/UIKit/Button/Large.hs+96 octopod-frontend/src/Frontend/UIKit/Button/Sort.hs+58 octopod-frontend/src/Frontend/UIKit/Button/Static.hs+84 octopod-frontend/src/Frontend/UIKit/Button/Tree.hs+84 octopod-frontend/src/Frontend/Utils.hs+255−479 octopod-frontend/src/Main.hs+3−3 octopod-frontend/src/Page/ClassicPopup.hs+16−6 octopod-frontend/src/Page/Deployment.hs+91−100 octopod-frontend/src/Page/Deployments.hs+103−166 octopod-frontend/src/Page/Elements/Links.hs+20−11 octopod-frontend/src/Page/Popup/EditDeployment.hs+27−13 octopod-frontend/src/Page/Popup/NewDeployment.hs+54−33 octopod-frontend/src/Reflex/Dom/AsyncEvent.hs+30 octopod-frontend/src/Reflex/Dom/Renderable.hs+35 octopod-frontend/src/Reflex/MultiEventWriter/Class.hs+10−8 octopod-frontend/src/Servant/Reflex/Extra.hs+18−14 octopod_local_install.sh+38−8 stack.yaml+3−1
.github/actions/common_setup/action.yamlnew file+40
@@ -0,0 +1,40 @@
1+name: "Common setup"
2+description: "Common setup steps"
3+inputs:
4+ authToken:
5+ description: "Cachix auth token"
6+ required: true
7+runs:
8+ using: "composite"
9+ steps:
10+ - name: Install skopeo
11+ shell: bash
12+ run: |
13+ sudo apt-get -y update
14+ sudo apt-get -y install skopeo
15+
16+ - name: Increase swap
17+ shell: bash
18+ run: |
19+ free -h
20+
21+ sudo swapoff /mnt/swapfile
22+ sudo fallocate -l 12G /mnt/swapfile
23+ sudo mkswap /mnt/swapfile
24+ sudo swapon /mnt/swapfile
25+
26+ free -h
27+
28+ - name: Install Nix
29+ uses: cachix/install-nix-action@v16
30+ with:
31+ extra_nix_config: |
32+ trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
33+ substituters = https://hydra.iohk.io https://cache.nixos.org/
34+ system-features = kvm
35+
36+ - name: Login to Cachix
37+ uses: cachix/cachix-action@v10
38+ with:
39+ name: typeable
40+ authToken: ${{ inputs.authToken }}
.github/actions/push_docker_images/action.yamlnew file+26
@@ -0,0 +1,26 @@
1+name: "Push Docker Images"
2+description: "Push Docker Images"
3+inputs:
4+ tag:
5+ description: "The docker image tag to push."
6+ required: true
7+ docker_username:
8+ description: "The Docker Hub username"
9+ required: true
10+ docker_password:
11+ description: "The Docker Hub password"
12+ required: true
13+runs:
14+ using: "composite"
15+ steps:
16+ - name: Login to DockerHub
17+ uses: docker/login-action@v1.10.0
18+ with:
19+ username: ${{ inputs.docker_username }}
20+ password: ${{ inputs.docker_password }}
21+
22+ - name: Push Docker Images to DockerHub
23+ shell: bash
24+ run: |
25+ skopeo copy docker-archive:$(nix-build ./nix -j auto -A octopod-server-container) docker://typeable/octopod:${{ inputs.tag }}
26+ skopeo copy docker-archive:$(nix-build ./nix -j auto -A octo-cli-container) docker://typeable/octo:${{ inputs.tag }}
.github/workflows/build.yaml+65−84
@@ -1,4 +1,4 @@
1−name: Build
1+name: Build and release
22on:
33 push:
44 branches:
@@ -10,8 +10,28 @@ on:
1010 - master
1111 - develop
1212 - staging
13+ workflow_dispatch:
14+ inputs:
15+ release:
16+ description: Release the new version?
17+ required: false
18+ default: "true"
19+ version:
20+ description: The version to release
21+ required: true
1322
1423jobs:
24+ check-formatting:
25+ name: Check Haskell formatting
26+ runs-on: ubuntu-latest
27+ steps:
28+ - uses: actions/checkout@v2
29+ - uses: cachix/install-nix-action@v12
30+ - name: Run fourmolu
31+ run: |
32+ nix-shell nix/ci.nix -j auto --run "git ls-files '*.hs' | xargs fourmolu -m inplace --ghc-opt -XRecursiveDo --ghc-opt -XTypeApplications --ghc-opt -XPatternSynonyms"
33+ git diff --exit-code
34+
1535 octo-macOS:
1636 name: macOS octo CLI Release (Stack)
1737 runs-on: macos-latest
@@ -37,17 +57,12 @@ jobs:
3757 timeout-minutes: 600
3858 steps:
3959 - uses: actions/checkout@v2
40− - uses: cachix/install-nix-action@v12
60+ - name: Common setup
61+ uses: ./.github/actions/common_setup
4162 with:
42− extra_nix_config: |
43− trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
44− substituters = https://hydra.iohk.io https://cache.nixos.org/
45− - uses: cachix/cachix-action@v8
46− with:
47− name: octopod
48− signingKey: "${{ secrets.CACHIX_SIGNING_KEY }}"
63+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
4964 - name: Build
50− run: nix-build -A projectCross.musl64.hsPkgs.octo-cli.components.exes.octo --arg prod true
65+ run: nix-build -A projectCross.musl64.hsPkgs.octo-cli.components.exes.octo --arg prod true -j auto
5166 - uses: actions/upload-artifact@v2
5267 with:
5368 name: octo-cli-linux
@@ -57,77 +72,28 @@ jobs:
5772 runs-on: ubuntu-latest
5873 timeout-minutes: 600
5974 steps:
60− - name: Increase swap
61− run: |
62− free -h
63−
64− sudo swapoff /mnt/swapfile
65− sudo fallocate -l 12G /mnt/swapfile
66− sudo mkswap /mnt/swapfile
67− sudo swapon /mnt/swapfile
68−
69− free -h - uses: actions/checkout@v2
70−
7175 - uses: actions/checkout@v2
7276
73− - name: Install Nix
74− uses: cachix/install-nix-action@v12
75− with:
76− extra_nix_config: |
77− trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
78− substituters = https://hydra.iohk.io https://cache.nixos.org/
79−
80− - name: Login to Cachix
81− uses: cachix/cachix-action@v8
77+ - name: Common setup
78+ uses: ./.github/actions/common_setup
8279 with:
83− name: octopod
84− signingKey: ${{ secrets.CACHIX_SIGNING_KEY }}
80+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
8581
8682 - name: Build Docker Images
87− run: |
88− # enable required features (see https://github.com/cachix/install-nix-action/issues/19)
89− mkdir -p ~/.config/nix
90− echo "system-features = kvm" >> ~/.config/nix/nix.conf
91−
92− # build docker images
93− ./build.sh build
94−
95− - name: Login to DockerHub
96− id: login-docker-hub
97− if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/develop'
98− uses: docker/login-action@v1
99− with:
100− username: ${{ secrets.DOCKERHUB_USERNAME }}
101− password: ${{ secrets.DOCKERHUB_TOKEN }}
102−
103− - name: Push Docker Images to DockerHub
104− if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/staging'
105− run: |
106− # push docker images to DockerHub
107− image_name=`cat octo-docker | docker load | awk '{print $3}'`
108− docker tag $image_name typeable/octo:latest
109− docker push typeable/octo:latest
110−
111− image_name=`cat octopod-server-docker | docker load | awk '{print $3}'`
112− docker tag $image_name typeable/octopod:latest
113− docker push typeable/octopod:latest
83+ run: nix-build ./nix -j auto
11484
11585 - name: Push Nightly Docker Images to DockerHub
116− if: github.ref == 'refs/heads/develop'
117− run: |
118− # push docker images to DockerHub
119− image_name=`cat octo-docker | docker load | awk '{print $3}'`
120− docker tag $image_name typeable/octo:nightly
121− docker push typeable/octo:nightly
122−
123− image_name=`cat octopod-server-docker | docker load | awk '{print $3}'`
124− docker tag $image_name typeable/octopod:nightly
125− docker push typeable/octopod:nightly
86+ if: github.ref == 'refs/heads/master' && github.event_name == 'push'
87+ uses: ./.github/actions/push_docker_images
88+ with:
89+ tag: nightly
90+ docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
91+ docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
12692
12793 - name: Rename Docker image archives
12894 run: |
129− mv octo-docker octo-cli-image.tar.gz
130− mv octopod-server-docker octopod-server-image.tar.gz
95+ cp $(nix-build ./nix -j auto -A octo-cli-container) octo-cli-image.tar.gz
96+ cp $(nix-build ./nix -j auto -A octopod-server-container) octopod-server-image.tar.gz
13197
13298 - name: Upload octo CLI Image
13399 uses: actions/upload-artifact@v2
@@ -140,32 +106,47 @@ jobs:
140106 with:
141107 name: octopod-server-image
142108 path: octopod-server-image.tar.gz
143−
144− - name: Clean up
145− if: always()
146− continue-on-error: true
147− run: |
148− docker logout ${{ steps.login-docker-hub.outputs.registry }}
149−
150109 release:
151− name: "Release"
152− if: github.ref == 'refs/heads/master'
153− runs-on: "ubuntu-latest"
110+ name: Release
111+ if: github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'true'
154112 needs: [octo-macOS, octo-linux, docker-images]
113+ runs-on: "ubuntu-latest"
155114 steps:
115+ - uses: actions/checkout@v2
116+ - name: Common setup
117+ uses: ./.github/actions/common_setup
118+ with:
119+ authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
120+ - name: Push Docker Release Images to DockerHub
121+ uses: ./.github/actions/push_docker_images
122+ with:
123+ tag: ${{ github.event.inputs.version }}
124+ docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
125+ docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
126+
127+ - name: Push Docker latest Images to DockerHub
128+ uses: ./.github/actions/push_docker_images
129+ with:
130+ tag: latest
131+ docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
132+ docker_password: ${{ secrets.DOCKERHUB_TOKEN }}
133+
156134 - uses: actions/download-artifact@v2
157− - name: Zip
135+ - name: Zip and move
158136 run: |
159137 chmod +x octo-cli-macos/octo
160138 zip octo-cli-macos octo-cli-macos/octo
161139 chmod +x octo-cli-linux/octo
162140 zip octo-cli-linux octo-cli-linux/octo
141+
142+ mv octo-cli-image/* .
143+ mv octopod-server-image/* .
163144 - uses: "marvinpinto/action-automatic-releases@latest"
164145 with:
165146 repo_token: "${{ secrets.GITHUB_TOKEN }}"
166147 automatic_release_tag: "latest"
167− prerelease: true
168− title: "Pre-Release"
148+ prerelease: false
149+ title: "${{ github.event.inputs.version }}"
169150 files: |
170151 *.zip
171152 *.tar.gz
.github/workflows/check_documentation.yaml+1−1
@@ -21,4 +21,4 @@ jobs:
2121
2222 - name: Run linter
2323 run: |
24− 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 charts
24+ 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-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 charts
.gitignore+1
@@ -10,3 +10,4 @@ octopod-css/node_modules
1010dev/certs/*
1111tmp/*
1212octopod-css/production
13+.idea/
.hlint.yamlnew file+55
@@ -0,0 +1,55 @@
1+# HLint configuration file
2+# https://github.com/ndmitchell/hlint
3+##########################
4+
5+# This file contains a template configuration file, which is typically
6+# placed as .hlint.yaml in the root of your project
7+
8+# Specify additional command line arguments
9+#
10+# - arguments: [--color, --cpp-simple, -XQuasiQuotes]
11+
12+# Control which extensions/flags/modules/functions can be used
13+#
14+# - extensions:
15+# - default: false # all extension are banned by default
16+# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
17+# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
18+#
19+# - flags:
20+# - {name: -w, within: []} # -w is allowed nowhere
21+#
22+# - modules:
23+# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
24+# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
25+#
26+# - functions:
27+# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
28+
29+# Add custom hints for this project
30+#
31+# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
32+# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
33+
34+# Turn on hints that are off by default
35+#
36+# Ban "module X(module X) where", to require a real export list
37+# - warn: {name: Use explicit module export list}
38+#
39+# Replace a $ b $ c with a . b $ c
40+# - group: {name: dollar, enabled: true}
41+#
42+# Generalise map to fmap, ++ to <>
43+# - group: {name: generalise, enabled: true}
44+
45+# Ignore some builtin hints
46+# - ignore: {name: Use let}
47+# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
48+
49+- ignore: { name: Replace case with maybe }
50+- ignore: { name: Use if }
51+# Define some custom infix operators
52+# - fixity: infixr 3 ~^#^~
53+
54+# To generate a suitable file for HLint do:
55+# $ hlint --default > .hlint.yaml
.nova/Configuration.jsonnew file+5
@@ -0,0 +1,5 @@
1+{
2+ "env-generator" : "nix-shell --run env",
3+ "haskell.checkProject" : false,
4+ "haskell.formattingProvider" : "fourmolu"
5+}
.vscode/settings.json+6−1
@@ -1,4 +1,9 @@
11{
22 "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix",
3− "haskell.formattingProvider": "fourmolu"
3+ "haskell.formattingProvider": "fourmolu",
4+ "haskell.plugin.hlint.diagnosticsOn": false,
5+ "haskell.hlint.logLevel": "none",
6+ "haskell.hlint.run": "never",
7+ "haskell.serverExecutablePath": "haskell-language-server",
8+ "editor.formatOnSave": true
49}
Development_guide.md+5−12
@@ -2,14 +2,7 @@
22
33## Git flow
44
5−`master` contains the latest "release version" only.
6−
7−All development should be done in the `develop` branch.
8−
9−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.
10−
11−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.
12−
5+Feature branches are merged into `master`. There is no `develop` branch.
136## Building
147
158### Nix Installation
@@ -22,13 +15,13 @@ curl https://nixos.org/nix/install | sh
2215
2316### Nix cache
2417
25−#### Reflex platform cache
18+#### Haskell.nix cache
2619
27−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`:
20+To speedup initial project builds you will want to set up the haskell.nix binary nix cache – append the following to `/etc/nix/nix.conf`:
2821
2922```
30−binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
31−binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
23+binary-caches = https://hydra.iohk.io https://cache.nixos.org/
24+binary-cache-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
3225binary-caches-parallel-connections = 40
3326```
3427
Makefile+3
@@ -36,5 +36,8 @@ run-backend-dev:
3636run-frontend-dev:
3737 `nix-build dev -A frontend -j auto`
3838
39+run-frontend-prod:
40+ `nix-build dev -A frontend --arg prod true -j auto`
41+
3942run-caddy-for-ghcid:
4043 `nix-build dev -A caddyForGhcid -j auto`
README.md+77−15
@@ -1,5 +1,5 @@
11<p align="center">
2− <a href="#"><img src="img/logo.svg" width="400"></img></a>
2+ <a href="#"><img src="img/logo.svg" width="500"></img></a>
33</p>
44
55<p align="center">
@@ -7,14 +7,43 @@
77 <a href="https://github.com/typeable/octopod/actions?query=workflow%3ADocumentation+branch%3Amaster"><img src="https://github.com/typeable/octopod/workflows/Documentation/badge.svg?branch=master" alt="Documentation"></a>
88</p>
99
10−_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.
10+_Octopod_ is a fully open-source self-service portal which empowers your team to leverage the full power of Helm without the need for technical expertise.
1111
12−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.
12+Octopod is a way for you to provide a self-service portal for managing instances of a service in your cluster. Anything you can describe in a Helm Chart, any member of team can deploy. No matter how complex the deployment process is.
13+
14+The recipe is simple: you provide a url to your Helm repo, Octopod then automatically extracts the possible configuration that the Chart can have, and automatically manages the lifecycle of the services. What users see is just a simple web interface:
1315
1416## 🖥 Demo
1517
1618<p align="center"><img src="img/demo.gif"></img></p>
1719
20+## 🧑‍🔬 Try it out for yourself
21+
22+You can have the installation from the demo above running on your local machine in minutes by running the following command:
23+
24+```bash
25+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/typeable/octopod/master/octopod_local_install.sh)"
26+```
27+
28+You can dive deeper into the installation process in our [deployment guide](docs/en/Octopod_deployment_guide.md).
29+
30+<details>
31+<summary>You can remove everything the command above installs by running the following command:</summary>
32+
33+```bash
34+kind delete cluster --name octopod
35+```
36+
37+</details>
38+
39+## Motivation
40+
41+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.
42+
43+The members of our team that facilitate rigorously testing are also the member that require the most attention from our DevOps engineers to manage the many staging environments required for a proper testing cycle.
44+
45+With Octopod we were able to reduce the reliance on our DevOps teams for testing to practically zero, while increasing the DevOps maneuverability of our QA engineers and process.
46+
1847## 📑 Documentation
1948
2049### 🔭 High-level notes
@@ -23,27 +52,56 @@ We created _Octopod_ because we believe that everything we release should be rig
2352- [🧑‍💻 Technical case study](docs/en/Tech_case_study.md)
2453
2554### 🛠️ Technical documentation
26−- [🏗 Technical architecture](docs/en/Technical_architecture.md) [[RU](docs/ru/Technical_architecture.md)]
27−- [⚙️ Control script guide][cs] [[RU](docs/ru/Control_scripts.md)]
28−- [🔧🐙 Octopod deployment guide](docs/en/Octopod_deployment_guide.md) [[RU](docs/ru/Octopod_deployment_with_K8S.md)]
29−- [🔧🚀 Helm-based Octopod project setup](docs/en/Helm-based_deployment_guide.md) [[RU](docs/ru/Helm-based_deployment_guide.md)]
30−- [🐙🎛 octo CLI user guide][octo] [[RU](docs/ru/Octo_user_guide.md)]
55+- [🏗 Technical architecture](docs/en/Technical_architecture.md)
56+- [⚙️ Control script guide][cs]
57+- [🔧🐙 Octopod deployment guide](docs/en/Octopod_deployment_guide.md)
58+- [🔧🚀 Helm-based Octopod project setup](docs/en/Helm-based_deployment_guide.md)
59+- [🐙🎛 octo CLI user guide][octo]
3160- [🤖 CI integration](docs/en/Integration.md)
32−- [🔒 Octopod security model](docs/en/Security_model.md) [[RU](docs/ru/Security_model.md)]
61+- [🔒 Octopod security model](docs/en/Security_model.md)
3362
3463## ℹ️ FAQ
3564
3665### How long does it take to set up _Octopod_?
3766
38−The longest part of setting up _Octopod_ for your project will probably be writing [_Control Scripts_][cs]. In total you should be able to get things running in about a day.
67+Deploying Octopod itself can be done in a matter of minutes if you have a Kubernetes cluster at hand. If you want to deploy it locally we have [a handy script](docs/en/Octopod_deployment_guide.md#if-you-want-to-try-it-locally) that sets everything up automatically.
68+
69+After you have Octopod up and running the only thing you need is a Helm Chart that your team might want to deploy. That's it.
3970
4071### Will _Octopod_ work with my project if it uses X?
4172
4273Yes. _Octopod_ is project-agnostic. If you can run your project in a Docker container, then you can use _Octopod_ with that project.
4374
75+### Does _Octopod_ work with "vanilla" Kubernetes projects that don't use Helm?
76+
77+If you do not have Helm Charts for your projects, you will not be able to leverage the simple Octopod installation procedure we have developed.
78+
79+To use _Octopod_ with your project will need to either:
80+
81+- Write Helm Charts for your project
82+- Write a set of [_control scripts_][cs] that deploy "vanilla" Kubernetes services
83+
84+You can always contact us if you need help. A good first place to ask questions is [Octopod Discussions](https://github.com/typeable/octopod/discussions).
85+
4486### What do I need to know to set up Octopod?
4587
46−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_][cs] in whatever language you like.
88+You need to understand the basics of _Kubernetes_ and be familiar with whatever hosting provider you will be using.
89+
90+### What separates Octopod from exiting solutions like Gitlab Dynamic Environments?
91+
92+Most existing solutions like Gitlab Dynamic Environments offer you an extension of CI/CD pipelines. All they do is allow you to run bash scripts.
93+
94+This has several drawback the main of which is the fact that you need to understand the under-the-hood details of the deployment process to use them. Existing solutions don't abstract the deployment process. They package up steps of deployment. The user is still responsible for coordinating the steps. You still need to manually manage the state of your deployed services.
95+
96+Observability and transparency of the deployment management process is another drawback. To inspect the state of a deployed system you need to drop down to the Kubernetes level. At that point you alienate anyone without DevOps expertise. This is the problem Octopod solves.
97+
98+Octopod _actually_ abstracts the under-the-hood detail like Helm, Kubernetes and Docker from the user into an intuitive and easy to understand model. This allows anyone to manage and deploy services.
99+
100+### Octopod is great, but my service deployment lifecycle is slightly different.
101+
102+Octopod was developed in a modular way from the very start. It offers specific extension points called _control scripts_ that allow you to modify the behavior of Octopod. The _control scripts_ can be implemented in any programming language, even Bash. You can read more about _control scripts_ in our [_control script guide_][cs].
103+
104+We have developed [a generic set of _control scripts_](helm-control-scripts/) that should work great with any Helm Chart. You can use them as a base for your custom _control scripts_.
47105
48106### Does _Octopod_ work with my CI?
49107
@@ -64,9 +122,6 @@ There are several places where things can go wrong:
64122
65123 If you have supplied a [_Kubernetes Dashboard_](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/) URL to _Octopod_ during deployment, then you will have a "Details" button in the _Web UI_. You can click that button to get a filtered view of the _Kubernetes Dashboard_ where you can see what could have gone wrong and diagnose the issue.
66124
67−### Why Haskell and Rust?
68−
69−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.
70125
71126## Quotations
72127
@@ -78,7 +133,14 @@ We believe that there is a lot to be gained in programming in general by being a
78133
79134If you still have questions, be sure to ask them in our [Octopod Discussions](https://github.com/typeable/octopod/discussions).
80135
81−<p align="center"><a href="https://typeable.io"><img src="img/typeable.png" width="177px"></img></a></p>
136+
137+<br />
138+
139+<p align="center">
140+ <i>Star the project of you like it</i>
141+</p>
142+
143+<p align="center"><a href="https://typeable.io"><img src="img/typeable_logo.svg" width="177px"></img></a></p>
82144
83145[cs]: docs/en/Control_scripts.md
84146[octo]: docs/en/Octo_user_guide.md
Release_checklist.md+5−18
@@ -1,21 +1,8 @@
11# Release checklist
22
331. Update the referenced tags in the documentation
4−2. If there were changes to the examples:
5− 1. Build and push the new containers:
6− 1. octopod-web-app-example
7− 2. octopod-helm-example
8− 2. Create a new tag incrementing the integer version number of the tag:
9− 1. Build the updated images
10− 2. Tag it with the new `<integer>` (`docker tag`)
11− 3. Push the new tag (`docker push`)
12− 3. Update docs where the tags are referenced.
13−3. Merge the `develop` branch into the `staging` branch.
14−4. CI will automatically update the `latest` tag for both `octo` and `octopod`. Wait for CI to complete.
15−5. Deploy everything in our private cluser and make sure everything works.
16−6. Merge the `staging` branch into `master` using **Rebase and Merge**.
17−7. 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.
18−8. Edit the created release in GitHub to match the version you are releasing.
19− 1. Change the release name to the version being released.
20− 2. Uncheck "This is a pre-release"
21−9. Push the new release of `octo` and `octopod`. To do this run `./release.sh <version>`.
4+2. If there were changes to control scripts:
5+ 1. _**\<TODO\>**_
6+3. [Dispatch the "Build and release" workflow](https://github.com/typeable/octopod/actions/workflows/build.yaml), specifying the version you are releasing:
7+
8+ ![](./img/release_workflow.png)
build.shdeleted−60
@@ -1,60 +0,0 @@
1−#!/usr/bin/env bash
2−
3−set -e
4−
5−build_octo_cli_docker_image() {
6− nix-build ./nix -A octo-cli-container -o "$1"
7−}
8−
9−build_octopod_server_docker_image() {
10− nix-build ./nix -A octopod-server-container --arg migrations "$1" -o "$2"
11−}
12−
13−push_docker_images() {
14− outfile=latest-octopod-server-docker
15− for image_name in $octo_cli_docker $octopod_server_docker; do
16− image_type=$(echo "$image_name" | cut -d- -f1)
17− image=$(ls -ls "$image_name" | awk '{print $12}')
18− echo "size: $(du -sh $image)"
19− docker load --input "$image" | tee "$outfile"
20− nixcontainer=$(awk '{print $3}' $outfile)
21− docker tag "$nixcontainer" "typeable/${image_type}:$1"
22− docker push "typeable/${image_type}:$1"
23− echo "Published: ${image_type}:$1"
24− done
25− rm $outfile
26−}
27−
28−build_docker_images() {
29− build_octo_cli_docker_image "$octo_cli_docker"
30− build_octopod_server_docker_image "$migrations" "$octopod_server_docker"
31−}
32−
33−export tag=$(git rev-parse HEAD)
34−export migrations="./migrations"
35−export octo_cli_docker="octo-docker"
36−export octopod_server_docker="octopod-server-docker"
37−
38−case "$1" in
39−build-and-push)
40− echo "$1 mode"
41−
42− if test -z "$2"; then
43− echo "Please provide a tag to upload to"
44− exit 1
45− fi
46−
47− build_docker_images
48− push_docker_images $2
49− ;;
50−build)
51− echo "$1 mode"
52− build_docker_images
53− ;;
54−*)
55− echo "usage:"
56− echo " $0 build Builds the docker images."
57− echo " $0 build-and-push <tag> Builds the docker images and uploads it to Docker Hub under the tag <tag>."
58− exit 1
59− ;;
60−esac
cabal.project+6
@@ -31,5 +31,11 @@ source-repository-package
3131 subdir: lib/executable-config/lookup
3232 --sha256: 0dlk8y6rxc87crw7764zq2py7nqn38lw496ca1y893m9gdq8qdkz
3333
34+source-repository-package
35+ type: git
36+ location: https://github.com/typeable/ordered-containers.git
37+ tag: 3eb05fb2f44fe482e9092aff447e9c68fbd6a7f6
38+ --sha256: 0q3cp3nar41bqh2ldl1ykzaycf0bjxh0n58za5g19x4g7ab8h9gm
39+
3440package reflex-dom
3541 flags: +use-warp
charts/octopod/Chart.yaml+3−3
@@ -2,12 +2,12 @@ apiVersion: v2
22name: octopod
33description: An opensource self-hosted solution for managing multiple deployments in a Kubernetes cluster.
44type: application
5−version: 0.5.1
6−appVersion: 1.3.1
5+version: 0.6.11
6+appVersion: 1.5.2
77keywords:
88 - kubernetes
99 - octopod
10−home: https://octopod.site
10+home: https://github.com/typeable/octopod
1111sources:
1212 - https://github.com/typeable/octopod
1313maintainers:
charts/octopod/README.md+28−11
@@ -1,24 +1,32 @@
11# Octopod
22
3−[Octopod](https://octopod.site/) 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.
3+[Octopod](https://github.com/typeable/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.
44
55## TL;DR
66```console
77$ helm repo add typeable https://typeable.github.io/octopod/
8−$ kubectl create ns octopod-deployment
8+$ helm repo update
9+$ kubectl create namespace octopod-deployment
910$ helm install octopod typeable/octopod --set octopod.baseDomain="your-domain.com"
1011```
1112
1213## Introduction
1314
14−This chart bootstraps an Octopod deployment in a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
15+This chart bootstraps Octopod deployment in a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
1516
1617## Prerequisites
1718
18−- Kubernetes 1.12+
19−- Helm 3.1.0
20−- PV support (for postgresql persistense)
21−- nginx-ingress controller
19+### Mandatory
20+
21+- Kubernetes version >= 1.12.0 <= 1.22.0
22+- PVC support (for PostgreSQL persistence)
23+- [NGINX Ingress](https://kubernetes.github.io/ingress-nginx/) contoller version <= 0.49.3 installed. NGINX Ingress controller v1.x.x is not currently supported
24+
25+### Optional
26+
27+- Cert Manager ([cert-manager](https://cert-manager.io/docs/installation/)) installed, if you want to get SSL certificates from Let's Encrypt automatically.
28+- Cluster issuer ([ACME Issuer](https://cert-manager.io/docs/configuration/acme/#creating-a-basic-acme-issuer)) created. By default we assume that it would be named `letsencrypt`. You can change it by setting `ingress.tls.clusterIssuer` parameter.
29+
2230
2331## Installing the Chart
2432This chart will not create or delete any namespaces for you.
@@ -31,13 +39,14 @@ You'll need to create 2 namespaces before installing:
3139 ```
32402. A namespace in which Octopod will deploy all your deployments (configured in `octopod.deploymentNamespace`):
3341 ```console
34− $ kubectl create namespace octopod-deployments
42+ $ kubectl create namespace octopod-deployment
3543 ```
3644
3745To install the chart with the release name `my-release` execute:
3846
3947```console
4048$ helm repo add typeable https://typeable.github.io/octopod/
49+$ helm repo update
4150$ helm -n octopod install my-release typeable/octopod --set octopod.baseDomain="your-domain.com"
4251```
4352
@@ -51,10 +60,10 @@ To uninstall/delete the `my-release` deployment:
5160$ helm -n octopod delete my-release
5261```
5362
54−The command removes all the Kubernetes components but PVC's associated with the postgres chart and deletes the release.
63+The command removes all the Kubernetes components except PVCs associated with the postgres chart, and deletes the release.
5564
5665## Note about generated values
57−Some values (such as passwords) in this chart (and its dependencies) are generated automatically, but due to [a limitation in helm](https://github.com/helm/charts/issues/5167) the values are changing on every upgrade. To prevent this you must fix these values by providing them via `--set` flags or in the [values file](https://helm.sh/docs/chart_template_guide/values_files/).
66+Some values (such as passwords) in this chart (and its dependencies) are generated automatically, but due to [the limitation in Helm](https://github.com/helm/charts/issues/5167) the values are changing on every upgrade. To prevent this you must set these values explicitly by providing them via `--set` flags or in the [values file](https://helm.sh/docs/chart_template_guide/values_files/).
5867
5968These values are:
6069- `postgresql.postgresqlPassword` ― main db password
@@ -155,7 +164,7 @@ $ helm install my-release -f values.yaml .
155164## Configuration and installation details
156165
157166If you want to have authentication for Octopod UI you can use project like [Oauth2-Proxy](https://github.com/oauth2-proxy/oauth2-proxy) or directly use your oauth provider.
158−After that you can add following annotations to UI ingress (considering that your oauth provider installed on oath.example.com):
167+After that you can add following annotations to UI ingress (considering that your oauth provider installed on oauth.example.com):
159168```yaml
160169ingress:
161170 ui:
@@ -163,3 +172,11 @@ ingress:
163172 nginx.ingress.kubernetes.io/auth-url: "https://oauth.example.com/oauth2/auth"
164173 nginx.ingress.kubernetes.io/auth-signin: "https://oauth.example.com/oauth2/start?rd=/redirect/$http_host$request_uri"
165174```
175+
176+<br />
177+
178+<p align="center">
179+ <i>Star the project of you like it</i>
180+</p>
181+
182+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
charts/octopod/index.yamlnew file+37
@@ -0,0 +1,37 @@
1+apiVersion: v1
2+entries:
3+ postgresql:
4+ - annotations:
5+ category: Database
6+ apiVersion: v2
7+ appVersion: 11.12.0
8+ created: "2022-08-23T14:43:53.959078+03:00"
9+ dependencies:
10+ - name: common
11+ repository: https://charts.bitnami.com/bitnami
12+ version: 1.x.x
13+ description: Chart for PostgreSQL, an object-relational database management system
14+ (ORDBMS) with an emphasis on extensibility and on standards-compliance.
15+ digest: afece60469d73b9cc4b578fff9b7627dd6ae609be50a04dabac5ad0be5105f33
16+ home: https://github.com/bitnami/charts/tree/master/bitnami/postgresql
17+ icon: https://bitnami.com/assets/stacks/postgresql/img/postgresql-stack-220x234.png
18+ keywords:
19+ - postgresql
20+ - postgres
21+ - database
22+ - sql
23+ - replication
24+ - cluster
25+ maintainers:
26+ - email: containers@bitnami.com
27+ name: Bitnami
28+ - email: cedric@desaintmartin.fr
29+ name: desaintmartin
30+ name: postgresql
31+ sources:
32+ - https://github.com/bitnami/bitnami-docker-postgresql
33+ - https://www.postgresql.org/
34+ urls:
35+ - charts/postgresql-10.5.3.tgz
36+ version: 10.5.3
37+generated: "2022-08-23T14:43:53.955624+03:00"
charts/octopod/templates/NOTES.txt+9−5
@@ -1,8 +1,12 @@
1−Thank you for installing Octopod!
2−You can access the Web UI here: {{ include "httpScheme" . }}://{{ include "uiIngressHost" . }}
3−To access Octopod via octo CLI you can use this URL: {{ include "httpScheme" . }}://{{ include "powerAppIngressHost" . }}
4−To get your CLI secret you need to execute this command:
5− kubectl -n {{ .Release.Namespace }} get secret {{ include "octopodCliAuthSecretName" . }} -o jsonpath='{.data.cli-secret}' | base64 -d
1+🐙 Thank you for installing Octopod!
2+
3+ 🖥️ You can access the Web UI here: {{ include "httpScheme" . }}://{{ include "uiIngressHost" . }}
4+
5+ 🎛 To access Octopod via octo CLI you can use this URL: {{ include "httpScheme" . }}://{{ include "powerAppIngressHost" . }}
6+
7+ 🔑 To get your CLI secret you need to execute this command:
8+ kubectl -n {{ .Release.Namespace }} get secret {{ include "octopodCliAuthSecretName" . }} -o jsonpath='{.data.cli-secret}' | base64 -d
9+
610{{ if and (not .Values.octopod.cliAuthSecret) (not .Values.vault.enabled) .Release.IsUpgrade }}
711{{ fail "To upgrade you need to set the octopod.cliAuthSecret variable with your current secret" }}
812{{- end }}
charts/octopod/templates/ingress-app.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-app
@@ -41,7 +44,10 @@ spec:
4144 http:
4245 paths:
4346 - path: /
47+ pathType: ImplementationSpecific
4448 backend:
45− serviceName: {{ $fullName }}
46− servicePort: {{ .Values.service.ports.app }}
49+ service:
50+ name: {{ $fullName }}
51+ port:
52+ number: {{ .Values.service.ports.app }}
4753{{- end }}
charts/octopod/templates/ingress-powerapp.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-powerapp
@@ -35,7 +38,10 @@ spec:
3538 http:
3639 paths:
3740 - path: /
41+ pathType: ImplementationSpecific
3842 backend:
39− serviceName: {{ $fullName }}
40− servicePort: {{ .Values.service.ports.powerApp }}
43+ service:
44+ name: {{ $fullName }}
45+ port:
46+ number: {{ .Values.service.ports.powerApp }}
4147{{- end }}
charts/octopod/templates/ingress-ui.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-ui
@@ -35,7 +38,10 @@ spec:
3538 http:
3639 paths:
3740 - path: /
41+ pathType: ImplementationSpecific
3842 backend:
39− serviceName: {{ $fullName }}
40− servicePort: {{ .Values.service.ports.ui }}
43+ service:
44+ name: {{ $fullName }}
45+ port:
46+ number: {{ .Values.service.ports.ui }}
4147{{- end }}
charts/octopod/templates/ingress-ws.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-ws
@@ -35,7 +38,10 @@ spec:
3538 http:
3639 paths:
3740 - path: /
41+ pathType: ImplementationSpecific
3842 backend:
39− serviceName: {{ $fullName }}
40− servicePort: {{ .Values.service.ports.ws }}
43+ service:
44+ name: {{ $fullName }}
45+ port:
46+ number: {{ .Values.service.ports.ws }}
4147{{- end }}
charts/octopod/templates/nginx-configmap.yaml+5−5
@@ -1,4 +1,5 @@
11{{- $octopodAppAuthPassword := include "octopodUiAuthSecret" . -}}
2+{{- $etag := sha256sum (now | date "Mon Jan 2 15:04:05 MSK 2021") -}}
23apiVersion: v1
34kind: ConfigMap
45metadata:
@@ -14,11 +15,10 @@ data:
1415 index index.html;
1516 error_page 404 =200 /index.html;
1617 charset utf-8;
17− gzip on;
18− gzip_disable msie6;
19− gzip_buffers 32 4k;
20− gzip_comp_level 3;
21− gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/rss+xml text/javascript image/svg+xml application/vnd.ms-f ontobject application/x-font-ttf font/opentype;
18+ location ~* \.(html|css|js|xml)$ {
19+ gzip_static on;
20+ add_header 'ETag' '{{ $etag }}';
21+ }
2222 }
2323 config.json: |
2424 {
charts/octopod/templates/octopod-configmap.yaml+6−1
@@ -16,8 +16,13 @@ data:
1616 CHECKING_COMMAND: {{ printf "%s/check" (include "controlScriptsPath" .) | quote }}
1717 CLEANUP_COMMAND: {{ printf "%s/cleanup" (include "controlScriptsPath" .) | quote }}
1818 ARCHIVE_CHECKING_COMMAND: {{ printf "%s/archive_check" (include "controlScriptsPath" .) | quote }}
19− TAG_CHECKING_COMMAND: {{ printf "%s/tag_check" (include "controlScriptsPath" .) | quote }}
19+ CONFIG_CHECKING_COMMAND: {{ printf "%s/config_check" (include "controlScriptsPath" .) | quote }}
2020 INFO_COMMAND: {{ printf "%s/info" (include "controlScriptsPath" .) | quote }}
21+ DEPLOYMENT_CONFIG_COMMAND: {{ printf "%s/deployment_overrides" (include "controlScriptsPath" .) | quote }}
22+ DEPLOYMENT_KEYS_COMMAND: {{ printf "%s/deployment_keys" (include "controlScriptsPath" .) | quote }}
23+ APPLICATION_CONFIG_COMMAND: {{ printf "%s/app_overrides" (include "controlScriptsPath" .) | quote }}
24+ APPLICATION_KEYS_COMMAND: {{ printf "%s/app_keys" (include "controlScriptsPath" .) | quote }}
25+ UNARCHIVE_COMMAND: {{ printf "%s/unarchive" (include "controlScriptsPath" .) | quote }}
2126 {{- range $name, $value := .Values.octopod.env }}
2227 {{ $name }}: {{ $value | quote }}
2328 {{- end }}
charts/octopod/values.yaml+6−6
@@ -79,27 +79,27 @@ octopod:
7979 projectName: Octopod
8080 deploymentNamespace: octopod-deployment
8181 baseDomain: ""
82− statusUpdateTimeout: 600
83− archiveRetention: 1209600
82+ statusUpdateTimeout: "600"
83+ archiveRetention: "1209600"
8484 migrations:
8585 enabled: true
8686 env:
8787 HELM_BIN: "/utils/helm"
88− KUBECTL_BIN: "/utils/kubectl"
8988 DEFAULTS: |
9089 {
9190 "chart_name": "wordpress",
92− "chart_repo_name": "bitnami",
9391 "chart_repo_url": "https://charts.bitnami.com/bitnami",
9492 "chart_version": "12.0.0",
95− "default_overrides": []
93+ "default_overrides": [
94+ "ingress.enabled=true"
95+ ]
9696 }
9797 vaultEnv: {}
9898controlScripts:
9999 image:
100100 repository: typeable/octopod-helm-control-scripts
101101 pullPolicy: IfNotPresent
102− tag: 0.1.0
102+ tag: 0.2.4
103103sqitch:
104104 image:
105105 repository: typeable/sqitch
default.nix+28−5
@@ -8,8 +8,15 @@
88}:
99let
1010 octopod-css = import ./octopod-css { inherit pkgsSrc; };
11+ insaneOptimizationFlags = [
12+ "-O2"
13+ "-fexpose-all-unfoldings"
14+ "-fspecialise-aggressively"
15+ ];
1116 addLocalOptions = x:
12− if prod then x // { ghcOptions = [ "-Werror" ]; }
17+ if prod then x // {
18+ ghcOptions = [ "-Werror" ] ++ insaneOptimizationFlags;
19+ }
1320 else x // { ghcOptions = [ "-O0" ]; };
1421
1522 hsPkgs = pkgs.haskell-nix.cabalProject {
@@ -28,11 +35,27 @@ let
2835
2936 modules = [
3037 {
31− ghcOptions = [ "-O2" ];
38+ ghcOptions = insaneOptimizationFlags;
3239 dontStrip = false;
3340 dontPatchELF = false;
3441 enableDeadCodeElimination = true;
35− packages.octopod-backend = addLocalOptions { src = ./octopod-backend; };
42+ packages.octopod-backend = addLocalOptions (
43+ if prod
44+ then {
45+ src = pkgs.runCommand "octopod-backend-src" { }
46+ ''
47+ mkdir -p $out
48+ cp -r ${./octopod-backend}/* $out
49+ cp -r ${builtins.path { path = ./.git; name = "dot-git"; }}/ $out/.git/
50+ '';
51+ components.exes.octopod-exe = {
52+ build-tools =
53+ pkgs.lib.mkForce [ pkgs.buildPackages.buildPackages.gitReallyMinimal ];
54+ extraSrcFiles = [ ".git/**/*" ];
55+ };
56+ }
57+ else { src = ./octopod-backend; }
58+ );
3659 packages.octo-cli = addLocalOptions { src = ./octo-cli; };
3760 packages.octopod-api = addLocalOptions { src = ./octopod-api; };
3861 packages.octopod-frontend = addLocalOptions { src = ./octopod-frontend; };
@@ -40,8 +63,8 @@ let
4063 }
4164 ];
4265
43− index-state = "2021-08-04T00:00:00Z";
44− compiler-nix-name = "ghc8105";
66+ index-state = "2021-11-22T00:00:00Z";
67+ compiler-nix-name = "ghc8107";
4568 };
4669in
4770hsPkgs // {
dev/default.nix+72−10
@@ -1,10 +1,12 @@
11{ pkgs ? hsPkgs.pkgs
22, sources ? import ../nix/sources.nix
33, nix-filter ? import sources.nix-filter
4−, hsPkgs ? import ./.. { }
4+, hsPkgs ? import ./.. { inherit prod system; }
55, migrations ? ../migrations
66, octopod-css ? import ../octopod-css { inherit pkgsSrc; }
77, pkgsSrc ? hsPkgs.pkgsSrc
8+, prod ? false
9+, system ? builtins.currentSystem
810}:
911let frontendConfig = pkgs.writeTextDir "config.json" ''
1012 {
@@ -23,6 +25,8 @@ in
2325 caddyConfig = pkgs.writeText "caddy-config" ''
2426 http://localhost:8000
2527
28+ header Cache-Control no-cache
29+
2630 reverse_proxy /api/* localhost:3002
2731
2832 file_server /config.json {
@@ -52,6 +56,8 @@ in
5256 caddyConfig = pkgs.writeText "caddy-config" ''
5357 http://localhost:8000
5458
59+ header Cache-Control no-cache
60+
5561 reverse_proxy /api/* localhost:3002
5662
5763 @notStatic {
@@ -97,16 +103,67 @@ in
97103 failScript = pkgs.writeScript "fail.sh" ''
98104 #!${pkgs.bash}/bin/bash
99105
106+ echo "You did something wrong :("
107+ echo "You did something wrong twice :("
108+
109+ 1>&2 echo "You did something wrong, but you shouldn't see this."
110+
100111 exit 1
101112 '';
102113
103114 infoScript = pkgs.writeScript "info.sh" ''
104115 #!${pkgs.bash}/bin/bash
105116
106− sleep 1
117+ sleep 4
118+
119+ echo "foo.vat1.fjij.key1,"
120+
121+ for i in {1..10}
122+ do
123+ for j in {1..10}
124+ do
125+ echo "foo.vat$i.fjij.key$j,value"
126+ done
127+ done
128+
129+ for i in {1..10}
130+ do
131+ echo "fjij.key$i,value"
132+ done
133+
134+ exit 0
135+ '';
136+ smallInfoScript = pkgs.writeScript "info.sh" ''
137+ #!${pkgs.bash}/bin/bash
138+
139+ sleep 4
140+
141+ for i in {1..10}
142+ do
143+ echo "fjij.key$i,value"
144+ done
145+
146+ exit 0
147+ '';
148+
149+ keysScript = pkgs.writeScript "keys.sh" ''
150+ #!${pkgs.bash}/bin/bash
151+
152+ sleep 4
153+
154+ for i in {1..10}
155+ do
156+ echo "foo.vat.fkijf.fjij.key$i"
157+ done
158+ for i in {1..10}
159+ do
160+ echo "foo.vat.fjij.key$i"
161+ done
107162
108− echo "key,value"
109− echo "key2,value2"
163+ for i in {1..10}
164+ do
165+ echo "fjij.key$i"
166+ done
110167
111168 exit 0
112169 '';
@@ -137,15 +194,20 @@ in
137194 export CHECKING_COMMAND=${echoScript}
138195 export CLEANUP_COMMAND=${echoScript}
139196 export ARCHIVE_CHECKING_COMMAND=${echoScript}
140− export TAG_CHECKING_COMMAND=${echoScript}
141− export INFO_COMMAND=${infoScript}
197+ export CONFIG_CHECKING_COMMAND=${failScript}
198+ export INFO_COMMAND=${smallInfoScript}
142199 export NOTIFICATION_COMMAND=${writeScript}
143− export DEPLOYMENT_OVERRIDES_COMMAND=${infoScript}
144− export DEPLOYMENT_KEYS_COMMAND=${infoScript}
145− export APPLICATION_OVERRIDES_COMMAND=${infoScript}
146− export APPLICATION_KEYS_COMMAND=${infoScript}
200+ export DEPLOYMENT_CONFIG_COMMAND=${infoScript}
201+ export DEPLOYMENT_KEYS_COMMAND=${keysScript}
202+ export APPLICATION_CONFIG_COMMAND=${infoScript}
203+ export APPLICATION_KEYS_COMMAND=${keysScript}
147204 export UNARCHIVE_COMMAND=${writeScript}
148205 export POWER_AUTHORIZATION_HEADER="123"
206+ export CACHE_INVALIDATION_TIME="60"
207+ export CACHE_UPDATE_TIME="20"
208+ export CONTROL_SCRIPT_TIMEOUT="10"
209+ export DEBUG=true
210+ export PROD_LOGS=false
149211 ${hsPkgs.octopod-backend.components.exes.octopod-exe}/bin/octopod-exe \
150212 --port 4443 \
151213 --ui-port 3002 \
docs/Makefile−9
@@ -20,12 +20,3 @@ plantuml:
2020 name=`basename $$src .mmd`; \
2121 plantuml -I $$src -o "../../diagrams/images" -tpng; \
2222 done
23−
24−plantuml:
25− # to install plantuml go to https://plantuml.com/command-line
26− #
27− # plantuml takes output paths relative to input file
28− for src in `ls diagrams/src/*.puml`; do \
29− name=`basename $$src .mmd`; \
30− plantuml -I $$src -o "../../diagrams/images" -tpng; \
31− done
docs/README.md+6−6
@@ -6,10 +6,10 @@
66- [🧑‍💻 Technical case study](en/Tech_case_study.md)
77
88## 🛠️ Technical documentation
9−- [🏗 Technical architecture](en/Technical_architecture.md) [[RU](ru/Technical_architecture.md)]
10−- [⚙️ Control script guide](en/Control_scripts.md) [[RU](ru/Control_scripts.md)]
11−- [🔧🐙 Octopod deployment guide](en/Octopod_deployment_guide.md) [[RU](ru/Octopod_deployment_with_K8S.md)]
12−- [🔧🚀 Helm-based Octopod project setup](en/Helm-based_deployment_guide.md) [[RU](ru/Helm-based_deployment_guide.md)]
13−- [🐙🎛 octo CLI user guide](en/Octo_user_guide.md) [[RU](ru/Octo_user_guide.md)]
9+- [🏗 Technical architecture](en/Technical_architecture.md)
10+- [⚙️ Control script guide](en/Control_scripts.md)
11+- [🔧🐙 Octopod deployment guide](en/Octopod_deployment_guide.md)
12+- [🔧🚀 Helm-based Octopod project setup](en/Helm-based_deployment_guide.md)
13+- [🐙🎛 octo CLI user guide](en/Octo_user_guide.md)
1414- [🤖 CI integration](en/Integration.md)
15−- [🔒 Octopod security model](en/Security_model.md) [[RU](ru/Security_model.md)]
15+- [🔒 Octopod security model](en/Security_model.md)
docs/diagrams/images/app-architecture.png

Binary file not shown.

docs/diagrams/images/technical-architecture-archive-via-cli.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-archive-via-ui.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-archive.pngnew file

Binary file not shown.

docs/diagrams/images/technical-architecture-cleanup-via-cli.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-cleanup-via-ui.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-cleanup.pngnew file

Binary file not shown.

docs/diagrams/images/technical-architecture-create-via-cli.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-create-via-ui.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-create.pngnew file

Binary file not shown.

docs/diagrams/images/technical-architecture-deployment-states-fsm.png

Binary file not shown.

docs/diagrams/images/technical-architecture-restore-via-cli.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-restore-via-ui.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-restore.pngnew file

Binary file not shown.

docs/diagrams/images/technical-architecture-update-via-cli.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-update-via-ui.pngdeleted

Binary file not shown.

docs/diagrams/images/technical-architecture-update.pngnew file

Binary file not shown.

docs/diagrams/src/technical-architecture-archive-via-cli.mmddeleted−37
@@ -1,37 +0,0 @@
1−sequenceDiagram
2− participant octo CLI
3− participant Octopod Server
4− participant PostgreSQL
5− participant UI
6− participant Octopod Server/BgWorker
7− participant Octopod Server/StatusUpdater
8− participant ControlScripts
9− participant KubeAPI
10− octo CLI->>Octopod Server: archive(name)
11− Octopod Server->>PostgreSQL: status=ArchivePending
12− alt name not found
13− PostgreSQL-->>Octopod Server: error: name not found
14− Octopod Server-->>octo CLI: error: name not found
15− else
16− PostgreSQL-->>Octopod Server: ok
17− Octopod Server-xUI: event FrontendPleaseUpdateEverything
18− UI->>Octopod Server: get deployments info
19− Octopod Server-->>UI: deployments info
20− Octopod Server->>Octopod Server/BgWorker: archive
21− Octopod Server-->>octo CLI: done
22− Octopod Server/BgWorker->>ControlScripts: archive
23− ControlScripts->>KubeAPI: archive deployment
24− KubeAPI-->>ControlScripts: done
25− ControlScripts-->>Octopod Server/BgWorker: done
26− Octopod Server/BgWorker->>PostgreSQL: write logs
27− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
28− UI->>Octopod Server: get deployments info
29− Octopod Server-->>UI: deployments info
30− Note over Octopod Server/StatusUpdater: wait 5 minutes
31− loop check deployment status every 30 seconds
32− Octopod Server/StatusUpdater->>PostgreSQL: status=Archived/ArchivePending
33− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
34− UI->>Octopod Server: get deployments info
35− Octopod Server-->>UI: deployments info
36− end
37− end
docs/diagrams/src/technical-architecture-archive-via-ui.mmddeleted−36
@@ -1,36 +0,0 @@
1−sequenceDiagram
2− participant UI
3− participant Octopod Server
4− participant PostgreSQL
5− participant Octopod Server/BgWorker
6− participant Octopod Server/StatusUpdater
7− participant ControlScripts
8− participant KubeAPI
9− UI->>Octopod Server: archive(name)
10− Octopod Server->>PostgreSQL: status=ArchivePending
11− alt name not found
12− PostgreSQL-->>Octopod Server: error: name not found
13− Octopod Server-->>UI: error: name not found
14− else
15− PostgreSQL-->>Octopod Server: ok
16− Octopod Server-xUI: event FrontendPleaseUpdateEverything
17− UI->>Octopod Server: get deployments info
18− Octopod Server-->>UI: deployments info
19− Octopod Server->>Octopod Server/BgWorker: archive
20− Octopod Server-->>UI: done
21− Octopod Server/BgWorker->>ControlScripts: archive
22− ControlScripts->>KubeAPI: archive deployment
23− KubeAPI-->>ControlScripts: done
24− ControlScripts-->>Octopod Server/BgWorker: done
25− Octopod Server/BgWorker->>PostgreSQL: write logs
26− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
27− UI->>Octopod Server: get deployments info
28− Octopod Server-->>UI: deployments info
29− Note over Octopod Server/StatusUpdater: wait 5 minutes
30− loop check deployment status every 30 seconds
31− Octopod Server/StatusUpdater->>PostgreSQL: status=Archived/ArchivePending
32− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
33− UI->>Octopod Server: get deployments info
34− Octopod Server-->>UI: deployments info
35− end
36− end
docs/diagrams/src/technical-architecture-archive.mmdnew file+31
@@ -0,0 +1,31 @@
1+sequenceDiagram
2+ participant User
3+ participant Octopod Frontend
4+ participant Octopod Backend
5+ participant Control Scipts
6+ participant Octopod DB
7+
8+ User ->>+ Octopod Frontend: Archive deployment
9+ activate Octopod Frontend
10+
11+ Octopod Frontend ->>+ Octopod Backend: Archive deployment
12+
13+
14+ Octopod Backend ->>+ Control Scipts: archive – Archive the deployment
15+ Control Scipts -->>- Octopod Backend: Deployment archive started
16+
17+ Octopod Backend ->>+ Octopod DB: Set deployment state to ArchivePending
18+ Octopod DB -->>- Octopod Backend: Deployment status set
19+
20+ loop
21+ Octopod Backend ->>+ Control Scipts: archive_check – Is the deployment archived?
22+ Control Scipts -->>- Octopod Backend: deployment is not archived
23+ end
24+
25+ Octopod Backend ->>+ Control Scipts: archive_check – Is the deployment archived?
26+ Control Scipts -->>- Octopod Backend: deployment is archived!
27+
28+ Octopod Backend ->>+ Octopod DB: Set deployment state to Archived
29+ Octopod DB -->>- Octopod Backend: Deployment status set
30+
31+ deactivate Octopod Backend
docs/diagrams/src/technical-architecture-cleanup-via-cli.mmddeleted−20
@@ -1,20 +0,0 @@
1−sequenceDiagram
2− participant octo CLI
3− participant Octopod Server
4− participant PostgreSQL
5− participant UI
6− participant Octopod Server/BgWorker
7− participant Octopod Server/StatusUpdater
8− participant ControlScripts
9− participant KubeAPI
10− octo CLI->>Octopod Server: cleanup(name)
11− Octopod Server->>Octopod Server/BgWorker: cleanup
12− Octopod Server-->>octo CLI: done
13− Octopod Server/BgWorker->>ControlScripts: cleanup
14− ControlScripts->>KubeAPI: cleanup deployment
15− KubeAPI-->>ControlScripts: done
16− ControlScripts-->>Octopod Server/BgWorker: done
17− Octopod Server->>PostgreSQL: delete config and logs
18− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
19− UI->>Octopod Server: get deployments info
20− Octopod Server-->>UI: deployments info
docs/diagrams/src/technical-architecture-cleanup-via-ui.mmddeleted−19
@@ -1,19 +0,0 @@
1−sequenceDiagram
2− participant UI
3− participant Octopod Server
4− participant PostgreSQL
5− participant Octopod Server/BgWorker
6− participant Octopod Server/StatusUpdater
7− participant ControlScripts
8− participant KubeAPI
9− UI->>Octopod Server: cleanup(name)
10− Octopod Server->>Octopod Server/BgWorker: cleanup
11− Octopod Server-->>UI: done
12− Octopod Server/BgWorker->>ControlScripts: cleanup
13− ControlScripts->>KubeAPI: cleanup deployment resources
14− KubeAPI-->>ControlScripts: done
15− ControlScripts-->>Octopod Server/BgWorker: done
16− Octopod Server->>PostgreSQL: delete config and logs
17− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
18− UI->>Octopod Server: get deployments info
19− Octopod Server-->>UI: deployments info
docs/diagrams/src/technical-architecture-cleanup.mmdnew file+14
@@ -0,0 +1,14 @@
1+sequenceDiagram
2+ participant Octopod Backend
3+ participant Control Scipts
4+ participant Octopod DB
5+
6+ activate Octopod Backend
7+
8+ Octopod Backend ->>+ Control Scipts: cleanup – Clean up deployment resouces
9+ Control Scipts -->>- Octopod Backend: Deployment resouces cleaned up
10+
11+ Octopod Backend ->>+ Octopod DB: Delete everything related to the deployment
12+ Octopod DB -->>- Octopod Backend: Everything deleted
13+
14+ deactivate Octopod Backend
docs/diagrams/src/technical-architecture-create-via-cli.mmddeleted−41
@@ -1,41 +0,0 @@
1−sequenceDiagram
2− participant octo CLI
3− participant Octopod Server
4− participant PostgreSQL
5− participant UI
6− participant Octopod Server/BgWorker
7− participant Octopod Server/StatusUpdater
8− participant ControlScripts
9− participant KubeAPI
10− octo CLI->>Octopod Server: create(name, tag, [override])
11− Octopod Server->>PostgreSQL: store config, status=CreatePending
12− alt name already exists
13− PostgreSQL->>Octopod Server: error: deployment already exists
14− Octopod Server-->>octo CLI: error: deployment already exists
15− else
16− alt tag not found
17− Octopod Server-->>octo CLI: error: tag not found
18− else
19− PostgreSQL-->>Octopod Server: ok
20− Octopod Server-xUI: event FrontendPleaseUpdateEverything
21− UI->>Octopod Server: get deployments info
22− Octopod Server-->>UI: deployments info
23− Octopod Server->>Octopod Server/BgWorker: create
24− Octopod Server-->>octo CLI: done
25− Octopod Server/BgWorker->>ControlScripts: create
26− ControlScripts->>KubeAPI: create deployment
27− KubeAPI-->>ControlScripts: done
28− ControlScripts-->>Octopod Server/BgWorker: done
29− Octopod Server/BgWorker->>PostgreSQL: write logs
30− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
31− UI->>Octopod Server: get deployments info
32− Octopod Server-->>UI: deployments info
33− Note over Octopod Server/StatusUpdater: wait 5 minutes
34− loop check deployment status every 30 seconds
35− Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure
36− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
37− UI->>Octopod Server: get deployments info
38− Octopod Server-->>UI: deployments info
39− end
40− end
41− end
docs/diagrams/src/technical-architecture-create-via-ui.mmddeleted−40
@@ -1,40 +0,0 @@
1−sequenceDiagram
2− participant UI
3− participant Octopod Server
4− participant PostgreSQL
5− participant Octopod Server/BgWorker
6− participant Octopod Server/StatusUpdater
7− participant ControlScripts
8− participant KubeAPI
9− UI->>Octopod Server: create(name, tag, [override])
10− Octopod Server->>PostgreSQL: store config, status=CreatePending
11− alt name already exists
12− PostgreSQL-->>Octopod Server: error: deployment already exists
13− Octopod Server-->>UI: error: deployment already exists
14− else
15− alt tag not found
16− Octopod Server-->>UI: error: tag not found
17− else
18− PostgreSQL-->>Octopod Server: ok
19− Octopod Server-xUI: event FrontendPleaseUpdateEverything
20− UI->>Octopod Server: get deployments info
21− Octopod Server-->>UI: deployments info
22− Octopod Server->>Octopod Server/BgWorker: create
23− Octopod Server-->>UI: done
24− Octopod Server/BgWorker->>ControlScripts: create
25− ControlScripts->>KubeAPI: create deployment
26− KubeAPI-->>ControlScripts: done
27− ControlScripts-->>Octopod Server/BgWorker: done
28− Octopod Server/BgWorker->>PostgreSQL: write logs
29− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
30− UI->>Octopod Server: get deployments info
31− Octopod Server-->>UI: deployments info
32− Note over Octopod Server/StatusUpdater: wait 5 minutes
33− loop check deployment status every 30 seconds
34− Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure
35− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
36− UI->>Octopod Server: get deployments info
37− Octopod Server-->>UI: deployments info
38− end
39− end
40− end
docs/diagrams/src/technical-architecture-create.mmdnew file+41
@@ -0,0 +1,41 @@
1+sequenceDiagram
2+ participant User
3+ participant Octopod Frontend
4+ participant Octopod Backend
5+ participant Control Scipts
6+ participant Octopod DB
7+
8+ User ->>+ Octopod Frontend: Create deployment
9+ activate Octopod Frontend
10+
11+ Octopod Frontend ->>+ Octopod Backend: Create deployment
12+ Octopod Backend ->>+ Control Scipts: config_check – Check deployment config
13+ Control Scipts ->>- Octopod Backend: Deployment config OK
14+ Octopod Backend ->>+ Octopod DB: Create deployment
15+ Octopod DB -->>- Octopod Backend: Deployment Created
16+
17+ Octopod Backend -->> Octopod Frontend: Delpoyment created
18+ Octopod Frontend -->>- User: Close deployment popup
19+
20+ Octopod Backend ->>+ Control Scipts: info – Get deployment metadata
21+ Control Scipts -->>- Octopod Backend: Deployment metadata
22+
23+ Octopod Backend ->>+ Octopod DB: Set deployment metadata
24+ Octopod DB -->>- Octopod Backend: Deployment metadata set
25+
26+
27+ Octopod Backend ->>+ Control Scipts: create – Create the delpoyment
28+ Control Scipts -->>- Octopod Backend: Deployment creation started
29+
30+ loop
31+ Octopod Backend ->>+ Control Scipts: check – Is the deployment up?
32+ Control Scipts -->>- Octopod Backend: deployment is not up
33+ end
34+
35+ Octopod Backend ->>+ Control Scipts: check – Is the deployment up?
36+ Control Scipts -->>- Octopod Backend: deployment is up!
37+
38+ Octopod Backend ->>+ Octopod DB: Set deployment state to Running
39+ Octopod DB -->>- Octopod Backend: Deployment status set
40+
41+ deactivate Octopod Backend
docs/diagrams/src/technical-architecture-deployment-states-fsm.mmd+1
@@ -14,3 +14,4 @@ stateDiagram-v2
1414 UpdatePending --> Failure: 5m passed and 'check' said "nok"
1515 ArchivePending --> Archived: 30s passed and 'archive_check' said "ok"
1616 ArchivePending --> ArchivePending: 30s passed and 'archive_check' said "nok"
17+ Archived --> CleanupFailed
docs/diagrams/src/technical-architecture-restore-via-cli.mmddeleted−41
@@ -1,41 +0,0 @@
1−sequenceDiagram
2− participant octo CLI
3− participant Octopod Server
4− participant PostgreSQL
5− participant UI
6− participant Octopod Server/BgWorker
7− participant Octopod Server/StatusUpdater
8− participant ControlScripts
9− participant KubeAPI
10− octo CLI->>Octopod Server: restore(name)
11− Octopod Server->>PostgreSQL: status=CreatePending
12− alt name not found
13− PostgreSQL-->>Octopod Server: error: name not found
14− Octopod Server-->>octo CLI: error: name not found
15− else
16− alt tag not found
17− Octopod Server-->>octo CLI: error: tag not found
18− else
19− PostgreSQL-->>Octopod Server: ok
20− Octopod Server-xUI: event FrontendPleaseUpdateEverything
21− UI->>Octopod Server: get deployments info
22− Octopod Server-->>UI: deployments info
23− Octopod Server->>Octopod Server/BgWorker: create
24− Octopod Server-->>octo CLI: done
25− Octopod Server/BgWorker->>ControlScripts: create
26− ControlScripts->>KubeAPI: restore deployment
27− KubeAPI-->>ControlScripts: done
28− ControlScripts-->>Octopod Server/BgWorker: done
29− Octopod Server/BgWorker->>PostgreSQL: write logs
30− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
31− UI->>Octopod Server: get deployments info
32− Octopod Server-->>UI: deployments info
33− Note over Octopod Server/StatusUpdater: wait 5 minutes
34− loop check deployment status every 30 seconds
35− Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure
36− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
37− UI->>Octopod Server: get deployments info
38− Octopod Server-->>UI: deployments info
39− end
40− end
41− end
docs/diagrams/src/technical-architecture-restore-via-ui.mmddeleted−40
@@ -1,40 +0,0 @@
1−sequenceDiagram
2− participant UI
3− participant Octopod Server
4− participant PostgreSQL
5− participant Octopod Server/BgWorker
6− participant Octopod Server/StatusUpdater
7− participant ControlScripts
8− participant KubeAPI
9− UI->>Octopod Server: restore(name)
10− Octopod Server->>PostgreSQL: status=CreatePending
11− alt name not found
12− PostgreSQL-->>Octopod Server: error: name not found
13− Octopod Server-->>UI: error: name not found
14− else
15− alt tag not found
16− Octopod Server-->>UI: error: tag not found
17− else
18− PostgreSQL-->>Octopod Server: ok
19− Octopod Server-xUI: event FrontendPleaseUpdateEverything
20− UI->>Octopod Server: get deployments info
21− Octopod Server-->>UI: deployments info
22− Octopod Server->>Octopod Server/BgWorker: create
23− Octopod Server-->>UI: done
24− Octopod Server/BgWorker->>ControlScripts: create
25− ControlScripts->>KubeAPI: restore deployment
26− KubeAPI-->>ControlScripts: done
27− ControlScripts-->>Octopod Server/BgWorker: done
28− Octopod Server/BgWorker->>PostgreSQL: write logs
29− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
30− UI->>Octopod Server: get deployments info
31− Octopod Server-->>UI: deployments info
32− Note over Octopod Server/StatusUpdater: wait 5 minutes
33− loop check deployment status every 30 seconds
34− Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure
35− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
36− UI->>Octopod Server: get deployments info
37− Octopod Server-->>UI: deployments info
38− end
39− end
40− end
docs/diagrams/src/technical-architecture-restore.mmdnew file+42
@@ -0,0 +1,42 @@
1+sequenceDiagram
2+ participant User
3+ participant Octopod Frontend
4+ participant Octopod Backend
5+ participant Control Scipts
6+ participant Octopod DB
7+
8+ User ->>+ Octopod Frontend: Restore deployment
9+ activate Octopod Frontend
10+
11+ Octopod Frontend ->>+ Octopod Backend: Restore deployment
12+ Octopod Backend ->>+ Control Scipts: config_check – Check deployment config
13+ Control Scipts ->>- Octopod Backend: Deployment config OK
14+
15+ Octopod Backend -->> Octopod Frontend: Delpoyment restore started
16+ Octopod Frontend -->>- User: Deployment restore started
17+
18+
19+ Octopod Backend ->>+ Control Scipts: info – Get deployment metadata
20+ Control Scipts -->>- Octopod Backend: Deployment metadata
21+
22+ Octopod Backend ->>+ Octopod DB: Set deployment metadata
23+ Octopod DB -->>- Octopod Backend: Deployment metadata set
24+
25+ Octopod Backend ->>+ Control Scipts: unarchive – Unarchive the delpoyment
26+ Control Scipts -->>- Octopod Backend: Deployment unarchiving started
27+
28+ Octopod Backend ->>+ Octopod DB: Set deployment state to CreatePending
29+ Octopod DB -->>- Octopod Backend: Deployment status set
30+
31+ loop
32+ Octopod Backend ->>+ Control Scipts: check – Is the deployment up?
33+ Control Scipts -->>- Octopod Backend: deployment is not up
34+ end
35+
36+ Octopod Backend ->>+ Control Scipts: check – Is the deployment up?
37+ Control Scipts -->>- Octopod Backend: deployment is up!
38+
39+ Octopod Backend ->>+ Octopod DB: Set deployment state to Running
40+ Octopod DB -->>- Octopod Backend: Deployment status set
41+
42+ deactivate Octopod Backend
docs/diagrams/src/technical-architecture-update-via-cli.mmddeleted−41
@@ -1,41 +0,0 @@
1−sequenceDiagram
2− participant octo CLI
3− participant Octopod Server
4− participant PostgreSQL
5− participant UI
6− participant Octopod Server/BgWorker
7− participant Octopod Server/StatusUpdater
8− participant ControlScripts
9− participant KubeAPI
10− octo CLI->>Octopod Server: create(name, tag, [override])
11− Octopod Server->>PostgreSQL: store config, status=UpdatePending
12− alt name not found
13− PostgreSQL-->>Octopod Server: error: name not found
14− Octopod Server-->>octo CLI: error: name not found
15− else
16− alt tag not found
17− Octopod Server-->>octo CLI: error: tag not found
18− else
19− PostgreSQL-->>Octopod Server: ok
20− Octopod Server-xUI: event FrontendPleaseUpdateEverything
21− UI->>Octopod Server: get deployments info
22− Octopod Server-->UI: deployments info
23− Octopod Server->>Octopod Server/BgWorker: update
24− Octopod Server-->>octo CLI: done
25− Octopod Server/BgWorker->>ControlScripts: update
26− ControlScripts->>KubeAPI: upgrade deployment
27− KubeAPI-->>ControlScripts: done
28− ControlScripts-->>Octopod Server/BgWorker: done
29− Octopod Server/BgWorker->>PostgreSQL: write logs
30− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
31− UI->>Octopod Server: get deployments info
32− Octopod Server-->UI: deployments info
33− Note over Octopod Server/StatusUpdater: wait 5 minutes
34− loop check deployment status every 30 seconds
35− Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure
36− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
37− UI->>Octopod Server: get deployments info
38− Octopod Server-->UI: deployments info
39− end
40− end
41− end
docs/diagrams/src/technical-architecture-update-via-ui.mmddeleted−40
@@ -1,40 +0,0 @@
1−sequenceDiagram
2− participant UI
3− participant Octopod Server
4− participant PostgreSQL
5− participant Octopod Server/BgWorker
6− participant Octopod Server/StatusUpdater
7− participant ControlScripts
8− participant KubeAPI
9− UI->>Octopod Server: create(name, tag, [override])
10− Octopod Server->>PostgreSQL: store config, status=UpdatePending
11− alt name not found
12− PostgreSQL-->>Octopod Server: error: name not found
13− Octopod Server-->>UI: error: name not found
14− else
15− alt tag not found
16− Octopod Server-->>UI: error: tag not found
17− else
18− PostgreSQL-->>Octopod Server: ok
19− Octopod Server-xUI: event FrontendPleaseUpdateEverything
20− UI->>Octopod Server: get deployments info
21− Octopod Server-->>UI: deployments info
22− Octopod Server->>Octopod Server/BgWorker: update
23− Octopod Server-->>UI: done
24− Octopod Server/BgWorker->>ControlScripts: update
25− ControlScripts->>KubeAPI: upgrade deployment
26− KubeAPI-->>ControlScripts: done
27− ControlScripts-->>Octopod Server/BgWorker: done
28− Octopod Server/BgWorker->>PostgreSQL: write logs
29− Octopod Server/BgWorker-xUI: event FrontendPleaseUpdateEverything
30− UI->>Octopod Server: get deployments info
31− Octopod Server-->>UI: deployments info
32− Note over Octopod Server/StatusUpdater: wait 5 minutes
33− loop check deployment status every 30 seconds
34− Octopod Server/StatusUpdater->>PostgreSQL: status=Running/Failure
35− Octopod Server/StatusUpdater-xUI: event FrontendPleaseUpdateEverything
36− UI->>Octopod Server: get deployments info
37− Octopod Server-->>UI: deployments info
38− end
39− end
40− end
docs/diagrams/src/technical-architecture-update.mmdnew file+46
@@ -0,0 +1,46 @@
1+sequenceDiagram
2+ participant User
3+ participant Octopod Frontend
4+ participant Octopod Backend
5+ participant Control Scipts
6+ participant Octopod DB
7+
8+ User ->>+ Octopod Frontend: Update deployment
9+ activate Octopod Frontend
10+
11+ Octopod Frontend ->>+ Octopod Backend: Update deployment
12+ Octopod Backend ->>+ Control Scipts: config_check – Check deployment config
13+ Control Scipts ->>- Octopod Backend: Deployment config OK
14+
15+ Octopod Backend -->> Octopod Frontend: Delpoyment update started
16+ Octopod Frontend -->>- User: Close deployment popup
17+
18+
19+ Octopod Backend ->>+ Octopod DB: Update deployment config
20+ Octopod DB -->>- Octopod Backend: Deployment config updated
21+
22+
23+ Octopod Backend ->>+ Control Scipts: info – Get deployment metadata
24+ Control Scipts -->>- Octopod Backend: Deployment metadata
25+
26+ Octopod Backend ->>+ Octopod DB: Set deployment metadata
27+ Octopod DB -->>- Octopod Backend: Deployment metadata set
28+
29+ Octopod Backend ->>+ Control Scipts: update – Update the delpoyment
30+ Control Scipts -->>- Octopod Backend: Deployment update started
31+
32+ Octopod Backend ->>+ Octopod DB: Set deployment state to UpdatePending
33+ Octopod DB -->>- Octopod Backend: Deployment status set
34+
35+ loop
36+ Octopod Backend ->>+ Control Scipts: check – Is the deployment up?
37+ Control Scipts -->>- Octopod Backend: deployment is not up
38+ end
39+
40+ Octopod Backend ->>+ Control Scipts: check – Is the deployment up?
41+ Control Scipts -->>- Octopod Backend: deployment is up!
42+
43+ Octopod Backend ->>+ Octopod DB: Set deployment state to Running
44+ Octopod DB -->>- Octopod Backend: Deployment status set
45+
46+ deactivate Octopod Backend
docs/en/Control_scripts.md+141−167
@@ -1,86 +1,38 @@
11# Control scripts
22
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [General behavior](#general-behavior)
7−- [Scripts](#scripts)
8− - [🔁 init](#-init)
9− - [Description](#description)
10− - [Sample implementation](#sample-implementation)
11− - [✨ create](#-create)
12− - [Description](#description-1)
13− - [Execution example](#execution-example)
14− - [Sample implementation](#sample-implementation-1)
15− - [🔧 update](#-update)
16− - [Description](#description-2)
17− - [Execution example](#execution-example-1)
18− - [Sample implementation](#sample-implementation-2)
19− - [🗃 archive](#-archive)
20− - [Description](#description-3)
21− - [Execution example](#execution-example-2)
22− - [Sample implementation](#sample-implementation-3)
23− - [✅ check](#-check)
24− - [Description](#description-4)
25− - [Execution example](#execution-example-3)
26− - [Sample implementation](#sample-implementation-4)
27− - [🚮 cleanup](#-cleanup)
28− - [Description](#description-5)
29− - [Execution example](#execution-example-4)
30− - [Sample implementation](#sample-implementation-5)
31− - [🗃✅ archive_check](#-archive_check)
32− - [Description](#description-6)
33− - [Execution example](#execution-example-5)
34− - [Sample implementation](#sample-implementation-6)
35− - [🐋✅ tag_check](#-tag_check)
36− - [Description](#description-7)
37− - [Execution example](#execution-example-6)
38− - [👀 info](#-info)
39− - [Description](#description-8)
40− - [Execution example](#execution-example-7)
41− - [Sample implementation](#sample-implementation-7)
42− - [🔔 notifications](#-notifications)
43− - [Description](#description-9)
44− - [Execution example](#execution-example-8)
45−
46−</details>
473
484## General behavior
495
506All _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.
517
52−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.
8+Control scripts can be roughly split into two categories: (1) control scripts involved in the deployment lifecycle and (2) service control scripts which handle everything else.
539
54−> *NOTE: Logs from `check`, `archive_check` and `tag_check` are not collected because they are called very often.*
10+## Deployment lifecycle scripts
5511
56−There are four arguments that are passed to **all** *scripts*. The first three arguments come from the [_Kubernetes ConfigMap_][configmap]:
12+_stdout_ and _stderr_ output from the most important scripts involved in the deployment lifecycle will be collected and stored. DevOps engineers can then view these logs from the _octo CLI_, should that be needed.
13+
14+All of the _scripts_ in this category receive the exact same set of command line arguments.
15+
16+There are four "static" arguments that are passed to all *scripts* in this category. The first three arguments come from the [_Kubernetes ConfigMap_][configmap]:
5717* `--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.
5818* `--base-domain` – the base domain. It can be useful for generating the URLs of deployments.
5919* `--namespace` – The namespace in which the deployment should be created.
6020* `--name` – The name of the deployment supplied in the _Web UI_. It can be useful for generating the deployment URL.
6121
62−<a id="star"></a>*NOTE:* If an argument is marked with a ⭐, it means that the argument can be passed any number of times.
63−
64−## Scripts
22+Additionally, _scripts_ in this category receive the deployment configuration – a set of key/value pairs. There are two types of configurations that can be set from the _Web UI_: deployment
23+configuration and application configuration.
6524
66−### 🔁 init
25+Both types of configurations can have any number of key/value pairs. Every pair is passed a separate command line argument:
6726
68−#### Description
27+* `--application-config <KEY>=<VALUE>` represents an application configuration key/value pair.
28+* `--deployment-config <KEY>=<VALUE>` represents an deployment configuration key/value pair.
6929
70−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.
30+Any of these two command line arguments can be passed any number of times.
7131
72−It is guaranteed that this script will be called **before** any of the other scripts.
73−
74−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.
75−
76−Unlike all other scripts, this script receives no arguments.
77−
78−#### Sample implementation
32+So, a _script_ might be called something like this:
7933
8034```bash
81−mkdir $HOME/.ssh
82−echo -e "Host github.com\nHostname github.com\nPort 22\nUser git\nIdentityFile $HOME/.ssh/deploy.key" > $HOME/.ssh/config
83−echo "MY_DEPLOY_KEY" > $HOME/.ssh/deploy.key"
35+create --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" --application-config "EMAIL_TOKEN=123123" --application-config "SECRET_BUTTON_ENABLED=True" --deployment-config "FANCY_DATABASE=True"
8436```
8537
8638### ✨ create
@@ -89,19 +41,6 @@ echo "MY_DEPLOY_KEY" > $HOME/.ssh/deploy.key"
8941
9042Creates a new deployment in the _Kubernetes_ cluster.
9143
92−This script receives the following additional command-line arguments as input:
93−* `--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.)
94−* `--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.
95−* `--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.
96−
97−#### Execution example
98−
99−The script might be called something like this:
100−
101−```bash
102−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"
103−```
104−
10544#### Sample implementation
10645
10746```bash
@@ -115,24 +54,38 @@ helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
11554 --timeout 300
11655```
11756
118−### 🔧 update
57+### 🎛✅ config_check
11958
12059#### Description
12160
122−Updates a deployment in _Kubernetes_ to a new *Docker Image tag*.
61+This script is called right before [`create`](#-create) and [`update`](#-update) scripts to check that the given set of arguments is valid input for other commands. This is run synchronously and the _stdout_ text is report to the user in the _Web UI_. This means that the command should run relatively fast. You can check thing like that the needed arguments are present and that the required docker tags exist in the docker registry.
12362
124−This script receives the same additional command-line arguments as [`create`](#-create):
125−* `--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.)
126−* `--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.
127−* `--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.
63+As with other commands, exit code `0` indicates that everything is fine and a non-zero exit code indicates that something is wrong and _stdout_ should be shown to the user.
12864
129−#### Execution example
65+### 👀 info
13066
131−The script might be called something like this:
67+#### Description
68+
69+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.
70+
71+The script should return the metadata as a two-column CSV table to _stdout_:
72+
73+```
74+app,https://foo.example.com
75+api,https://api.foo.example.com
76+```
77+
78+#### Sample implementation
13279
13380```bash
134−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"
81+echo "app,https://${name}.example.com"
82+echo "api,https://api.${name}.example.com"
13583```
84+### 🔧 update
85+
86+#### Description
87+
88+Updates a deployment in _Kubernetes_ to a new *Docker Image tag*.
13689
13790#### Sample implementation
13891
@@ -157,15 +110,7 @@ Deleting the _Persistent Volume Claims_ should be done in the [`cleanup`](#-clea
157110
158111This script should in some sense be the inverse of [`create`](#-create) (up to _Persistent Volumes_).
159112
160−This script receives only [the default command-line arguments](#general-behavior) as input.
161−
162−#### Execution example
163−
164−The script might be called something like this:
165−
166−```bash
167−archive --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button"
168−```
113+A good way of doing this is to scale the number of replicas of your server to 0.
169114
170115#### Sample implementation
171116
@@ -173,17 +118,18 @@ archive --project-name "Cactus store" --base-domain "cactus-store.com" --namespa
173118helm delete "$name" --purge
174119```
175120
121+### 🗃↩️ unarchive
122+
123+#### Description
124+
125+This command should undo whatever the [`archive`](#-archive) command did. So, if the [`archive`](#-archive) command scaled the server to 0 replicas, this command should scale it back up to the number of replicas required for normal operation.
126+
176127### ✅ check
177128
178129#### Description
179130
180131This script checks the status of the deployment.
181132
182−This script receives the following additional command-line arguments as input:
183−* `--tag` – The _Docker Image tag_ that should be checked.
184−* `--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.
185−* `--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.
186−
187133If 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.
188134
189135You can specify exactly what error occured using exit codes:
@@ -191,14 +137,6 @@ You can specify exactly what error occured using exit codes:
191137- `2` – the deployment is partially down (some containers are unhealthy).
192138- `3` – tag mismatch, the deployment has not been updated to the expected version.
193139
194−#### Execution example
195−
196−The script might be called something like this:
197−
198−```bash
199−check ---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"
200−```
201−
202140#### Sample implementation
203141
204142```bash
@@ -214,16 +152,6 @@ Cleans up any persistent resources a deployment might have allocated, such as _P
214152
215153This script will always be called **after** [`archive`](#-archive) has been called on the given deployment.
216154
217−This script receives only [the default command-line arguments](#general-behavior) as input.
218−
219−#### Execution example
220−
221−The script might be called something like this:
222−
223−```bash
224−cleanup --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button"
225−```
226−
227155#### Sample implementation
228156
229157```bash
@@ -238,106 +166,152 @@ This script checks that a given deployment really has been archived and is no lo
238166
239167If 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.
240168
169+#### Sample implementation
241170
242−This script receives only [the default command-line arguments](#general-behavior) as input.
171+```bash
172+helm status $name
173+```
243174
244−#### Execution example
175+## Service control scripts
245176
246−The script might be called something like this:
177+### 🔁 init
247178
248−```bash
249−archive_check --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button"
250−```
179+#### Description
180+
181+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.
182+
183+It is guaranteed that this script will be called **before** any of the other scripts.
184+
185+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.
186+
187+Unlike all other scripts, this script receives no arguments.
251188
252189#### Sample implementation
253190
254191```bash
255−helm status $name
192+mkdir $HOME/.ssh
193+echo -e "Host github.com\nHostname github.com\nPort 22\nUser git\nIdentityFile $HOME/.ssh/deploy.key" > $HOME/.ssh/config
194+echo "MY_DEPLOY_KEY" > $HOME/.ssh/deploy.key"
256195```
257196
258−### 🐋✅ tag_check
197+### 🔔 notifications
259198
260199#### Description
261200
262−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_.
201+This script gets called every time a deployment changes its status (apart from creation and deletion). It might be useful if you want to send notifications about certain deployment status transitions. The complete list of statuses and their transitions can be found in the [technical architecture document](Technical_architecture.md#️-deployment-state-transitions).
202+
203+It is optional and can be omitted altogether.
263204
264205This script receives the following additional command-line arguments as input:
265−* `--tag` – The _Docker Image tag_ that should be checked.
266−* `--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.
267−* `--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.
206+* `--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.
207+* `--base-domain` – the base domain. It can be useful for generating the URLs of deployments.
208+* `--namespace` – The namespace in which the deployment should be created.
209+* `--name` – The name of the deployment supplied in the _Web UI_. It can be useful for generating the deployment URL.
210+* `--old-status` – The previous status the deployment was in.
211+* `--new-status` – The new status the deployment transitioned to.
212+
213+The last two arguments can have one of the following values:
214+- `Running`
215+- `GenericFailure`
216+- `TagMismatch`
217+- `PartialAvailability`
218+- `CreatePending`
219+- `UpdatePending`
220+- `ArchivePending`
221+- `Archived`
222+- `CleanupFailed`
268223
269224#### Execution example
270225
271226The script might be called something like this:
272227
273228```bash
274−tag_check --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"
229+notification --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" --old-status "UpdatePending" --new-status "Running"
275230```
276231
277−### 👀 info
232+### 🎛🔧 deployment_config
278233
279234#### Description
280235
281−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.
236+This script is used to generate default deployment configuration key/value pairs. This configuration is shown to the user in the _Web UI_ with the ability to modify it.
282237
283−This script receives the following additional command-line arguments as input:
284−* `--tag` – The _Docker Image tag_ that should be checked.
285−* `--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.
286−* `--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.
238+The key/value pairs are read from the _stdout_ stream of the script. It should be formatted as a CSV file with two columns.
287239
288−The script should return the metadata as a two-column CSV table:
240+This script receives the following arguments:
289241
290−```
291−app,https://foo.example.com
292−api,https://api.foo.example.com
242+* `--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.
243+* `--base-domain` – the base domain. It can be useful for generating the URLs of deployments.
244+* `--namespace` – The namespace in which the deployment should be created.
245+
246+#### Sample implementation
247+
248+```bash
249+echo "image.tag,"
250+echo "application_api_version,1.0"
293251```
294252
295−This script receives only [the default command-line arguments](#general-behavior) as input.
253+### 🎛🔧🔑 deployment_config_keys
296254
297−#### Execution example
255+#### Description
298256
299−The script might be called something like this:
257+This script is used to get the set of keys that the deployment configuration can have. This is useful to suggest possible values when the user wants to add new values to in the _Web UI_.
258+
259+The keys are read from the _stdout_ stream of the script. It should be formatted as a CSV file with one column (a list separated by newlines).
260+
261+This script receives the same arguments as the [`deployment_config`](#-deployment_config) command.
300262
301−```bash
302−info --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"
303−```
304263
305264#### Sample implementation
306265
307266```bash
308−echo "app,https://${name}.example.com"
309−echo "api,https://api.${name}.example.com"
267+echo "image.tag"
268+echo "application_api_version"
269+echo "database_version"
310270```
311271
312−### 🔔 notifications
272+### 🎛📱 application_config
313273
314274#### Description
315275
316−This script gets called every time a deployment changes its status (apart from creation and deletion). It might be useful if you want to send notifications about certain deployment status transitions. The complete list of statuses and their transitions can be found in the [technical architecture document](Technical_architecture.md#️-deployment-state-transitions).
276+This script is used to generate default application configuration key/value pairs. This configuration is shown to the user in the _Web UI_ with the ability to modify it.
317277
318−It is optional and can be omitted altogether.
278+The key/value pairs are read from the _stdout_ stream of the script. It should be formatted as a CSV file with two columns.
319279
320−This script receives the following additional command-line arguments as input:
321−* `--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.)
322−- `--old-status` – The previous status the deployment was in.
323−- `--new-status` – The new status the deployment transitioned to.
280+This script receives the same arguments as the [`deployment_config`](#-deployment_config) command.
324281
325−The last two arguments can have one of the following values:
326−- `Running`
327−- `GenericFailure`
328−- `TagMismatch`
329−- `PartialAvailability`
330−- `CreatePending`
331−- `UpdatePending`
332−- `ArchivePending`
333−- `Archived`
282+Additionally, the script receives the deployment configuration key/value pairs as described in the ["Deployment lifecycle scripts" section](#deployment-lifecycle-scripts): any number of `--deployment-config <KEY>=<VALUE>` arguments.
334283
335−#### Execution example
284+#### Sample implementation
336285
337−The script might be called something like this:
286+```bash
287+echo "image.tag,"
288+echo "application_api_version,1.0"
289+```
290+
291+### 🎛📱🔑 application_config_keys
292+
293+#### Description
294+
295+This script is used to get the set of keys that the application configuration can have. This is useful to suggest possible values when the user wants to add new values to in the _Web UI_.
296+
297+The keys are read from the _stdout_ stream of the script. It should be formatted as a CSV file with one column (a list separated by newlines).
298+
299+This script receives the same arguments as the [`application_config` script](#-application_config).
300+
301+#### Sample implementation
338302
339303```bash
340−notification --project-name "Cactus store" --base-domain "cactus-store.com" --namespace "cactus" --name "orange-button" --tag "c9bbc3fcc69e5aa094bca110c6f79419ab7be77a" --old-status "UpdatePending" --new-status "Running"
304+echo "image.tag"
305+echo "application_api_version"
306+echo "database_version"
341307```
342308
309+<br />
310+
311+<p align="center">
312+ <i>Star the project of you like it</i>
313+</p>
314+
315+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
316+
343317[configmap]: https://kubernetes.io/docs/concepts/configuration/configmap/
docs/en/Helm-based_deployment_guide.md+29−24
@@ -21,24 +21,21 @@ Click on the _New Deployment_ button:
2121
2222### The config
2323
24−Here you can fill your deployment parameters. Let's have it filled up!
24+As you can see we filled some parameters for you ― it's very convinient, because you don't want fill URL to your private helm repo over and over again, right?
2525
26−![](../images/octopod_deployment_filled.png)
27−
28−**Name** ― we've chosen `wordpress`, but you can choose whatever name you like.
26+All these parameters can be modified at the chart level, [here](../../charts/octopod/values.yaml#L90) is an example. Or you can, of course, override them manually from the Octopod UI.
2927
30−**Tag** ― `5.8.0`. We took the tag name from [chart parameters](https://github.com/bitnami/charts/blob/master/bitnami/wordpress/Chart.yaml#L4)
28+One thing is left to fill, though.
3129
32−**App Overrides:**
30+![](../images/octopod_deployment_filled.png)
3331
32+**Name** ― we've chosen `wordpress`, but you can choose whatever name you like.
3433
35−| Key | Value |
36−| -----------------: | ------------------ |
37−| `ingress.enabled` | `true` |
38−| `ingress.hostname` | `wordpress.lvh.me` |
34+Octopod loads all overrides from the [chart values](https://github.com/bitnami/charts/blob/master/bitnami/wordpress/values.yaml) dynamically for each chart you want to deploy.
3935
36+Try to add new App override to see list of available keys for the wordpress chart.
4037
41−We took these overrides from the [chart documentation](https://github.com/bitnami/charts/tree/master/bitnami/wordpress#traffic-exposure-parameters). Basically you can tweak any parameters from there.
38+![](../images/octopod_app_keys_list.png)
4239
4340### Deploy
4441
@@ -52,9 +49,7 @@ So here you have it, your first Octopod deployment!
5249
5350## Going further
5451
55−Right now you may be wondering, how did Octopod take wordpress from the bitnami repository when we haven't filled any repo information whatsoever? This is because we've set this up on the chart level [here](../../charts/octopod/values.yaml#L90).
56−
57−You can override all of this using deployment overrides. Let's dive right in!
52+As we said earlier you can override all default parameters using Octopod UI. Let's dive right in!
5853
5954First, let's archive our wordpress deployment:
6055
@@ -68,8 +63,6 @@ And create one more deployment, this time using a different set of overrides:
6863
6964**Name:** octopod-internal
7065
71−**Tag:** 1.3.1
72−
7366**App Overrides:**
7467
7568| Key | Value |
@@ -80,14 +73,26 @@ And create one more deployment, this time using a different set of overrides:
8073
8174**Deployment Overrides:**
8275
83−| Key | Value |
84−| ----------------: | ------------------------------------- |
85−| `chart_name` | `octopod` |
86−| `chart_repo_name` | `typeable` |
87−| `chart_repo_url` | `https://typeable.github.io/octopod/` |
88−| `chart_version` | `0.5.1` |
76+| Key | Value |
77+| ---------------: | ------------------------------------- |
78+| `chart_name` | `octopod` |
79+| `chart_repo_url` | `https://typeable.github.io/octopod/` |
80+| `chart_version` | `0.6.0` |
81+
82+## Where can I get these values?
83+
84+As in the previous example Octopod parsed the _App Overrides_ from the [chart values](../../charts/octopod/values.yaml), but the _Deployment Overrides_ are passed as configuration to the control scripts. You can read more about these parameters in the [control script docs](../../helm-control-scripts/README.md).
85+
86+`chart_repo_url`, `chart_name` parameters are usually provided by the chart developer. Version however is usually skipped and you encouraged to take the last one, but we think about this as a bad practice.
87+
88+If you're not sure where to get the `chart_version` parameter you can add repo locally using `helm repo add` and then execute `helm search repo <CHART_NAME> -l` too see the whole list of available chart versions.
89+
90+Now you have Octopod inside Octopod! Try to install your own chart with Octopod!
8991
92+<br />
9093
91−As in the previous example we took the _App Overrides_ from the [chart documentation](../../charts/octopod/README.md#Parameters), but the _Deployment Overrides_ are passed as configuration to the control scripts. You can read more about these parameters in the [control script docs](../../helm-control-scripts/README.md).
94+<p align="center">
95+ <i>Star the project of you like it</i>
96+</p>
9297
93−Now you have Octopod inside Octopod! Now you can install your own helm chart!
98+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/Integration.md+9−8
@@ -1,12 +1,5 @@
11# Integration into existing CI/CD pipelines
22
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [✨ Creating deployments](#-creating-deployments)
7−- [🚀 Updating deployments](#-updating-deployments)
8−
9−</details>
103
114You 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.
125
@@ -28,8 +21,16 @@ octo create -n $NAME -t $IMAGE_TAG
2821
2922## 🚀 Updating deployments
3023
31−Updating deployments is done using the same arguments, but you need to call `create` command, instead of the `update` command:
24+Updating deployments is done using the same arguments, but you need to call the `create` command, instead of the `update` command:
3225
3326```bash
3427octo update -n $NAME -t $IMAGE_TAG
3528```
29+
30+<br />
31+
32+<p align="center">
33+ <i>Star the project of you like it</i>
34+</p>
35+
36+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/Octo_user_guide.md+31−76
@@ -1,66 +1,20 @@
11# Octo CLI User Guide
22
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [Environment variables](#environment-variables)
7− - [`OCTOPOD_URL`](#octopod_url)
8− - [`TLS_CERT_PATH` and `TLS_KEY_PATH`](#tls_cert_path-and-tls_key_path)
9−- [Commands](#commands)
10− - [create](#create)
11− - [Description](#description)
12− - [Options](#options)
13− - [Usage example](#usage-example)
14− - [list](#list)
15− - [Description](#description-1)
16− - [Options](#options-1)
17− - [Usage example](#usage-example-1)
18− - [archive](#archive)
19− - [Description](#description-2)
20− - [Options](#options-2)
21− - [Usage example](#usage-example-2)
22− - [update](#update)
23− - [Description](#description-3)
24− - [Options](#options-3)
25− - [Usage example](#usage-example-3)
26− - [info](#info)
27− - [Description](#description-4)
28− - [Options](#options-4)
29− - [Usage example](#usage-example-4)
30− - [cleanup](#cleanup)
31− - [Description](#description-5)
32− - [Options](#options-5)
33− - [Usage example](#usage-example-5)
34− - [restore](#restore)
35− - [Description](#description-6)
36− - [Options](#options-6)
37− - [Usage example](#usage-example-6)
38− - [clean-archive](#clean-archive)
39− - [Description](#description-7)
40− - [Options](#options-7)
41− - [Usage example](#usage-example-7)
42− - [logs](#logs)
43− - [Description](#description-8)
44− - [Options](#options-8)
45− - [Usage example](#usage-example-8)
46−
47−</details>
48−
493## Environment variables
504
515All 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.
526
537### `OCTOPOD_URL`
548
55−> **_NOTE:_** this argument is **required** for _octo CLI_ to function.
56−
57−`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`.
9+`OCTOPOD_URL` is an environment variable _octo CLI_ reads to find your particular _Octopod Server_ installation. For example, it could contain `https://octopod-powerapp.example.com:443`.
5810
59−### `TLS_CERT_PATH` and `TLS_KEY_PATH`
11+### `OCTO_AUTHORIZATION_HEADER`
6012
61−`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_.
13+`OCTO_AUTHORIZATION_HEADER` is an environment variable that should contain the `Authorization` header to use to authenticate requests to your Octopod server. You can obtain the header value by running the following command after installing Octopod:
6214
63−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`.
15+```bash
16+kubectl -n octopod get secret <HELM_RELEASE_NAME>-cli-auth-secret -o jsonpath='{.data.cli-secret}' | base64 -d
17+```
6418
6519## Commands
6620
@@ -76,8 +30,8 @@ Creates a new deployment.
7630
7731- `-n,--name ARG` – The name of the deployment to create
7832- `-t,--tag ARG` – The _Docker tag_ to deploy
79−- `-e,--set-app-env-override ARG` [⭐](#star) – Set an application-level override. Expects a string in the format `KEY=VALUE`.
80−- `-o,--set-deployment-override ARG` [⭐](#star) – Set a deployment-level override. Expects a string in the format `KEY=VALUE`.
33+- `-e,--set-app-config ARG` [⭐](#star) – Set an application-level configuration. Expects a string in the format `KEY=VALUE`.
34+- `-o,--set-deployment-config ARG` [⭐](#star) – Set a deployment-level configuration. Expects a string in the format `KEY=VALUE`.
8135
8236#### Usage example
8337
@@ -130,10 +84,10 @@ Updates the parameters of a given deployment.
13084
13185- `-n,--name ARG` – The name of the deployment to update
13286- `-t,--tag ARG` – The new _Docker tag_ to update the deployment to
133−- `-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`.
134−- `-E,--unset-app-env-override ARG` [⭐](#star) – Removes an existing application-level override.
135−- `-o,--set-deployment-override ARG` [⭐](#star) – Add a new or replace an existing deployment-level override. Expects a string in the format `KEY=VALUE`.
136−- `-O,--unset-deployment-override` [⭐](#star) – Removes an existing deployment-level override.
87+- `-e,--set-app-config ARG` [⭐](#star) – Add a new or replace an existing application-level configuration. Expects a string in the format `KEY=VALUE`.
88+- `-E,--unset-app-env-configuration ARG` [⭐](#star) – Removes an existing application-level configuration.
89+- `-o,--set-deployment-config ARG` [⭐](#star) – Add a new or replace an existing deployment-level configuration. Expects a string in the format `KEY=VALUE`.
90+- `-O,--unset-deployment-configuration` [⭐](#star) – Removes an existing deployment-level configuration.
13791
13892#### Usage example
13993
@@ -157,21 +111,21 @@ Gets detailed information about a deployment, including a log of all preformed a
157111$ octo info -n hello-octopod
158112Current settings:
159113tag: v1
160−application overrides: app=1 (Public)
114+application configurations: app=1 (Public)
161115
162−deployment overrides: dep=2 (Public)
116+deployment configurations: dep=2 (Public)
163117
164118metadata:
165119 app: https://ree.lvh.me
166120
167121Last logs:
168−┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
169−┃ Created at ┃ Action id ┃ Action ┃ Tag ┃ App overrides ┃ Deployment overrides ┃ Exit code ┃
170−┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
171−│ 2020-11-02T17:14:03 │ 7 │ create │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │
172−├─────────────────────┼───────────┼────────┼─────┼────────────────┼──────────────────────┼───────────┤
173−│ 2020-11-02T19:01:02 │ 8 │ update │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │
174−└─────────────────────┴───────────┴────────┴─────┴────────────────┴──────────────────────┴───────────┘
122+┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
123+┃ Created at ┃ Action id ┃ Action ┃ App configurations ┃ Deployment configurations ┃ Exit code ┃
124+┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
125+│ 2020-11-02T17:14:03 │ 7 │ create │ app=1 (Public) │ dep=2 (Public) │ 1 │
126+├─────────────────────┼───────────┼────────┼────────────────┼──────────────────────┼───────────┤
127+│ 2020-11-02T19:01:02 │ 8 │ update │ app=1 (Public) │ dep=2 (Public) │ 1 │
128+└─────────────────────┴───────────┴────────┴────────────────┴──────────────────────┴───────────┘
175129```
176130
177131### cleanup
@@ -205,13 +159,6 @@ Restores a previously archived deployment.
205159```bash
206160$ octo restore -n hello-octopod
207161```
208−
209−### clean-archive
210−
211−#### Description
212−
213−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_.
214−
215162#### Options
216163
217164This command does not have any options.
@@ -242,10 +189,18 @@ $ octo logs -a 13
242189
243190 stderr:
244191
245−error: Found argument '--deployment-override' which wasn't expected, or isn't valid in this context
192+error: Found argument '--deployment-configuration' which wasn't expected, or isn't valid in this context
246193
247194USAGE:
248− update --app-env-override <app-env-override>... --base-domain <base-domain> --name <name> --namespace <namespace> --project-name <project-name> --tag <tag>
195+ update --app-env-configuration <app-env-configuration>... --base-domain <base-domain> --name <name> --namespace <namespace> --project-name <project-name> --tag <tag>
249196
250197For more information try --help
251198```
199+
200+<br />
201+
202+<p align="center">
203+ <i>Star the project of you like it</i>
204+</p>
205+
206+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/Octopod_deployment_guide.md+50−9
@@ -1,18 +1,46 @@
11# Octopod Server deployment guide
22
33## Installation options
4−There are several options to install octopod depending on your needs.
4+There are several options to install Octopod depending on your needs.
55
6−### If you have a kubernetes cluster
7−You can install octopod in any kubernetes cluster using our [Helm chart](../../charts/octopod).
6+### If you have a Kubernetes cluster
7+You can install Octopod in any Kubernetes cluster using our [Helm chart](../../charts/octopod).
88
99
10−Your cluster must satisfy following requirements:
11−- PVC support
12−- Ingress contoller ([ingress-nginx](https://kubernetes.github.io/ingress-nginx/)) installed
13−- Kubernetes version >= 1.19.0
10+Your cluster must satisfy the following requirements:
11+
12+#### Mandatory requirements
13+
14+- Kubernetes version >= 1.12.0 <= 1.22.0
15+- PVC support (for PostgreSQL persistence)
16+- [NGINX Ingress](https://kubernetes.github.io/ingress-nginx/) contoller version <= 0.49.3 installed. NGINX Ingress controller v1.x.x is not currently supported
17+
18+#### Optional requirements
19+
20+- Cert Manager ([cert-manager](https://cert-manager.io/docs/installation/)) installed, if you want to get SSL certificates from Let's Encrypt automatically.
21+
22+After ensuring that your cluster satisfies the requirements you can follow [the Helm installation instruction](../../charts/octopod/README.md) provided with our Helm chart.
23+
24+### Running Octopod in production considerations.
25+
26+You must consider several things before running Octopod in production and onboarding your team.
27+
28+#### DNS records
29+
30+Octopod will create a lot of on-demand environments and they must be reachable for your team. Usually it implies creating a lot of DNS records, pointing to Octopod managed environments. This process must be automated.
31+
32+We highly recommend you to use [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) as a very versatile solution for DNS records automation.
33+In certain cases, however, it isn't possible to use ExternalDNS. In this case you can use a wildcard DNS record, pointing to the service endpoint of your ingress controller. We do not recommend using wildcard DNS records. Even though they are much easier to implement, they could lead to hard-to-trace errors and they are also implementation-dependent.
34+
35+#### Certificates
36+
37+If you want to request SSL certificates dynamically from Let's Encrypt you must be aware of their [limits](https://letsencrypt.org/docs/rate-limits/). Because of these limits we recommend you to use a wildcard certificate for all Octopod deployments. The wildcard certificate has its limitations, like being valid only for one subdomain. This implies a need for planning your deployments DNS naming.
38+
39+#### Resources
40+
41+This is the most obvious one of the three, but you need to plan your cluster capacity based on your team needs. Setting [resource limits](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) for your workloads is one of the best practices of using Kubernetes, and you probably have done this already.
42+Also consider using autoscaling since it really helps to reduce costs in highly dynamic environments.
1443
15−After ensuring that your cluster satisfies the requirements you can follow [the Helm installation instruction](../../charts/octopod/README.md) provided with our helm chart.
1644
1745### If you want to try it locally
1846
@@ -24,6 +52,11 @@ Before running the script make sure that you have following tools installed:
2452- [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl)
2553- [helm 3](https://helm.sh/docs/intro/quickstart/#install-helm)
2654
55+> **ℹ️ NOTE:** using Octopod involves deploying many instances of some service (many instances of Wordpress in our example). This means, it will use up a lot of resources. Many local installations of Docker and Kubenetes have resource limits set quite low by default. If you experience performance issues you should try increasing the resource limits. Here are some useful links:
56+> - Docker resource limits in Docker Desktop for Mac – [docs.docker.com/desktop/mac/#resources](https://docs.docker.com/desktop/mac/#resources)
57+> - Docker resource limits in Docker Desktop for Windows – [docs.docker.com/desktop/windows/#resources](https://docs.docker.com/desktop/windows/#resources)
58+> - General Docker resource constraint documentation – [docs.docker.com/config/containers/resource_constraints](https://docs.docker.com/config/containers/resource_constraints/)
59+
2760After you have all of the necessary tools installed, you can run the script:
2861
2962```bash
@@ -36,4 +69,12 @@ Script will expose octopod ui at `octopod.lvh.me`.
3669
3770## What next?
3871
39−Now you may want to check how to [install helm charts](Helm-based_deployment_guide.md) with Octopod.
72+Now you may want to check how to [install Helm charts](Helm-based_deployment_guide.md) with Octopod.
73+
74+<br />
75+
76+<p align="center">
77+ <i>Star the project of you like it</i>
78+</p>
79+
80+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/Overview.md+12−22
@@ -1,22 +1,5 @@
11# Octopod overview
22
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [Intro](#intro)
7−- [🎯 The aim of Octopod](#-the-aim-of-octopod)
8−- [🔬 Example](#-example)
9− - [💽 The server](#-the-server)
10− - [🎨 Changing button colors](#-changing-button-colors)
11− - [😖 Why have such a complicated staging deployment?](#-why-have-such-a-complicated-staging-deployment)
12−- [🛠 The way Octopod is set up](#-the-way-octopod-is-set-up)
13−- [🎛️ CLI](#️-cli)
14−- [🔒 Authentication in the UI](#-authentication-in-the-ui)
15−- [🤖 Automatic deployment / CD](#-automatic-deployment--cd)
16−- [📗 Glossary](#-glossary)
17−
18−</details>
19−
203**NOTE: it is not recommended to use Octopod for managing production environments**
214
225## Intro
@@ -25,7 +8,7 @@ _Octopod_ is a tool which implements the multi-staging deployment model (MSDM) o
258
269_Octopod_ exists to reduce the overhead in deploying and maintaining per-feature staging environments. This responsibility may otherwise fall to:
2710
28−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.
11+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 extended periods while DevOps engineers deal with the additional load.
29122. **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.
3013
3114The 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).
@@ -38,7 +21,7 @@ Octopod aims to extract the mentioned similarity between deploying different sta
3821
3922### 💽 The server
4023
41−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
24+You are developing a server, which is accessed through _nginx_, and the server needs access to a [*Postgres*](https://www.postgresql.org) database and a [*Redis*](https://redis.io) database
4225
4326![](../diagrams/images/sample_architecture.png)
4427
@@ -50,13 +33,13 @@ Now imagine that two different developers each completed one of the tasks, and y
5033
5134A 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).
5235
53−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.
36+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_, [*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.
5437
5538This 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.
5639
5740_Octopod_ aims to factor out the common parts of creating and deploying a staging.
5841
59−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.
42+If developers were using _Octopod_ to deploy stagings, literally the only thing needed from them would be to specify the git commit hash (if you use git commit hashes as tags for your docker images) 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.
6043
6144![](../diagrams/images/sample_deployment.png)
6245
@@ -95,4 +78,11 @@ Fot more information see the [integration guide](Integration.md).
9578- _Octopod Server_ ― the server responsible for managing deployments
9679- _deployment control scripts_ ― scripts used to interact with your specific environment setup
9780- _web UI_ ― the interface developers, project managers, QA engineers, etc. use to manage stagings.
98−- <a id="overrides"></a>_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.
81+
82+<br />
83+
84+<p align="center">
85+ <i>Star the project of you like it</i>
86+</p>
87+
88+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/PM_case_study.md+9−1
@@ -56,7 +56,7 @@ guarantee that the feature won’t unexpectedly break someone else’s code.
5656If a critical part of the application, like authentication, gets broken,
5757this may block the testing process completely until the problem is
5858resolved. And to top it off, when this happens, it might not be
59−immediately clear who’s change is to blame. Thus, everyone who did
59+immediately clear whose change is to blame. Thus, everyone who did
6060recent merges begin looking into their code trying to see whether it was
6161their check-in that broke the server. Of course, it takes time and
6262blocks the development process as well until the culprit merge is found
@@ -120,3 +120,11 @@ that, once installed and set up, supports your multi-tier development
120120workflow at a level that does not require deep technical knowledge. Octopod
121121simplifies the procedure of creating new servers and allows the
122122implementation of common CD solutions in just a few clicks.
123+
124+<br />
125+
126+<p align="center">
127+ <i>Star the project of you like it</i>
128+</p>
129+
130+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/README.md+1−1
@@ -7,7 +7,7 @@
77
88## 🛠️ Technical documentation
99- [🏗 Technical architecture](Technical_architecture.md)
10−- [⚙️ Control script guide](Control_scripts.md)
10+- [⚙️ Control scripts guide](Control_scripts.md)
1111- [🔧🐙 Octopod deployment guide](Octopod_deployment_guide.md)
1212- [🔧🚀 Helm-based Octopod project setup](Helm-based_deployment_guide.md)
1313- [🐙🎛 octo CLI user guide](Octo_user_guide.md)
docs/en/Security_model.md+10−13
@@ -1,16 +1,5 @@
11# Security model
22
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [Octopod roles](#octopod-roles)
7−- [Kubernetes role-based access control](#kubernetes-role-based-access-control)
8−- [Web UI authentication](#web-ui-authentication)
9−- [Web UI OAuth](#web-ui-oauth)
10−- [octo CLI authentication](#octo-cli-authentication)
11−
12−</details>
13−
143## Octopod roles
154
165There are two user roles in _Octopod_:
@@ -33,7 +22,7 @@ There is currently no way to give someone access to _octo CLI_ without giving th
3322_Octopod Server_ is deployed in the `octopod` _Kubernetes_ namespace. Deployments are deployed in the `deployments` namespace.
3423_Octopod Server_ uses the `octopod` [_Service Account_][kubernetes-service-account].
3524
36−Octopod needs sufficient permissions to run helm inside kubernetes and create all resources described in helm chart it is installing. Thus permissions are quite extensive.
25+Octopod needs sufficient permissions to run Helm inside Kubernetes and create all resources described in the Helm chart it is installing. Thus permissions are quite extensive.
3726
3827[RBAC][kubernetes-rbac] rules to describe permissions needed are added automatically by [the Octopod Helm Chart](../../charts/octopod/templates/rbac.yaml).
3928
@@ -47,9 +36,17 @@ The [_Web UI_](Technical_architecture.md#-web-ui) on its own does not have any a
4736
4837## octo CLI authentication
4938
50−Authentication between _octo CLI_ and _Octopod Server_ is done through special token which is generated automatically or specified by user in `octopod.cliAuthSecret` parameter, as described [here](../../charts/octopod/README.md#parameters)
39+Authentication between _octo CLI_ and _Octopod Server_ is done through special token which is generated automatically or specified by user in `octopod.cliAuthSecret` parameter, as described [here](../../charts/octopod/README.md#parameters).
5140
5241[kubernetes-service-account]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account
5342[kubernetes-ingress-nginx-external-auth]: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#external-authentication
5443[kubernetes-rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac
5544[oauth2-proxy]: https://oauth2-proxy.github.io/oauth2-proxy
45+
46+<br />
47+
48+<p align="center">
49+ <i>Star the project of you like it</i>
50+</p>
51+
52+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/Tech_case_study.md+9−16
@@ -1,20 +1,5 @@
11# Octopod technical case study
22
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [Single staging model](#single-staging-model)
7−- [The multi-staging model](#the-multi-staging-model)
8−- [Implications](#implications)
9− - [Isolation – separate configurations](#isolation--separate-configurations)
10− - [Isolation – more information](#isolation--more-information)
11− - [Isolation – independent state](#isolation--independent-state)
12− - [Isolation – freedom to experiment](#isolation--freedom-to-experiment)
13− - [Infrastructure reuse](#infrastructure-reuse)
14− - [Continuous staging deployment](#continuous-staging-deployment)
15−
16−</details>
17−
183In this case study, we consider developing a system that is mostly comprised of a server.
194
205## Single staging model
@@ -37,7 +22,7 @@ It is sometimes the case that certain features need a "special" environment to b
3722
3823"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.
3924
40−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.
25+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 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.
4126
4227Having per-feature stagings allow you to have completely separate and independent configurations for every single feature you develop and test at no additional cost.
4328
@@ -72,3 +57,11 @@ When setting up Octopod you will need to implement the exact steps required to s
7257It 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.
7358
7459A 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.
60+
61+<br />
62+
63+<p align="center">
64+ <i>Star the project of you like it</i>
65+</p>
66+
67+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/en/Technical_architecture.md+18−169
@@ -1,29 +1,5 @@
11# Technical architecture
22
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [⚒️ Used tools](#️-used-tools)
7−- [📐 App architecture](#-app-architecture)
8− - [🖥 Web UI](#-web-ui)
9− - [🐙 Octopod Server](#-octopod-server)
10− - [🐘 PostgreSQL](#-postgresql)
11− - [🎛 octo CLI](#-octo-cli)
12− - [📑 Control scripts](#-control-scripts)
13− - [🚮⏲ Clean Archive CronJob](#-clean-archive-cronjob)
14− - [Kube API Server](#kube-api-server)
15−- [📦 Octopod Distribution model](#-octopod-distribution-model)
16−- [Process view](#process-view)
17− - [✨ Create](#-create)
18− - [🔧 Update](#-update)
19− - [🗃 Archive](#-archive)
20− - [🚮 Cleanup](#-cleanup)
21− - [🔁 Restore](#-restore)
22−- [👨‍💻👩‍💻 How we use it](#-how-we-use-it)
23−- [🗂️ Deployment state transitions](#️-deployment-state-transitions)
24−
25−</details>
26−
273## ⚒️ Used tools
284
295The main goal of _Octopod_ is to simplify deployment in [_Kubernetes_][kube].
@@ -61,7 +37,7 @@ The server receives commands from the [_octo CLI_](#-octo-cli) and the [_Web UI_
6137
6238### 🎛 octo CLI
6339
64−_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).
40+_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 the `Authorization` header](Security_model.md#octo-cli-authentication).
6541
6642It can perform all actions available in the [_Web UI_](#-web-ui), but also has access to view deployment logs.
6743
@@ -75,13 +51,8 @@ This is necessary to make _Octopod_ itself independent from any particular deplo
7551
7652When 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.
7753
78−These [scripts need to be implemented](Control_scripts.md) to deploy _Octopod_.
79−
80−### 🚮⏲ Clean Archive CronJob
81−
82−_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).
54+These [scripts need to be implemented](Control_scripts.md) to deploy _Octopod_ unless the set of generic scripts is used.
8355
84−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.
8556
8657### Kube API Server
8758
@@ -97,156 +68,29 @@ A _Docker Image_ with [_control scripts_](#-control-scripts) should be provided
9768
9869## Process view
9970
100−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_.
101−
102−### ✨ Create
103−
104−_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).
105−
106−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:
107−
108−```bash
109−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
110− --set "global.project-name=$project_name" \
111− --set "global.base-domain=$base-domain" \
112− --set "app.tag=$tag" \
113− --set "app.env.foo=$app_env_override_1" \
114− --set "app.bar=$deployment_override_1" \
115− --wait \
116− --timeout 300
117−```
118−
119−<details>
120− <summary>Create via CLI sequence diagram</summary>
121−
122−![Create](../diagrams/images/technical-architecture-create-via-cli.png)
123−
124−</details>
125−
126−<details>
127− <summary>Create via UI sequence diagram</summary>
128−
129−![Create](../diagrams/images/technical-architecture-create-via-ui.png)
71+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 _Control Scripts_.
13072
131−</details>
73+### ✨ Deployment creation
13274
133−### 🔧 Update
75+![Create](../diagrams/images/technical-architecture-create.png)
13476
135−_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).
13677
137−[_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:
78+### 🔧 Deployment update
13879
139−```bash
140−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
141− --set "global.project-name=$project_name" \
142− --set "global.base-domain=$base-domain" \
143− --set "app.tag=$tag" \
144− --set "app.env.foo=$app_env_override_1" \
145− --set "app.bar=$deployment_override_1" \
146− --wait \
147− --timeout 300
148−```
149−
150−
151−<details>
152− <summary>Update via CLI sequence diagram</summary>
153−
154−![Update](../diagrams/images/technical-architecture-update-via-cli.png)
155−
156−</details>
157−
158−<details>
159− <summary>Update via UI sequence diagram</summary>
160−
161−![Update](../diagrams/images/technical-architecture-update-via-ui.png)
162−
163−</details>
80+![Update](../diagrams/images/technical-architecture-update.png)
16481
16582### 🗃 Archive
16683
167−_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.
168−
169−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).
170−
171−The arguments are forwarded to the [_delete_](Control_scripts.md#-archive) script which in turn frees the computational resources. It might call something like:
172−
173−```bash
174−helm delete "$name" --purge
175−```
176−
177−<details>
178− <summary>Archive via CLI sequence diagram</summary>
179−
180−![Archive](../diagrams/images/technical-architecture-archive-via-cli.png)
181−
182−</details>
183−
184−<details>
185− <summary>Archive via UI sequence diagram</summary>
186−
187−![Archive](../diagrams/images/technical-architecture-archive-via-ui.png)
188−
189−</details>
84+![Archive](../diagrams/images/technical-architecture-archive.png)
19085
19186### 🚮 Cleanup
19287
193−_Cleanup_ – releases **all** resources captured by the deployment.
194−
195−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.
196−
197−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:
198−
199−```bash
200−kubectl delete pvc -n "$namespace" "$name-postgres-pvc"
201−kubectl delete certificate -n "$namespace" "$name-postgres-cert"
202−```
203−
204−<details>
205− <summary>Cleanup via CLI sequence diagram</summary>
206−
207−![Cleanup](../diagrams/images/technical-architecture-cleanup-via-cli.png)
208−
209−</details>
210−
211−<details>
212− <summary>Cleanup via UI sequence diagram</summary>
213−
214−![Cleanup](../diagrams/images/technical-architecture-cleanup-via-ui.png)
21588
216−</details>
89+![Cleanup](../diagrams/images/technical-architecture-cleanup.png)
21790
21891### 🔁 Restore
21992
220−_restore_ – restores an archived deployment in the state it was last in. Calls the same _script_ that is called in [_create_](#-create).
221−
222−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.
223−
224−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:
225−
226−```bash
227−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
228− --set "global.project-name=$project_name" \
229− --set "global.base-domain=$base-domain" \
230− --set "app.tag=$tag" \
231− --set "app.env.foo=$app_env_override_1" \
232− --set "app.bar=$deployment_override_1" \
233− --wait \
234− --timeout 300
235−```
236−
237−<details>
238− <summary>Restore via CLI sequence diagram</summary>
239−
240−![Restore](../diagrams/images/technical-architecture-restore-via-cli.png)
241−
242−</details>
243−
244−<details>
245− <summary>Restore via UI sequence diagram</summary>
246−
247−![Restore](../diagrams/images/technical-architecture-restore-via-ui.png)
248−
249−</details>
93+![Restore](../diagrams/images/technical-architecture-restore.png)
25094
25195## 👨‍💻👩‍💻 How we use it
25296
@@ -284,9 +128,14 @@ _Running_, _Failure_, _Archived_ states are "permanent", meaning the deployment
284128
285129![Deployment Statuses](../diagrams/images/technical-architecture-deployment-states-fsm.png)
286130
287−<!-- [kubectl]: https://kubernetes.io/docs/reference/kubectl/ -->
288−<!-- [helm]: https://helm.sh -->
289−<!-- [kubedog]: https://github.com/werf/kubedog -->
290131[kube]: https://kubernetes.io
291132[chart]: https://helm.sh/docs/topics/charts/
292133[ingress]: https://kubernetes.io/docs/concepts/services-networking/ingress/
134+
135+<br />
136+
137+<p align="center">
138+ <i>Star the project of you like it</i>
139+</p>
140+
141+<p align="center"><a href="https://typeable.io"><img src="../../img/typeable_logo.svg" width="177px"></img></a></p>
docs/images/dev2.png

Binary file not shown.

docs/images/octopod_app_keys_list.pngnew file

Binary file not shown.

docs/images/octopod_archive.png

Binary file not shown.

docs/images/octopod_blank.png

Binary file not shown.

docs/images/octopod_deployment_blank.png

Binary file not shown.

docs/images/octopod_deployment_filled.png

Binary file not shown.

docs/images/octopod_in_octopod_deployment.png

Binary file not shown.

docs/images/wordpress_blank.png

Binary file not shown.

docs/ru/Control_scripts.mddeleted−241
@@ -1,241 +0,0 @@
1−# Control scripts
2−
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [create](#create)
7−- [update](#update)
8−- [archive](#archive)
9−- [check](#check)
10−- [cleanup](#cleanup)
11−- [archive_check](#archive_check)
12−- [tag_check](#tag_check)
13−- [init](#init)
14−- [info](#info)
15−- [notifications](#notifications)
16−
17−</details>
18−
19−## create
20−
21−Реализация создания нового развертывания.
22−
23−Получает на вход следующие аргументы:
24−* `--project-name` – название проекта
25−* `--base-domain` – базовый домен
26−* `--namespace` – namespace
27−* `--name` – имя развертывания
28−* `--tag` – тег развертывания
29−* `--app-env-override` – override уровня приложения (формат значения `FOO=BAR`, может быть передано 0 или более раз)
30−* `--deployment-override` – override уровня развертывания (формат значения `FOO=BAR`, может быть передано 0 или более раз)
31−
32−Успешность операции определяется по exit code (`0` – успех).
33−
34−Например `create` с полученными аргументами может выполнять:
35−
36−```bash
37−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
38− --set "global.project-name=$project_name" \
39− --set "global.base-domain=$base-domain" \
40− --set "app.tag=$tag" \
41− --set "app.env.foo=$app_env_override_1" \
42− --set "app.bar=$deployment_override_1" \
43− --wait \
44− --timeout 300
45−```
46−
47−## update
48−
49−Реализация обновления развертывания.
50−
51−Получает на вход следующие аргументы:
52−* `--project-name` – название проекта
53−* `--base-domain` – базовый домен
54−* `--namespace` – namespace
55−* `--name` – имя развертывания
56−* `--tag` – тег развертывания
57−* `--app-env-override` – override уровня приложения (формат значения `FOO=BAR`, может быть передано 0 или более раз)
58−* `--deployment-override` – override уровня развертывания (формат значения `FOO=BAR`, может быть передано 0 или более раз)
59−
60−Успешность операции определяется по exit code (`0` – успех).
61−
62−Например `update` с полученными аргументами может выполнять:
63−
64−```bash
65−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
66− --set "global.project-name=$project_name" \
67− --set "global.base-domain=$base-domain" \
68− --set "app.tag=$tag" \
69− --set "app.env.foo=$app_env_override_1" \
70− --set "app.bar=$deployment_override_1" \
71− --wait \
72− --timeout 300
73−```
74−
75−## archive
76−
77−Реализация удаления развертывания.
78−
79−Получает на вход следующие аргументы:
80−* `--project-name` – название проекта
81−* `--base-domain` – базовый домен
82−* `--namespace` – namespace
83−* `--name` – имя развертывания
84−
85−Успешность операции определяется по exit code (`0` – успех).
86−
87−Например `archive` с полученными аргументами может выполнять:
88−
89−```bash
90−helm delete "$name" --purge
91−```
92−
93−## check
94−
95−Реализация проверки состояния развертывания.
96−
97−Получает на вход следующие аргументы:
98−* `--project-name` – название проекта
99−* `--base-domain` – базовый домен
100−* `--namespace` – namespace
101−* `--name` – имя развертывания
102−* `--tag` – тег развертывания
103−
104−Успешность операции определяется по exit code (`0` – успех).
105−
106−Например `check` с полученными аргументами может выполнять:
107−
108−```bash
109−echo "{\"Deployments\": [{\"ResourceName\": \"app-${name}\", \"Namespace\": \"${namespace}\"}], \"StatefulSets\": [{\"ResourceName\": \"db-${name}\", \"Namespace\": \"${namespace}\"}]}" | \
110− kubedog multitrack -t 3
111−```
112−
113−В примере выше используется [_Kubedog_][kubedog].
114−Предполагается, что пользователь сам установит [_Kubedog_][kubedog]: сохранит его в `/` контейнера с _Control scripts_ или установит его в `$HOME` из [init](#init).
115−
116−При помощи exit code можно также сигнализировать какая именно ошибка произошла:
117−- `1` – Стандартная ошибка.
118−- `2` – Частичная недоступность (некоторые из подов недоступны).
119−- `3` – Несоответствие тегов (тег некорректно обновился).
120−
121−## cleanup
122−
123−Реализация очистки ресурсов развертывания.
124−
125−Получает на вход следующие аргументы:
126−* `--project-name` – название проекта
127−* `--base-domain` – базовый домен
128−* `--namespace` – namespace
129−* `--name` – имя развертывания
130−
131−Успешность операции определяется по exit code (`0` – успех).
132−
133−Например `cleanup` с полученными аргументами может выполнять:
134−
135−```bash
136−kubectl delete pvc -n $namespace -l "app=$name"
137−```
138−
139−## archive_check
140−
141−Реализация проверки успешности удаления развертывания.
142−
143−Получает на вход следующие аргументы:
144−* `--project-name` – название проекта
145−* `--base-domain` – базовый домен
146−* `--namespace` – namespace
147−* `--name` – имя развертывания
148−
149−Успешность операции определяется по exit code (`0` – успех).
150−
151−Например `archive_check` с полученными аргументами может выполнять:
152−
153−```bash
154−helm status $name
155−```
156−
157−## tag_check
158−
159−Реализация проверки существования тега развертывания.
160−
161−Получает на вход следующие аргументы:
162−* `--project-name` – название проекта
163−* `--base-domain` – базовый домен
164−* `--namespace` – namespace
165−* `--name` – имя развертывания
166−* `--tag` – тег развертывания
167−
168−Успешность операции определяется по exit code (`0` – успех).
169−
170−## init
171−
172−Реализация инициализации рабочего окружения для будущих запусков *Control Scripts*.
173−
174−В этом скрипте предлагается настроить доступ к системам контроля версий, облачным провайдерам и т.д.
175−Для этого можно сохранить настройки в файлы в $HOME.
176−
177−Этот скрипт однократно запускается во время создания пода `Octopod Server`.
178−
179−Успешность операции определяется по exit code (`0` – успех).
180−
181−Например `init` может выполнять:
182−
183−```bash
184−mkdir $HOME/.ssh
185−echo -e "Host github.com\nHostname github.com\nPort 22\nUser git\nIdentityFile $HOME/.ssh/deploy.key" > $HOME/.ssh/config
186−echo "MY_DEPLOY_KEY" > $HOME/.ssh/deploy.key"
187−```
188−
189−## info
190−
191−Реализация получения метаданных о deployment. Содержимое _stdout_ будет использованно в качестве дополнительной информации о deployment в _CLI/UI_.
192−Ожидаемый формат _stdout_ ― _CSV_ `ключ,значение`:
193−
194−```
195−app,https://foo.example.com
196−api,https://api.foo.example.com
197−```
198−
199−Ожидается, что `ключ` не может содержать символ `,`, ограничений на `значение` нет. Пробельные символы остаются без изменений.
200−
201−Получает на вход следующие аргументы:
202−* `--project-name` – название проекта
203−* `--base-domain` – базовый домен
204−* `--namespace` – namespace
205−* `--name` – имя стейджинга
206−
207−Успешность операции определяется по exit code (`0` – успех).
208−
209−Например `info` с полученными аргументами может выполнять:
210−
211−```bash
212−echo "app,https://${name}.example.com"
213−echo "api,https://api.${name}.example.com"
214−```
215−
216−[kubedog]: https://github.com/werf/kubedog
217−
218−## notifications
219−
220−Этот скрипт вызывается каждый раз, когда развертывание меняет свой статус. (При создании и удалении не вызывается.)
221−
222−Полный список статусов и их переходов можно найти в документе по [технической архитектуре](Technical_architecture.md#deployment-state-transitions).
223−
224−Получает на вход следующие аргументы:
225−* `--project-name` – название проекта
226−* `--base-domain` – базовый домен
227−* `--namespace` – namespace
228−* `--name` – имя стейджинга
229−* `--tag` – тег развертывания
230−- `--old-status` – предыдущий статус
231−- `--new-status` – новый статус
232−
233−Последние два аргумент могут иметь одно из следующих значений:
234−- `Running`
235−- `GenericFailure`
236−- `TagMismatch`
237−- `PartialAvailability`
238−- `CreatePending`
239−- `UpdatePending`
240−- `ArchivePending`
241−- `Archived`
docs/ru/Helm-based_deployment_guide.mddeleted−73
@@ -1,73 +0,0 @@
1−# Установка Helm чартов с помощью Octopod
2−
3−В этой инструкции вы узнаете как установить [чарт wordpress](https://github.com/bitnami/charts/tree/master/bitnami/wordpress) от bitnami с помощью Octopod.
4−
5−## Ваш первый Deployment
6−
7−Прежде чем проолжить убедитесь, что уже установили Octopod, воспользовавшись нашей [инструкцией](Octopod_deployment_with_K8S.md).
8−Обратите внимание, что в этой инструкции мы предполагаем, что вы установили Octopod локально.
9−
10−Открыв Octopod в вашем браузере вы увидите следующее:
11−
12−![](../images/octopod_blank.png)
13−
14−Нажмите на кнопку New Deployment
15−
16−![](../images/octopod_deployment_blank.png)
17−
18−Тут вы можете заполнить все параметры для вашего Deployment'. Давайте же их заполним!
19−
20−![](../images/octopod_deployment_filled.png)
21−
22−Name ― мы выбрали `wordpress`, но вы можете выбрать любое другое имя.
23−
24−Tag ― `5.8.0`. Мы взяли его из [параметров чарта](https://github.com/bitnami/charts/blob/master/bitnami/wordpress/Chart.yaml#L4)
25−
26−App Overrides:
27−`ingress.enabled: true`
28−`ingress.hostname: wordpress.lvh.me`
29−
30−Эти параметры мы взяли из [документации чарта](https://github.com/bitnami/charts/tree/master/bitnami/wordpress#traffic-exposure-parameters). Вы можете поставить любые параметры указанные там.
31−
32−После того, как вы всё заполнили, нажмите на кнопку Save и дождитесь пока Deployment перейдёт в состояние Running.
33−
34−![](../images/octopod_deployment_filled.png)
35−
36−Теперь вы можете нажать на ссылку `wordpress` в столбце Links и будете переаправлены на ваш инстанс wordpress.
37−
38−![](../images/wordpress_blank.png)
39−
40−Поздравляю, вы создали свой первый Deployment в Octopod!
41−
42−## Идём дальше
43−
44−Сейчас у вас скорее всего возник вопрос как Octopod установил чарт из репозитория bitnami, хотя мы не указывали вооюзе никаких параметров для их репозитория? Всё потому, что мы установили эти настройки на уровне чарта [тут](../../charts/octopod/values.yaml#L90).
45−Но вы можете переопределить все эти параметры. Давайте же приступим!
46−
47−Сначала отправим уже ненужный Deployment с wordpress в архив.
48−
49−![](../images/octopod_archive.png)
50−
51−И создадим ещё один новый Deployment, но теперь уже с другим списком Overrides.
52−
53−![](../images/octopod_in_octopod_deployment.png)
54−
55−Name: octopod-internal
56−
57−Tag: 1.3.1
58−
59−App Overrides:
60−
61−`octopod.baseDomain: octopod-internal.lvh.me`
62−`ingress.tls.enabled: false`
63−
64−Deployment Overrides:
65−
66−`chart_name: octopod`
67−`chart_repo_name: typeable`
68−`chart_repo_url: https://typeable.github.io/octopod/`
69−`chart_version: 0.5.1`
70−
71−Точно так же как и в предыдущем примере все параметры мы взяли из [документации чарта](../../charts/octopod/README.md#Parameters), но Deployment Overrides это параметры для скриптов управления. Чтобы узнать больше про скрипты ознакомьтесь с [этой документацией](../../helm-control-scripts/README.md).
72−
73−Теперь у вас есть Octopod в Octopod'е! Осталось только установить свой helm чарт.
docs/ru/Octo_user_guide.mddeleted−173
@@ -1,173 +0,0 @@
1−# Octo user guide
2−
3−_octo CLI_ – консольная утилита для управления deployments через отправку команд в _Octopod Server_.
4−
5−## Environment variables
6−
7−`OCTOPOD_URL` – URL к _Octopod Server_, например `https://octopod-power-app.example.com:443`, **необходимо определить, иначе _octo CLI_ не будет работать**.
8−
9−`TLS_CERT_PATH` – путь к файлу сертификата, который используется для аутентификации в _Octopod Server_, если не определенно, то используется `./cert.pem`.
10−
11−`TLS_KEY_PATH` – путь к файлу ключа, который используется для аутентификации в _Octopod Server_, если не определенно, то используется `./key.pem`.
12−
13−## Subcommands
14−
15−### create
16−
17−Создать новый deployment.
18−
19−Аргументы:
20−
21−* `-n` или `--name` – название deployment
22−* `-t` или `--tag` – тег deployment
23−* `-e` или `--set-app-env-override` – override уровна приложения, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз
24−* `-o` или `--set-deployment-override` – override увроня deployment, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз
25−
26−Пример:
27−
28−```bash
29−$ octo create -n hello-octopod -t ca5fd1fe08389f6422a506a59b68a5272ac37ba6 -e KEY1=VALUE1 -e KEY2=VALUE2
30−```
31−
32−### list
33−
34−Получить список всех deployments.
35−
36−Данная subcommand не требует передачи аргументов.
37−
38−Пример:
39−
40−```bash
41−$ octo list
42−hello-octopod
43−```
44−
45−### archive
46−
47−Удалить deployment.
48−
49−Аргументы:
50−
51−* `-n` или `--name` – название deployment
52−
53−Пример:
54−
55−```bash
56−$ octo archive -n hello-octopod
57−```
58−
59−### update
60−
61−Обновить deployment.
62−
63−Аргументы:
64−
65−* `-n` или `--name` – название deployment
66−* `-t` или `--tag` – тег deployment
67−* `-e` или `--set-app-env-override` – override уровна приложения, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз
68−* `-E` или `--unset-app-env-override` – override уровна приложения, которое надо удалить, формат: КЛЮЧ, можно использовать 0 и более раз
69−* `-o` или `--set-deployment-override` – override увроня deployment, формат: КЛЮЧ=ЗНАЧЕНИЕ, можно использовать 0 и более раз
70−* `-O` или `--unset-deployment-override` – override увроня deployment, которое надо удалить, формат: КЛЮЧ, можно использовать 0 и более раз
71−
72−Пример:
73−
74−```bash
75−$ octo update -n octopod -t 015f16ecf398fcadaac508c1855ae160af0969c4 -E KEY1 -e KEY2=VALUE22222
76−```
77−
78−### info
79−
80−Получить информацию о deployment.
81−
82−Аргументы:
83−
84−* `-n` или `--name` – название deployment
85−
86−Пример:
87−
88−```bash
89−$ octo info -n hello-octopod
90−Current settings:
91−tag: v1
92−application overrides: app=1 (Public)
93−
94−deployment overrides: dep=2 (Public)
95−
96−metadata:
97− app: https://ree.lvh.me
98−
99−Last logs:
100−┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┓
101−┃ Created at ┃ Action id ┃ Action ┃ Tag ┃ App overrides ┃ Deployment overrides ┃ Exit code ┃
102−┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━┩
103−│ 2020-11-02T17:14:03 │ 7 │ create │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │
104−├─────────────────────┼───────────┼────────┼─────┼────────────────┼──────────────────────┼───────────┤
105−│ 2020-11-02T19:01:02 │ 8 │ update │ v1 │ app=1 (Public) │ dep=2 (Public) │ 1 │
106−└─────────────────────┴───────────┴────────┴─────┴────────────────┴──────────────────────┴───────────┘
107−```
108−
109−### cleanup
110−
111−Очистить ресурсы от deployment.
112−
113−Аргументы:
114−
115−* `-n` или `--name` – название deployment
116−
117−Пример:
118−
119−```bash
120−$ octo cleanup -n hello-octopod
121−```
122−
123−### restore
124−
125−Восстановить deployment.
126−
127−Аргументы:
128−
129−* `-n` или `--name` – название deployment
130−
131−Пример:
132−
133−```bash
134−$ octo restore -n hello-octopod
135−```
136−
137−### clean-archive
138−
139−Удалить архивные deployments старее 2х недель.
140−
141−Данная subcommand не требует передачи аргументов.
142−
143−Пример:
144−
145−```bash
146−$ octo clean-archive
147−```
148−
149−### logs
150−
151−Печатает логи выбранного деплоя.
152−
153−Аргументы:
154−
155−* `-a` или `--action` – action id, который вернулся из команды `octo info`.
156−* `-l` или `--log-type` – Какие логи необходимо вернуть. Значения: `stdout`, `stderr`, `all`. По умолчанию будет `all`
157−
158−Пример:
159−
160−```
161−$ octo logs -a 13
162− stdout:
163−
164−
165− stderr:
166−
167−error: Found argument '--deployment-override' which wasn't expected, or isn't valid in this context
168−
169−USAGE:
170− update --app-env-override <app-env-override>... --base-domain <base-domain> --name <name> --namespace <namespace> --project-name <project-name> --tag <tag>
171−
172−For more information try --help
173−```
docs/ru/Octopod_deployment_with_K8S.mddeleted−30
@@ -1,30 +0,0 @@
1−# Руководство по установке Octopod
2−
3−## Варинаты установки
4−Есть несколько вариантов установки Octopod в зависимости от ваших предпочтений.
5−
6−### Если у вас есть свободный kubernetes кластер
7−Для установки Octopod на уже существуюший kubernetes вы можете восмользоваться [Helm чартом](../../charts/octopod).
8−Чтобы успешно установить Octopod ваш кластер должен соответствовать слежующи требованиям:
9−- Поддержка PVC
10−- Установленный Ingress контроллер ([ingress-nginx](https://kubernetes.github.io/ingress-nginx/)) installed
11−- Версия kubernetes >= 1.19.0
12−
13−Если кластер соответствует требованиям вы можете воспользоваться [инструкцией](../../charts/octopod/README.md) для установки Octopod с помощью helm чарта.
14−
15−### Если хочется установить локально
16−Можно воспользоваться скриптом [octopod_local_install.sh](../../octopod_local_install.sh) для разворачивания Octopod в кластере kind на локальной машине.
17−
18−Перед запуском скрипта убедитесь, что у вас установлены следующие утилиты:
19−- [docker](https://docs.docker.com/engine/install/)
20−- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
21−- [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl)
22−- [helm 3](https://helm.sh/docs/intro/quickstart/#install-helm)
23−
24−После установки зависимостей просто запустите скрипт `./octopod_local_install.sh`
25−В результате работы скрипта у вас будет возможность подлючиться к Octopod на домене `octopod.lvh.me`.
26−`lvh.me` это специальный домен который всегда резолвится на `127.0.0.1`
27−
28−## Что дальше?
29−
30−Сейчас вы, возможно захотите пройти инструкцию по [установке helm чартов](Helm-based_deployment_guide.md) c помощью Octopod.
docs/ru/README.mddeleted−8
@@ -1,8 +0,0 @@
1−# 🐙📑 Документация Octopod
2−
3−- [🏗 Техническая архитектура](Technical_architecture.md)
4−- [⚙️ Гайд по скриптам управления развертываниямия](Control_scripts.md)
5−- [🔧🐙 Гайд по делою самого Octopod](Octopod_deployment_with_K8S.md)
6−- [🔧🚀 Гайд по интеграции проекта с Octopod](Helm-based_deployment_guide.md)
7−- [🐙🎛 Гайд по использованию octo CLI](Octo_user_guide.md)
8−- [🔒 Модель безопасности Octopod](Security_model.md)
docs/ru/Security_model.mddeleted−120
@@ -1,120 +0,0 @@
1−# Security model
2−
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [Octopod roles](#octopod-roles)
7−- [Kubernetes role-based access control](#kubernetes-role-based-access-control)
8− - [Права на удаление сертификатов](#права-на-удаление-сертификатов)
9− - [Права на удаление Persistent Volumes Claims](#права-на-удаление-persistent-volumes-claims)
10−- [Users auth example with OAuth](#users-auth-example-with-oauth)
11−- [CLI client Auth](#cli-client-auth)
12−- [UI Auth](#ui-auth)
13−
14−</details>
15−
16−## Octopod roles
17−
18−В _Octopod_ есть 2 роли:
19−* _user_
20−* _admin_
21−
22−| role | управление развертываниями | просмотр логов развертываний |
23−| :---: | :------------------------: | :--------------------------: |
24−| user | ✅ | ❌ |
25−| admin | ✅ | ✅ |
26−
27−Пользователи _UI_ работают с правами _user_.
28−
29−Пользователи _octo CLI_ работают с правами _admin_.
30−
31−## Kubernetes role-based access control
32−
33−_Octopod Server_ разворачивается в `octopod` namespace. _Octopod Server_ разворачивает развертывания в `deployments` namespace.
34−_Octopod Server_ использует `octopod` [_Service Account_][kubernetes-service-account].
35−
36−Для очистки ресурсов _Octopod Server_ / _Контейнеру с утилитами (control scripts)_ может потребоваться
37−(зависит от настроект _Kubernetes_ и релизации _Контейнера с утилитами_)
38−доступ для удаления сертификатов и [_Persistent Volumes Claims_][kubernetes-pvc].
39−
40−Доступ можно выдать через [_RBAC_][kubernetes-rbac]:
41−
42−### Права на удаление сертификатов
43−
44−```yaml
45−---
46−apiVersion: rbac.authorization.k8s.io/v1
47−kind: ClusterRole
48−metadata:
49− name: cert-control-clusterrole
50−rules:
51− - apiGroups: ["cert-manager.io"]
52− resources: ["certificates"]
53− verbs: ["list", "delete", "deletecollection"]
54−---
55−apiVersion: rbac.authorization.k8s.io/v1
56−kind: RoleBinding
57−metadata:
58− name: octopod-cert-control-rolebinding
59− namespace: deployments
60−roleRef:
61− kind: ClusterRole
62− apiGroup: rbac.authorization.k8s.io
63− name: cert-control-clusterrole
64−subjects:
65− - kind: ServiceAccount
66− name: octopod
67− namespace: octopod
68−```
69−
70−### Права на удаление Persistent Volumes Claims
71−
72−```yaml
73−---
74−apiVersion: rbac.authorization.k8s.io/v1
75−kind: ClusterRole
76−metadata:
77− name: pvc-control-clusterrole
78−rules:
79− - apiGroups: [""]
80− resources: ["persistentvolumeclaims"]
81− verbs: ["list", "delete", "deletecollection"]
82−---
83−apiVersion: rbac.authorization.k8s.io/v1
84−kind: RoleBinding
85−metadata:
86− name: octopod-pvc-control-rolebinding
87− namespace: deployments
88−roleRef:
89− kind: ClusterRole
90− apiGroup: rbac.authorization.k8s.io
91− name: pvc-control-clusterrole
92−subjects:
93− - kind: ServiceAccount
94− name: octopod
95− namespace: octopod
96−```
97−
98−## Users auth example with OAuth
99−
100−Доступ к _UI_ по умолчанию не ограничен.
101−Но вы можете добавить аутентификацию через GitHub OAuth в [_Kubernetes Ingress Nginx_][kubernetes-ingress-nginx-external-auth],
102−для этого потребуется дополнительно развернуть [_OAuth2 Proxy_][oauth2-proxy] в кластере.
103−
104−Более подробно описано в примере [_External OAUTH Authentication_][oauth-external-auth].
105−
106−## CLI client Auth
107−
108−Для аутентификации _octo CLI_ в _Octopod Server_ используется сертификат.
109−
110−## UI Auth
111−
112−Для аутентификации _UI_ в _Octopod Server_ используется Basic Auth.
113−
114−
115−[kubernetes-service-account]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account
116−[kubernetes-rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac
117−[kubernetes-pvc]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#expanding-persistent-volumes-claims
118−[kubernetes-ingress-nginx-external-auth]: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#external-authentication
119−[oauth2-proxy]: https://oauth2-proxy.github.io/oauth2-proxy
120−[oauth-external-auth]: https://kubernetes.github.io/ingress-nginx/examples/auth/oauth-external-auth
docs/ru/Technical_architecture.mddeleted−275
@@ -1,275 +0,0 @@
1−# Technical architecture
2−
3−<details>
4− <summary>Table of contents</summary>
5−
6−- [Used tools](#used-tools)
7−- [App architecture](#app-architecture)
8− - [UI](#ui)
9− - [Octopod Server](#octopod-server)
10− - [PostgreSQL](#postgresql)
11− - [octo CLI](#octo-cli)
12− - [Скрипты управления развертываниями (control scripts)](#скрипты-управления-развертываниями-control-scripts)
13− - [Clean Archive CronJob](#clean-archive-cronjob)
14− - [Kube API Server](#kube-api-server)
15−- [Octopod Distribution model](#octopod-distribution-model)
16−- [Process view](#process-view)
17− - [Create](#create)
18− - [Create via CLI](#create-via-cli)
19− - [Create via UI](#create-via-ui)
20− - [Update](#update)
21− - [Update via CLI](#update-via-cli)
22− - [Update via UI](#update-via-ui)
23− - [Archive](#archive)
24− - [Archive via CLI](#archive-via-cli)
25− - [Archive via UI](#archive-via-ui)
26− - [Cleanup](#cleanup)
27− - [Cleanup via CLI](#cleanup-via-cli)
28− - [Cleanup via UI](#cleanup-via-ui)
29− - [Restore](#restore)
30− - [Restore via CLI](#restore-via-cli)
31− - [Restore via UI](#restore-via-ui)
32−- [How we use it](#how-we-use-it)
33−- [Deployment state transitions](#deployment-state-transitions)
34−
35−</details>
36−
37−## Used tools
38−
39−Основная цель *Octopod* ― упрощение управления разными развертываниями сервисов в [_Kubernetes_][kube].
40−
41−При разработке рассчитывалось, что сам _Octopod_ тоже будет разворачиваться при помощи [_Kubernetes_][kube].
42−
43−## App architecture
44−
45−Взаимодействовать с _Octopod_ возможно либо через веб-интерфейс (рассчитано на использование разработчиками, PM, QA и т. д.), либо через консольный клиент [_octo CLI_](#octopod-server) (рассчитано на использования DevOps инженерами и программно, например в CI).
46−
47−Все взаимодействие _Octopod_ с самими развертываниями производится через [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts), которые передаются ему при запуске. Это позволяет сделать _Octopod_ очень гибким ― его возможно настроить для работы с практически в любой организацией развертываний.
48−
49−Все данные об окружениях развертываний и всех операция совершаемых над ними _Octopod_ хранит в базе данных [_PostgreSQL_](#postgresql).
50−
51−![App architecture](../diagrams/images/app-architecture.png)
52−
53−### UI
54−
55−_UI_ – веб интерфейс,
56−используется для управления развертываниями посредством отправки команд управления развертываниями.
57−Взаимодействует с [_Octopod Server_](#octopod-server) посредством отправки HTTP/1.1 запросов, а также получает
58−события от [_Octopod Server_](#octopod-server) по _Websocket_. Между _UI_ и [_Octopod Server_](#octopod-server) Basic Auth. Написан на Haskell и Reflex-Dom.
59−
60−В интерфейсе нет технических деталей системной администрации и оркестрации ― работа с развертываниями производится очень просто. Интерфейс рассчитан на использование разработчиками любого уровня, QA инженерами, PM, а так же людьми без ИТ образования.
61−
62−### Octopod Server
63−
64−_Octopod Server_ – непосредственно сам сервер, который обрабатывает запросы работы с развертываниями и делегирует базовые операции, модифицирующие развертывания, [_скриптам управления стеджингами (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
65−
66−Сервер обрабатывает
67−команды управления развертываниями от [_octo CLI_](#octopod-server) и [_UI_](#ui), обновляет состояние развертываний.
68−[_octo CLI_](#octopod-server) и [_UI_](#ui) взаимодействуют с ним путем отправки HTTP/1.1 запросов.
69−Сервер шлет событие обновления на [_UI_](#ui) по _Websocket_. Сервер взаимодействует с [_Kube API Server_](#kube-api-server)
70−через [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). Для хранения настроек и статусов развертываний,
71−и логов действий пользователей используется [_PostgreSQL_](#postgresql). Написан на Haskell. Для работы
72−с развертываниями использует [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
73−
74−### PostgreSQL
75−
76−[_PostgreSQL_](https://www.postgresql.org) – РСУБД для хранения настроек и статусов развертываний, и логов
77−действий пользователей.
78−
79−### octo CLI
80−
81−_octo CLI_ – консольный клиент
82−используется для управления развертываниями посредством отправки команд управления
83−развертываниями. Взаимодействует с [_Octopod Server_](#octopod-server) посредством отправки HTTP/1.1 запросов.
84−Написан на Haskell. Между _octo CLI_ и [_Octopod Server_](#octopod-server) аутентификация по сертификату,
85−новый сертификаты создается вовремя каждой сборки и упаковываются в контейнеры с _octo CLI_ и [_Octopod Server_](#octopod-server).
86−
87−В нем можно делать все операции, которые можно делать в [_UI_](#ui), но также есть доступ к расширенным операциям над развертываниями, таким как просмотр логов деплоя, что может быть полезно при диагностике возникающих проблем.
88−
89−Этот клиент рассчитан на использование DevOps инженерами.
90−
91−Его также можно использовать если необходимо автоматизировать управление развертываниями. Например, при настройке автоматического обновления развертываний при успешно пройденном CI.
92−
93−### Скрипты управления развертываниями (control scripts)
94−
95−_Скрипты управления развертываниями (control scripts)_ – контейнер с исполняемыми файлами, в которые инкапсулирована
96−логика работы с [_Kube API Server_](#kube-api-server), облачными провайдерами, развертываниями, системами контроля версий и т.д.
97−
98−Они нужны для того, чтобы _Octopod_ не был завязан на какую-то конкретную организацию развертываний ― его можно настроить для работы с практически любой организацией развертываний.
99−
100−Во время старта пода [_Octopod Server_](#octopod-server) содержимое контейнера с утилитами копируется в ФС контейнера с [_Octopod Server_](#octopod-server),
101−поэтому исполняемые файлы должны быть либо интерпретируемы через _Bash_, либо статически слинкованны.
102−Логику исполняемых файлов предлагается реализовать пользователям _Octopod_ (смотри [_Control scripts_](Control_scripts.md)).
103−
104−### Clean Archive CronJob
105−
106−_Clean Archive CronJob_ – CronJob, которая запускается раз в час и через [_octo CLI_](#octopod-server) удаляет заархивированные развертывания старее 14 дней.
107−
108−Это сделано из-за того что при архивировании развертываний удаляются только вычислительные его части, _persistent volumes_ не удаляются, и при необходимости развертывание возможно восстановить в том виде, в котором он был до архивирования. Такая возможность пропадает по истечению 14 дней с момент его архивирования.
109−
110−### Kube API Server
111−
112−[Kube API Server](https://kubernetes.io/docs/concepts/overview/kubernetes-api/) – сервис на стороне [_Kubernetes_][kube], к которому должны обращаться [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
113−
114−## Octopod Distribution model
115−
116−[_octo CLI_](#octo-cli) в виде статически слинкованного исполняемого файла. Собранные исполняемые файлы доступны во вкладке "Releases" этого проекта на GitHub.
117−
118−[_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).
119−
120−Docker образ [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts) пользователь _Octopod_ собирает сам.
121−
122−## Process view
123−
124−Далее здесь для справки приводятся схемы последовательностей для каждой из базовых операция работы с развертываниями. При их исполнении вызываются [_скрипты управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts). На схеме они помечены как _ControlScripts_.
125−
126−### Create
127−
128−_create_ – создание нового развертывания.
129− В качестве аргументов принимаются `name`, `tag` и опциональные `overrides` (уровня App или Deployment, открытие или секретные).
130−
131− Переданные аргументы вместе с `project-name`, `base-domain`, `namespace` передаются в качестве аргументов в `create` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
132− `create` создает новое развертывание в кластере используя полученные аргументы.
133− Например, он может выполнять:
134−
135−```bash
136−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
137− --set "global.project-name=$project_name" \
138− --set "global.base-domain=$base-domain" \
139− --set "app.tag=$tag" \
140− --set "app.env.foo=$app_env_override_1" \
141− --set "app.bar=$deployment_override_1" \
142− --wait \
143− --timeout 300
144−```
145−
146−#### Create via CLI
147−![Create](../diagrams/images/technical-architecture-create-via-cli.png)
148−
149−#### Create via UI
150−![Create](../diagrams/images/technical-architecture-create-via-ui.png)
151−
152−### Update
153−
154−_update_ – обновление существующего развертывания.
155− В качестве аргументов принимаются `name`, `tag` и опционально изменения `overrides` (уровня App или Deployment, открытие или секретные).
156−
157− Затем из базы достаются `overrides` (уровня App или Deployment, открытие или секретные), они мерджатся с изменениями `overrides`
158− и передаются вместе с `project-name`, `base-domain`, `namespace` в качестве аргументов в `update` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
159− `update` обновляет существующее развертывание в кластере используя полученные аргументы.
160− Например, он может выполнять:
161−
162−```bash
163−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
164− --set "global.project-name=$project_name" \
165− --set "global.base-domain=$base-domain" \
166− --set "app.tag=$tag" \
167− --set "app.env.foo=$app_env_override_1" \
168− --set "app.bar=$deployment_override_1" \
169− --wait \
170− --timeout 300
171−```
172−
173−
174−#### Update via CLI
175−![Update](../diagrams/images/technical-architecture-update-via-cli.png)
176−
177−#### Update via UI
178−![Update](../diagrams/images/technical-architecture-update-via-ui.png)
179−
180−### Archive
181−
182−_delete_ – архивирование существующего развертывания.
183− Производится удаление только подов, Persistent Volumes (диски) сохраняются. Отменить действие этой команды можно с помощью команды restore.
184− В качестве аргументов принимается `name`.
185−
186− `name` вместе с `project-name`, `base-domain`, `namespace` передаются в качестве аргументов в `archive` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
187− `archive` удаляет существующее развертывание в кластере, используя полученные аргументы.
188− Например он может выполнять:
189−
190−```bash
191−helm delete "$name" --purge
192−```
193−
194−
195−#### Archive via CLI
196−![Archive](../diagrams/images/technical-architecture-archive-via-cli.png)
197−
198−#### Archive via UI
199−![Archive](../diagrams/images/technical-architecture-archive-via-ui.png)
200−
201−### Cleanup
202−
203−_cleanup_ – полная очистка развертывания.
204− В качестве аргументов принимаeтся `name`.
205−
206− Например, можно удалять сертификаты, Persistent Volume Claim и Persistent Volumes.
207−
208− `name` вместе с `project-name`, `base-domain`, `namespace` передаются в качестве аргументов в `cleanup` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
209− `cleanup` выполняет освобождение ресурсов в кластере, которые ранее использовались одним из удаленных развертываний.
210− Например, он может выполнять:
211−
212−```bash
213−kubectl delete pvc -n "$namespace" "$name-postgres-pvc"
214−kubectl delete certificate -n "$namespace" "$name-postgres-cert"
215−```
216−
217−#### Cleanup via CLI
218−![Cleanup](../diagrams/images/technical-architecture-cleanup-via-cli.png)
219−
220−#### Cleanup via UI
221−![Cleanup](../diagrams/images/technical-architecture-cleanup-via-ui.png)
222−
223−### Restore
224−
225−_restore_ – восстановление заархивированного развертывания с последними настройками.
226− В качестве аргументов принимается `name`.
227−
228− Из базы достаются недостающие для восстановления `tag` и опциональные `overrides` (уровня App или Deployment, открытие или секретные). Они передаются вместе с `project-name`, `base-domain`, `namespace` в качестве аргументов в `restore` из [_скриптов управления развертываниями (control scripts)_](#скрипты-управления-развертываниями-control-scripts).
229− `restore` выполняет восстановление в кластере ранее удаленного развертывания используя полученные аргументы.
230− Например, он может выполнять:
231−
232−```bash
233−helm upgrade --install --namespace "$namespace" "$name" "$deployment_chart" \
234− --set "global.project-name=$project_name" \
235− --set "global.base-domain=$base-domain" \
236− --set "app.tag=$tag" \
237− --set "app.env.foo=$app_env_override_1" \
238− --set "app.bar=$deployment_override_1" \
239− --wait \
240− --timeout 300
241−```
242−
243−#### Restore via CLI
244−![Restore](../diagrams/images/technical-architecture-restore-via-cli.png)
245−
246−#### Restore via UI
247−![Restore](../diagrams/images/technical-architecture-restore-via-ui.png)
248−
249−## How we use it
250−
251−Мы используем несколько кластеров [_Kubernetes_][kube]: отдельный кластер для каждого приложения, а так же разделяем по кластерам _production_ и _deployment_ окружения.
252−
253−В каждый _deployment_ кластер мы устанавливаем _Octopod_, через _Octopod_ осуществляем разворачивание различных версий deployment'ов необходимых для QA.
254−
255−## Deployment state transitions
256−
257−Существует 6 статусов развертываний:
258−1. *Running*
259−2. *Failure*
260−3. *CreatePending*
261−4. *UpdatePending*
262−5. *DeletePending*
263−6. *Archived*
264−
265−_Running_, _Failure_, _Archived_ являются постоянными, т.е. развертывание уже не находится в режиме выполнения команды.
266−
267−*CreatePending*, *UpdatePending*, *DeletePending* являются переходными, т.е. развертывание находится в режиме выполнения команды.
268−
269−![Deployment Statuses](../diagrams/images/technical-architecture-deployment-states-fsm.png)
270−
271−<!-- [kubectl]: https://kubernetes.io/docs/reference/kubectl/ -->
272−<!-- [helm]: https://helm.sh -->
273−<!-- [kubedog]: https://github.com/werf/kubedog -->
274−[kube]: https://kubernetes.io
275−[chart]: https://helm.sh/docs/topics/charts/
helm-control-scripts/Cargo.lock+459−168
@@ -1,7 +1,5 @@
11# This file is automatically @generated by Cargo.
22# It is not intended for manual editing.
3−version = 3
4−
53[[package]]
64name = "adler32"
75version = "1.2.0"
@@ -47,6 +45,17 @@ dependencies = [
4745 "syn",
4846]
4947
48+[[package]]
49+name = "async-trait"
50+version = "0.1.51"
51+source = "registry+https://github.com/rust-lang/crates.io-index"
52+checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
53+dependencies = [
54+ "proc-macro2",
55+ "quote",
56+ "syn",
57+]
58+
5059[[package]]
5160name = "atty"
5261version = "0.2.14"
@@ -72,9 +81,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
7281
7382[[package]]
7483name = "bitflags"
75−version = "1.2.1"
84+version = "1.3.2"
7685source = "registry+https://github.com/rust-lang/crates.io-index"
77−checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
86+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
7887
7988[[package]]
8089name = "block-buffer"
@@ -87,21 +96,21 @@ dependencies = [
8796
8897[[package]]
8998name = "bumpalo"
90−version = "3.7.0"
99+version = "3.8.0"
91100source = "registry+https://github.com/rust-lang/crates.io-index"
92−checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
101+checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
93102
94103[[package]]
95104name = "bytes"
96−version = "1.0.1"
105+version = "1.1.0"
97106source = "registry+https://github.com/rust-lang/crates.io-index"
98−checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
107+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
99108
100109[[package]]
101110name = "cc"
102−version = "1.0.69"
111+version = "1.0.71"
103112source = "registry+https://github.com/rust-lang/crates.io-index"
104−checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
113+checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
105114
106115[[package]]
107116name = "cfg-if"
@@ -132,7 +141,7 @@ dependencies = [
132141 "ansi_term",
133142 "atty",
134143 "bitflags",
135− "strsim",
144+ "strsim 0.8.0",
136145 "textwrap",
137146 "unicode-width",
138147 "vec_map",
@@ -140,9 +149,9 @@ dependencies = [
140149
141150[[package]]
142151name = "core-foundation"
143−version = "0.9.1"
152+version = "0.9.2"
144153source = "registry+https://github.com/rust-lang/crates.io-index"
145−checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
154+checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
146155dependencies = [
147156 "core-foundation-sys",
148157 "libc",
@@ -150,15 +159,15 @@ dependencies = [
150159
151160[[package]]
152161name = "core-foundation-sys"
153−version = "0.8.2"
162+version = "0.8.3"
154163source = "registry+https://github.com/rust-lang/crates.io-index"
155−checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
164+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
156165
157166[[package]]
158167name = "cpufeatures"
159−version = "0.1.5"
168+version = "0.2.1"
160169source = "registry+https://github.com/rust-lang/crates.io-index"
161−checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
170+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
162171dependencies = [
163172 "libc",
164173]
@@ -172,6 +181,51 @@ dependencies = [
172181 "cfg-if",
173182]
174183
184+[[package]]
185+name = "crypto-mac"
186+version = "0.11.1"
187+source = "registry+https://github.com/rust-lang/crates.io-index"
188+checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
189+dependencies = [
190+ "generic-array",
191+ "subtle",
192+]
193+
194+[[package]]
195+name = "darling"
196+version = "0.12.4"
197+source = "registry+https://github.com/rust-lang/crates.io-index"
198+checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
199+dependencies = [
200+ "darling_core",
201+ "darling_macro",
202+]
203+
204+[[package]]
205+name = "darling_core"
206+version = "0.12.4"
207+source = "registry+https://github.com/rust-lang/crates.io-index"
208+checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
209+dependencies = [
210+ "fnv",
211+ "ident_case",
212+ "proc-macro2",
213+ "quote",
214+ "strsim 0.10.0",
215+ "syn",
216+]
217+
218+[[package]]
219+name = "darling_macro"
220+version = "0.12.4"
221+source = "registry+https://github.com/rust-lang/crates.io-index"
222+checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
223+dependencies = [
224+ "darling_core",
225+ "quote",
226+ "syn",
227+]
228+
175229[[package]]
176230name = "digest"
177231version = "0.9.0"
@@ -234,6 +288,12 @@ version = "0.4.8"
234288source = "registry+https://github.com/rust-lang/crates.io-index"
235289checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
236290
291+[[package]]
292+name = "dyn-clone"
293+version = "1.0.4"
294+source = "registry+https://github.com/rust-lang/crates.io-index"
295+checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
296+
237297[[package]]
238298name = "either"
239299version = "1.6.1"
@@ -242,9 +302,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
242302
243303[[package]]
244304name = "encoding_rs"
245−version = "0.8.28"
305+version = "0.8.29"
246306source = "registry+https://github.com/rust-lang/crates.io-index"
247−checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
307+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
248308dependencies = [
249309 "cfg-if",
250310]
@@ -316,9 +376,9 @@ dependencies = [
316376
317377[[package]]
318378name = "futures"
319−version = "0.3.16"
379+version = "0.3.17"
320380source = "registry+https://github.com/rust-lang/crates.io-index"
321−checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
381+checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
322382dependencies = [
323383 "futures-channel",
324384 "futures-core",
@@ -331,9 +391,9 @@ dependencies = [
331391
332392[[package]]
333393name = "futures-channel"
334−version = "0.3.16"
394+version = "0.3.17"
335395source = "registry+https://github.com/rust-lang/crates.io-index"
336−checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
396+checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
337397dependencies = [
338398 "futures-core",
339399 "futures-sink",
@@ -341,15 +401,15 @@ dependencies = [
341401
342402[[package]]
343403name = "futures-core"
344−version = "0.3.16"
404+version = "0.3.17"
345405source = "registry+https://github.com/rust-lang/crates.io-index"
346−checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
406+checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
347407
348408[[package]]
349409name = "futures-executor"
350−version = "0.3.16"
410+version = "0.3.17"
351411source = "registry+https://github.com/rust-lang/crates.io-index"
352−checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
412+checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
353413dependencies = [
354414 "futures-core",
355415 "futures-task",
@@ -358,15 +418,15 @@ dependencies = [
358418
359419[[package]]
360420name = "futures-io"
361−version = "0.3.16"
421+version = "0.3.17"
362422source = "registry+https://github.com/rust-lang/crates.io-index"
363−checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
423+checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
364424
365425[[package]]
366426name = "futures-macro"
367−version = "0.3.16"
427+version = "0.3.17"
368428source = "registry+https://github.com/rust-lang/crates.io-index"
369−checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57"
429+checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
370430dependencies = [
371431 "autocfg",
372432 "proc-macro-hack",
@@ -377,21 +437,21 @@ dependencies = [
377437
378438[[package]]
379439name = "futures-sink"
380−version = "0.3.16"
440+version = "0.3.17"
381441source = "registry+https://github.com/rust-lang/crates.io-index"
382−checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
442+checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
383443
384444[[package]]
385445name = "futures-task"
386−version = "0.3.16"
446+version = "0.3.17"
387447source = "registry+https://github.com/rust-lang/crates.io-index"
388−checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
448+checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
389449
390450[[package]]
391451name = "futures-util"
392−version = "0.3.16"
452+version = "0.3.17"
393453source = "registry+https://github.com/rust-lang/crates.io-index"
394−checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
454+checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
395455dependencies = [
396456 "autocfg",
397457 "futures-channel",
@@ -431,9 +491,9 @@ dependencies = [
431491
432492[[package]]
433493name = "h2"
434−version = "0.3.3"
494+version = "0.3.6"
435495source = "registry+https://github.com/rust-lang/crates.io-index"
436−checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726"
496+checksum = "6c06815895acec637cd6ed6e9662c935b866d20a106f8361892893a7d9234964"
437497dependencies = [
438498 "bytes",
439499 "fnv",
@@ -465,15 +525,19 @@ dependencies = [
465525
466526[[package]]
467527name = "helm-control-scripts"
468−version = "0.1.0"
528+version = "0.2.3"
469529dependencies = [
470530 "dkregistry",
471531 "env_logger",
472532 "envy",
533+ "json-patch",
473534 "k8s-openapi",
474535 "kube",
475536 "log",
476537 "regex",
538+ "rusoto_core",
539+ "rusoto_ecr",
540+ "schemars",
477541 "serde",
478542 "serde_json",
479543 "serde_yaml",
@@ -484,18 +548,34 @@ dependencies = [
484548
485549[[package]]
486550name = "hermit-abi"
487−version = "0.1.18"
551+version = "0.1.19"
488552source = "registry+https://github.com/rust-lang/crates.io-index"
489−checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
553+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
490554dependencies = [
491555 "libc",
492556]
493557
558+[[package]]
559+name = "hex"
560+version = "0.4.3"
561+source = "registry+https://github.com/rust-lang/crates.io-index"
562+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
563+
564+[[package]]
565+name = "hmac"
566+version = "0.11.0"
567+source = "registry+https://github.com/rust-lang/crates.io-index"
568+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
569+dependencies = [
570+ "crypto-mac",
571+ "digest",
572+]
573+
494574[[package]]
495575name = "http"
496−version = "0.2.4"
576+version = "0.2.5"
497577source = "registry+https://github.com/rust-lang/crates.io-index"
498−checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
578+checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
499579dependencies = [
500580 "bytes",
501581 "fnv",
@@ -504,9 +584,9 @@ dependencies = [
504584
505585[[package]]
506586name = "http-body"
507−version = "0.4.2"
587+version = "0.4.3"
508588source = "registry+https://github.com/rust-lang/crates.io-index"
509−checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9"
589+checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5"
510590dependencies = [
511591 "bytes",
512592 "http",
@@ -515,9 +595,9 @@ dependencies = [
515595
516596[[package]]
517597name = "httparse"
518−version = "1.4.1"
598+version = "1.5.1"
519599source = "registry+https://github.com/rust-lang/crates.io-index"
520−checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68"
600+checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
521601
522602[[package]]
523603name = "httpdate"
@@ -533,9 +613,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
533613
534614[[package]]
535615name = "hyper"
536−version = "0.14.11"
616+version = "0.14.13"
537617source = "registry+https://github.com/rust-lang/crates.io-index"
538−checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11"
618+checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593"
539619dependencies = [
540620 "bytes",
541621 "futures-channel",
@@ -580,6 +660,12 @@ dependencies = [
580660 "tokio-native-tls",
581661]
582662
663+[[package]]
664+name = "ident_case"
665+version = "1.0.1"
666+source = "registry+https://github.com/rust-lang/crates.io-index"
667+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
668+
583669[[package]]
584670name = "idna"
585671version = "0.2.3"
@@ -603,9 +689,9 @@ dependencies = [
603689
604690[[package]]
605691name = "instant"
606−version = "0.1.10"
692+version = "0.1.12"
607693source = "registry+https://github.com/rust-lang/crates.io-index"
608−checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
694+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
609695dependencies = [
610696 "cfg-if",
611697]
@@ -618,19 +704,30 @@ checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
618704
619705[[package]]
620706name = "itoa"
621−version = "0.4.7"
707+version = "0.4.8"
622708source = "registry+https://github.com/rust-lang/crates.io-index"
623−checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
709+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
624710
625711[[package]]
626712name = "js-sys"
627−version = "0.3.52"
713+version = "0.3.55"
628714source = "registry+https://github.com/rust-lang/crates.io-index"
629−checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752"
715+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
630716dependencies = [
631717 "wasm-bindgen",
632718]
633719
720+[[package]]
721+name = "json-patch"
722+version = "0.2.6"
723+source = "registry+https://github.com/rust-lang/crates.io-index"
724+checksum = "f995a3c8f2bc3dd52a18a583e90f9ec109c047fa1603a853e46bcda14d2e279d"
725+dependencies = [
726+ "serde",
727+ "serde_json",
728+ "treediff",
729+]
730+
634731[[package]]
635732name = "jsonpath_lib"
636733version = "0.3.0"
@@ -679,6 +776,7 @@ dependencies = [
679776 "jsonpath_lib",
680777 "k8s-openapi",
681778 "kube-core",
779+ "kube-derive",
682780 "openssl",
683781 "pem",
684782 "pin-project",
@@ -702,12 +800,26 @@ checksum = "ffbce0d890efc42abb31e974419e25a643a97ab7c6b3b9498bda686d10b55f8d"
702800dependencies = [
703801 "form_urlencoded",
704802 "http",
803+ "json-patch",
705804 "k8s-openapi",
706805 "serde",
707806 "serde_json",
708807 "thiserror",
709808]
710809
810+[[package]]
811+name = "kube-derive"
812+version = "0.58.1"
813+source = "registry+https://github.com/rust-lang/crates.io-index"
814+checksum = "95a28be5ca4f233b5dd872f426fdc75d6765c34576b590c44564982c05fdb400"
815+dependencies = [
816+ "darling",
817+ "proc-macro2",
818+ "quote",
819+ "serde_json",
820+ "syn",
821+]
822+
711823[[package]]
712824name = "lazy_static"
713825version = "1.4.0"
@@ -716,15 +828,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
716828
717829[[package]]
718830name = "libc"
719−version = "0.2.97"
831+version = "0.2.104"
720832source = "registry+https://github.com/rust-lang/crates.io-index"
721−checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
833+checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
722834
723835[[package]]
724836name = "libflate"
725−version = "1.1.0"
837+version = "1.1.1"
726838source = "registry+https://github.com/rust-lang/crates.io-index"
727−checksum = "6d87eae36b3f680f7f01645121b782798b56ef33c53f83d1c66ba3a22b60bfe3"
839+checksum = "16364af76ebb39b5869bb32c81fa93573267cd8c62bb3474e28d78fac3fb141e"
728840dependencies = [
729841 "adler32",
730842 "crc32fast",
@@ -748,9 +860,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
748860
749861[[package]]
750862name = "lock_api"
751−version = "0.4.4"
863+version = "0.4.5"
752864source = "registry+https://github.com/rust-lang/crates.io-index"
753−checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
865+checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
754866dependencies = [
755867 "scopeguard",
756868]
@@ -766,15 +878,26 @@ dependencies = [
766878
767879[[package]]
768880name = "matches"
769−version = "0.1.8"
881+version = "0.1.9"
882+source = "registry+https://github.com/rust-lang/crates.io-index"
883+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
884+
885+[[package]]
886+name = "md-5"
887+version = "0.9.1"
770888source = "registry+https://github.com/rust-lang/crates.io-index"
771−checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
889+checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
890+dependencies = [
891+ "block-buffer",
892+ "digest",
893+ "opaque-debug",
894+]
772895
773896[[package]]
774897name = "memchr"
775−version = "2.4.0"
898+version = "2.4.1"
776899source = "registry+https://github.com/rust-lang/crates.io-index"
777−checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
900+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
778901
779902[[package]]
780903name = "mime"
@@ -784,9 +907,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
784907
785908[[package]]
786909name = "mio"
787−version = "0.7.13"
910+version = "0.7.14"
788911source = "registry+https://github.com/rust-lang/crates.io-index"
789−checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
912+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
790913dependencies = [
791914 "libc",
792915 "log",
@@ -806,9 +929,9 @@ dependencies = [
806929
807930[[package]]
808931name = "native-tls"
809−version = "0.2.7"
932+version = "0.2.8"
810933source = "registry+https://github.com/rust-lang/crates.io-index"
811−checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
934+checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
812935dependencies = [
813936 "lazy_static",
814937 "libc",
@@ -874,9 +997,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
874997
875998[[package]]
876999name = "openssl"
877−version = "0.10.35"
1000+version = "0.10.36"
8781001source = "registry+https://github.com/rust-lang/crates.io-index"
879−checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885"
1002+checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
8801003dependencies = [
8811004 "bitflags",
8821005 "cfg-if",
@@ -894,9 +1017,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
8941017
8951018[[package]]
8961019name = "openssl-sys"
897−version = "0.9.65"
1020+version = "0.9.67"
8981021source = "registry+https://github.com/rust-lang/crates.io-index"
899−checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d"
1022+checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
9001023dependencies = [
9011024 "autocfg",
9021025 "cc",
@@ -907,18 +1030,18 @@ dependencies = [
9071030
9081031[[package]]
9091032name = "ordered-float"
910−version = "2.7.0"
1033+version = "2.8.0"
9111034source = "registry+https://github.com/rust-lang/crates.io-index"
912−checksum = "039f02eb0f69271f26abe3202189275d7aa2258b903cb0281b5de710a2570ff3"
1035+checksum = "97c9d06878b3a851e8026ef94bf7fef9ba93062cd412601da4d9cf369b1cc62d"
9131036dependencies = [
9141037 "num-traits",
9151038]
9161039
9171040[[package]]
9181041name = "parking_lot"
919−version = "0.11.1"
1042+version = "0.11.2"
9201043source = "registry+https://github.com/rust-lang/crates.io-index"
921−checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
1044+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
9221045dependencies = [
9231046 "instant",
9241047 "lock_api",
@@ -927,9 +1050,9 @@ dependencies = [
9271050
9281051[[package]]
9291052name = "parking_lot_core"
930−version = "0.8.3"
1053+version = "0.8.5"
9311054source = "registry+https://github.com/rust-lang/crates.io-index"
932−checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
1055+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
9331056dependencies = [
9341057 "cfg-if",
9351058 "instant",
@@ -990,15 +1113,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
9901113
9911114[[package]]
9921115name = "pkg-config"
993−version = "0.3.19"
1116+version = "0.3.20"
9941117source = "registry+https://github.com/rust-lang/crates.io-index"
995−checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
1118+checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
9961119
9971120[[package]]
9981121name = "ppv-lite86"
999−version = "0.2.10"
1122+version = "0.2.14"
10001123source = "registry+https://github.com/rust-lang/crates.io-index"
1001−checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
1124+checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741"
10021125
10031126[[package]]
10041127name = "proc-macro-error"
@@ -1038,18 +1161,18 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
10381161
10391162[[package]]
10401163name = "proc-macro2"
1041−version = "1.0.27"
1164+version = "1.0.30"
10421165source = "registry+https://github.com/rust-lang/crates.io-index"
1043−checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
1166+checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
10441167dependencies = [
10451168 "unicode-xid",
10461169]
10471170
10481171[[package]]
10491172name = "quote"
1050−version = "1.0.9"
1173+version = "1.0.10"
10511174source = "registry+https://github.com/rust-lang/crates.io-index"
1052−checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
1175+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
10531176dependencies = [
10541177 "proc-macro2",
10551178]
@@ -1096,9 +1219,9 @@ dependencies = [
10961219
10971220[[package]]
10981221name = "redox_syscall"
1099−version = "0.2.9"
1222+version = "0.2.10"
11001223source = "registry+https://github.com/rust-lang/crates.io-index"
1101−checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
1224+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
11021225dependencies = [
11031226 "bitflags",
11041227]
@@ -1141,9 +1264,9 @@ dependencies = [
11411264
11421265[[package]]
11431266name = "reqwest"
1144−version = "0.11.4"
1267+version = "0.11.6"
11451268source = "registry+https://github.com/rust-lang/crates.io-index"
1146−checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22"
1269+checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280"
11471270dependencies = [
11481271 "base64",
11491272 "bytes",
@@ -1180,6 +1303,98 @@ version = "1.0.1"
11801303source = "registry+https://github.com/rust-lang/crates.io-index"
11811304checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac"
11821305
1306+[[package]]
1307+name = "rusoto_core"
1308+version = "0.47.0"
1309+source = "registry+https://github.com/rust-lang/crates.io-index"
1310+checksum = "5b4f000e8934c1b4f70adde180056812e7ea6b1a247952db8ee98c94cd3116cc"
1311+dependencies = [
1312+ "async-trait",
1313+ "base64",
1314+ "bytes",
1315+ "crc32fast",
1316+ "futures",
1317+ "http",
1318+ "hyper",
1319+ "hyper-tls",
1320+ "lazy_static",
1321+ "log",
1322+ "rusoto_credential",
1323+ "rusoto_signature",
1324+ "rustc_version",
1325+ "serde",
1326+ "serde_json",
1327+ "tokio",
1328+ "xml-rs",
1329+]
1330+
1331+[[package]]
1332+name = "rusoto_credential"
1333+version = "0.47.0"
1334+source = "registry+https://github.com/rust-lang/crates.io-index"
1335+checksum = "6a46b67db7bb66f5541e44db22b0a02fed59c9603e146db3a9e633272d3bac2f"
1336+dependencies = [
1337+ "async-trait",
1338+ "chrono",
1339+ "dirs-next",
1340+ "futures",
1341+ "hyper",
1342+ "serde",
1343+ "serde_json",
1344+ "shlex",
1345+ "tokio",
1346+ "zeroize",
1347+]
1348+
1349+[[package]]
1350+name = "rusoto_ecr"
1351+version = "0.47.0"
1352+source = "registry+https://github.com/rust-lang/crates.io-index"
1353+checksum = "93ec4a28e4fb276307c2129abb16cf7d5573da4ec24d9e3895cc5c8a8bc21a4d"
1354+dependencies = [
1355+ "async-trait",
1356+ "bytes",
1357+ "futures",
1358+ "rusoto_core",
1359+ "serde",
1360+ "serde_json",
1361+]
1362+
1363+[[package]]
1364+name = "rusoto_signature"
1365+version = "0.47.0"
1366+source = "registry+https://github.com/rust-lang/crates.io-index"
1367+checksum = "6264e93384b90a747758bcc82079711eacf2e755c3a8b5091687b5349d870bcc"
1368+dependencies = [
1369+ "base64",
1370+ "bytes",
1371+ "chrono",
1372+ "digest",
1373+ "futures",
1374+ "hex",
1375+ "hmac",
1376+ "http",
1377+ "hyper",
1378+ "log",
1379+ "md-5",
1380+ "percent-encoding",
1381+ "pin-project-lite",
1382+ "rusoto_credential",
1383+ "rustc_version",
1384+ "serde",
1385+ "sha2",
1386+ "tokio",
1387+]
1388+
1389+[[package]]
1390+name = "rustc_version"
1391+version = "0.4.0"
1392+source = "registry+https://github.com/rust-lang/crates.io-index"
1393+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
1394+dependencies = [
1395+ "semver",
1396+]
1397+
11831398[[package]]
11841399name = "ryu"
11851400version = "1.0.5"
@@ -1196,6 +1411,30 @@ dependencies = [
11961411 "winapi",
11971412]
11981413
1414+[[package]]
1415+name = "schemars"
1416+version = "0.8.6"
1417+source = "registry+https://github.com/rust-lang/crates.io-index"
1418+checksum = "d7a48d098c2a7fdf5740b19deb1181b4fb8a9e68e03ae517c14cde04b5725409"
1419+dependencies = [
1420+ "dyn-clone",
1421+ "schemars_derive",
1422+ "serde",
1423+ "serde_json",
1424+]
1425+
1426+[[package]]
1427+name = "schemars_derive"
1428+version = "0.8.6"
1429+source = "registry+https://github.com/rust-lang/crates.io-index"
1430+checksum = "4a9ea2a613fe4cd7118b2bb101a25d8ae6192e1975179b67b2f17afd11e70ac8"
1431+dependencies = [
1432+ "proc-macro2",
1433+ "quote",
1434+ "serde_derive_internals",
1435+ "syn",
1436+]
1437+
11991438[[package]]
12001439name = "scopeguard"
12011440version = "1.1.0"
@@ -1204,9 +1443,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
12041443
12051444[[package]]
12061445name = "security-framework"
1207−version = "2.3.1"
1446+version = "2.4.2"
12081447source = "registry+https://github.com/rust-lang/crates.io-index"
1209−checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
1448+checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
12101449dependencies = [
12111450 "bitflags",
12121451 "core-foundation",
@@ -1217,19 +1456,25 @@ dependencies = [
12171456
12181457[[package]]
12191458name = "security-framework-sys"
1220−version = "2.3.0"
1459+version = "2.4.2"
12211460source = "registry+https://github.com/rust-lang/crates.io-index"
1222−checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
1461+checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
12231462dependencies = [
12241463 "core-foundation-sys",
12251464 "libc",
12261465]
12271466
1467+[[package]]
1468+name = "semver"
1469+version = "1.0.4"
1470+source = "registry+https://github.com/rust-lang/crates.io-index"
1471+checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
1472+
12281473[[package]]
12291474name = "serde"
1230−version = "1.0.126"
1475+version = "1.0.130"
12311476source = "registry+https://github.com/rust-lang/crates.io-index"
1232−checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
1477+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
12331478dependencies = [
12341479 "serde_derive",
12351480]
@@ -1246,9 +1491,20 @@ dependencies = [
12461491
12471492[[package]]
12481493name = "serde_derive"
1249−version = "1.0.126"
1494+version = "1.0.130"
1495+source = "registry+https://github.com/rust-lang/crates.io-index"
1496+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
1497+dependencies = [
1498+ "proc-macro2",
1499+ "quote",
1500+ "syn",
1501+]
1502+
1503+[[package]]
1504+name = "serde_derive_internals"
1505+version = "0.25.0"
12501506source = "registry+https://github.com/rust-lang/crates.io-index"
1251−checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
1507+checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
12521508dependencies = [
12531509 "proc-macro2",
12541510 "quote",
@@ -1266,9 +1522,9 @@ dependencies = [
12661522
12671523[[package]]
12681524name = "serde_json"
1269−version = "1.0.64"
1525+version = "1.0.68"
12701526source = "registry+https://github.com/rust-lang/crates.io-index"
1271−checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
1527+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
12721528dependencies = [
12731529 "indexmap",
12741530 "itoa",
@@ -1290,21 +1546,21 @@ dependencies = [
12901546
12911547[[package]]
12921548name = "serde_yaml"
1293−version = "0.8.17"
1549+version = "0.8.21"
12941550source = "registry+https://github.com/rust-lang/crates.io-index"
1295−checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
1551+checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af"
12961552dependencies = [
12971553 "dtoa",
1298− "linked-hash-map",
1554+ "indexmap",
12991555 "serde",
13001556 "yaml-rust",
13011557]
13021558
13031559[[package]]
13041560name = "sha2"
1305−version = "0.9.5"
1561+version = "0.9.8"
13061562source = "registry+https://github.com/rust-lang/crates.io-index"
1307−checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
1563+checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
13081564dependencies = [
13091565 "block-buffer",
13101566 "cfg-if",
@@ -1313,6 +1569,12 @@ dependencies = [
13131569 "opaque-debug",
13141570]
13151571
1572+[[package]]
1573+name = "shlex"
1574+version = "1.1.0"
1575+source = "registry+https://github.com/rust-lang/crates.io-index"
1576+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
1577+
13161578[[package]]
13171579name = "signal-hook-registry"
13181580version = "1.4.0"
@@ -1324,21 +1586,21 @@ dependencies = [
13241586
13251587[[package]]
13261588name = "slab"
1327−version = "0.4.3"
1589+version = "0.4.5"
13281590source = "registry+https://github.com/rust-lang/crates.io-index"
1329−checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
1591+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
13301592
13311593[[package]]
13321594name = "smallvec"
1333−version = "1.6.1"
1595+version = "1.7.0"
13341596source = "registry+https://github.com/rust-lang/crates.io-index"
1335−checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
1597+checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
13361598
13371599[[package]]
13381600name = "socket2"
1339−version = "0.4.1"
1601+version = "0.4.2"
13401602source = "registry+https://github.com/rust-lang/crates.io-index"
1341−checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad"
1603+checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
13421604dependencies = [
13431605 "libc",
13441606 "winapi",
@@ -1350,11 +1612,17 @@ version = "0.8.0"
13501612source = "registry+https://github.com/rust-lang/crates.io-index"
13511613checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
13521614
1615+[[package]]
1616+name = "strsim"
1617+version = "0.10.0"
1618+source = "registry+https://github.com/rust-lang/crates.io-index"
1619+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
1620+
13531621[[package]]
13541622name = "structopt"
1355−version = "0.3.21"
1623+version = "0.3.25"
13561624source = "registry+https://github.com/rust-lang/crates.io-index"
1357−checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c"
1625+checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c"
13581626dependencies = [
13591627 "clap",
13601628 "lazy_static",
@@ -1363,9 +1631,9 @@ dependencies = [
13631631
13641632[[package]]
13651633name = "structopt-derive"
1366−version = "0.4.14"
1634+version = "0.4.18"
13671635source = "registry+https://github.com/rust-lang/crates.io-index"
1368−checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
1636+checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
13691637dependencies = [
13701638 "heck",
13711639 "proc-macro-error",
@@ -1392,11 +1660,17 @@ dependencies = [
13921660 "syn",
13931661]
13941662
1663+[[package]]
1664+name = "subtle"
1665+version = "2.4.1"
1666+source = "registry+https://github.com/rust-lang/crates.io-index"
1667+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
1668+
13951669[[package]]
13961670name = "syn"
1397−version = "1.0.73"
1671+version = "1.0.80"
13981672source = "registry+https://github.com/rust-lang/crates.io-index"
1399−checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
1673+checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
14001674dependencies = [
14011675 "proc-macro2",
14021676 "quote",
@@ -1405,9 +1679,9 @@ dependencies = [
14051679
14061680[[package]]
14071681name = "tar"
1408−version = "0.4.35"
1682+version = "0.4.37"
14091683source = "registry+https://github.com/rust-lang/crates.io-index"
1410−checksum = "7d779dc6aeff029314570f666ec83f19df7280bb36ef338442cfa8c604021b80"
1684+checksum = "d6f5515d3add52e0bbdcad7b83c388bb36ba7b754dda3b5f5bc2d38640cdba5c"
14111685dependencies = [
14121686 "filetime",
14131687 "libc",
@@ -1448,18 +1722,18 @@ dependencies = [
14481722
14491723[[package]]
14501724name = "thiserror"
1451−version = "1.0.26"
1725+version = "1.0.30"
14521726source = "registry+https://github.com/rust-lang/crates.io-index"
1453−checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
1727+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
14541728dependencies = [
14551729 "thiserror-impl",
14561730]
14571731
14581732[[package]]
14591733name = "thiserror-impl"
1460−version = "1.0.26"
1734+version = "1.0.30"
14611735source = "registry+https://github.com/rust-lang/crates.io-index"
1462−checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
1736+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
14631737dependencies = [
14641738 "proc-macro2",
14651739 "quote",
@@ -1479,9 +1753,9 @@ dependencies = [
14791753
14801754[[package]]
14811755name = "tinyvec"
1482−version = "1.3.1"
1756+version = "1.5.0"
14831757source = "registry+https://github.com/rust-lang/crates.io-index"
1484−checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
1758+checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
14851759dependencies = [
14861760 "tinyvec_macros",
14871761]
@@ -1494,9 +1768,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
14941768
14951769[[package]]
14961770name = "tokio"
1497−version = "1.7.1"
1771+version = "1.12.0"
14981772source = "registry+https://github.com/rust-lang/crates.io-index"
1499−checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2"
1773+checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
15001774dependencies = [
15011775 "autocfg",
15021776 "bytes",
@@ -1524,9 +1798,9 @@ dependencies = [
15241798
15251799[[package]]
15261800name = "tokio-macros"
1527−version = "1.3.0"
1801+version = "1.5.0"
15281802source = "registry+https://github.com/rust-lang/crates.io-index"
1529−checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
1803+checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
15301804dependencies = [
15311805 "proc-macro2",
15321806 "quote",
@@ -1545,9 +1819,9 @@ dependencies = [
15451819
15461820[[package]]
15471821name = "tokio-util"
1548−version = "0.6.7"
1822+version = "0.6.8"
15491823source = "registry+https://github.com/rust-lang/crates.io-index"
1550−checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
1824+checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd"
15511825dependencies = [
15521826 "bytes",
15531827 "futures-core",
@@ -1559,13 +1833,14 @@ dependencies = [
15591833
15601834[[package]]
15611835name = "tower"
1562−version = "0.4.8"
1836+version = "0.4.10"
15631837source = "registry+https://github.com/rust-lang/crates.io-index"
1564−checksum = "f60422bc7fefa2f3ec70359b8ff1caff59d785877eb70595904605bcc412470f"
1838+checksum = "c00e500fff5fa1131c866b246041a6bf96da9c965f8fe4128cb1421f23e93c00"
15651839dependencies = [
15661840 "futures-core",
15671841 "futures-util",
15681842 "pin-project",
1843+ "pin-project-lite",
15691844 "tokio",
15701845 "tokio-util",
15711846 "tower-layer",
@@ -1605,9 +1880,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
16051880
16061881[[package]]
16071882name = "tracing"
1608−version = "0.1.26"
1883+version = "0.1.29"
16091884source = "registry+https://github.com/rust-lang/crates.io-index"
1610−checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
1885+checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
16111886dependencies = [
16121887 "cfg-if",
16131888 "log",
@@ -1618,9 +1893,9 @@ dependencies = [
16181893
16191894[[package]]
16201895name = "tracing-attributes"
1621−version = "0.1.15"
1896+version = "0.1.18"
16221897source = "registry+https://github.com/rust-lang/crates.io-index"
1623−checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2"
1898+checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
16241899dependencies = [
16251900 "proc-macro2",
16261901 "quote",
@@ -1629,13 +1904,22 @@ dependencies = [
16291904
16301905[[package]]
16311906name = "tracing-core"
1632−version = "0.1.18"
1907+version = "0.1.21"
16331908source = "registry+https://github.com/rust-lang/crates.io-index"
1634−checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052"
1909+checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
16351910dependencies = [
16361911 "lazy_static",
16371912]
16381913
1914+[[package]]
1915+name = "treediff"
1916+version = "3.0.2"
1917+source = "registry+https://github.com/rust-lang/crates.io-index"
1918+checksum = "761e8d5ad7ce14bb82b7e61ccc0ca961005a275a060b9644a2431aa11553c2ff"
1919+dependencies = [
1920+ "serde_json",
1921+]
1922+
16391923[[package]]
16401924name = "try-lock"
16411925version = "0.2.3"
@@ -1644,18 +1928,15 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
16441928
16451929[[package]]
16461930name = "typenum"
1647−version = "1.13.0"
1931+version = "1.14.0"
16481932source = "registry+https://github.com/rust-lang/crates.io-index"
1649−checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
1933+checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
16501934
16511935[[package]]
16521936name = "unicode-bidi"
1653−version = "0.3.5"
1937+version = "0.3.7"
16541938source = "registry+https://github.com/rust-lang/crates.io-index"
1655−checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0"
1656−dependencies = [
1657− "matches",
1658−]
1939+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
16591940
16601941[[package]]
16611942name = "unicode-normalization"
@@ -1668,15 +1949,15 @@ dependencies = [
16681949
16691950[[package]]
16701951name = "unicode-segmentation"
1671−version = "1.7.1"
1952+version = "1.8.0"
16721953source = "registry+https://github.com/rust-lang/crates.io-index"
1673−checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
1954+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
16741955
16751956[[package]]
16761957name = "unicode-width"
1677−version = "0.1.8"
1958+version = "0.1.9"
16781959source = "registry+https://github.com/rust-lang/crates.io-index"
1679−checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
1960+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
16801961
16811962[[package]]
16821963name = "unicode-xid"
@@ -1732,21 +2013,19 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
17322013
17332014[[package]]
17342015name = "wasm-bindgen"
1735−version = "0.2.75"
2016+version = "0.2.78"
17362017source = "registry+https://github.com/rust-lang/crates.io-index"
1737−checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586"
2018+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
17382019dependencies = [
17392020 "cfg-if",
1740− "serde",
1741− "serde_json",
17422021 "wasm-bindgen-macro",
17432022]
17442023
17452024[[package]]
17462025name = "wasm-bindgen-backend"
1747−version = "0.2.75"
2026+version = "0.2.78"
17482027source = "registry+https://github.com/rust-lang/crates.io-index"
1749−checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f"
2028+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
17502029dependencies = [
17512030 "bumpalo",
17522031 "lazy_static",
@@ -1759,9 +2038,9 @@ dependencies = [
17592038
17602039[[package]]
17612040name = "wasm-bindgen-futures"
1762−version = "0.4.25"
2041+version = "0.4.28"
17632042source = "registry+https://github.com/rust-lang/crates.io-index"
1764−checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c"
2043+checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
17652044dependencies = [
17662045 "cfg-if",
17672046 "js-sys",
@@ -1771,9 +2050,9 @@ dependencies = [
17712050
17722051[[package]]
17732052name = "wasm-bindgen-macro"
1774−version = "0.2.75"
2053+version = "0.2.78"
17752054source = "registry+https://github.com/rust-lang/crates.io-index"
1776−checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c"
2055+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
17772056dependencies = [
17782057 "quote",
17792058 "wasm-bindgen-macro-support",
@@ -1781,9 +2060,9 @@ dependencies = [
17812060
17822061[[package]]
17832062name = "wasm-bindgen-macro-support"
1784−version = "0.2.75"
2063+version = "0.2.78"
17852064source = "registry+https://github.com/rust-lang/crates.io-index"
1786−checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f"
2065+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
17872066dependencies = [
17882067 "proc-macro2",
17892068 "quote",
@@ -1794,15 +2073,15 @@ dependencies = [
17942073
17952074[[package]]
17962075name = "wasm-bindgen-shared"
1797−version = "0.2.75"
2076+version = "0.2.78"
17982077source = "registry+https://github.com/rust-lang/crates.io-index"
1799−checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2"
2078+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
18002079
18012080[[package]]
18022081name = "web-sys"
1803−version = "0.3.52"
2082+version = "0.3.55"
18042083source = "registry+https://github.com/rust-lang/crates.io-index"
1805−checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696"
2084+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
18062085dependencies = [
18072086 "js-sys",
18082087 "wasm-bindgen",
@@ -1857,6 +2136,12 @@ dependencies = [
18572136 "libc",
18582137]
18592138
2139+[[package]]
2140+name = "xml-rs"
2141+version = "0.8.4"
2142+source = "registry+https://github.com/rust-lang/crates.io-index"
2143+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
2144+
18602145[[package]]
18612146name = "yaml-rust"
18622147version = "0.4.5"
@@ -1865,3 +2150,9 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
18652150dependencies = [
18662151 "linked-hash-map",
18672152]
2153+
2154+[[package]]
2155+name = "zeroize"
2156+version = "1.4.2"
2157+source = "registry+https://github.com/rust-lang/crates.io-index"
2158+checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970"
helm-control-scripts/Cargo.toml+28−4
@@ -1,6 +1,6 @@
11[package]
22name = "helm-control-scripts"
3−version = "0.1.0"
3+version = "0.2.3"
44authors = ["Aleksei Sizov <a.sizov@typeable.io>"]
55edition = "2018"
66
@@ -15,11 +15,15 @@ serde_json = "1.0"
1515serde_yaml = "0.8"
1616envy = "0.4"
1717yaml-rust = "0.4"
18−kube = "0.58.1"
18+kube = { version = "0.58.1", features = ["derive", "jsonpatch"]}
1919k8s-openapi = { version = "0.12.0", features = ["v1_19"]}
2020tokio = { version = "1.7.0", features = ["full"] }
2121dkregistry = { git = "https://github.com/camallo/dkregistry-rs", rev = "854d0da53bef5dd85b5e901123e85d43af97c74e"} # Version in crates uses old tokio
2222regex = "1"
23+rusoto_core = "0.47.0"
24+rusoto_ecr = "0.47.0"
25+schemars = "0.8.6"
26+json-patch = "0.2.6"
2327
2428[[bin]]
2529name = "create"
@@ -54,5 +58,25 @@ name = "init"
5458path = "src/init.rs"
5559
5660[[bin]]
57−name = "tag_check"
58−path = "src/tag_check.rs"
61+name = "config_check"
62+path = "src/config_check.rs"
63+
64+[[bin]]
65+name = "app_overrides"
66+path = "src/app_overrides.rs"
67+
68+[[bin]]
69+name = "deployment_overrides"
70+path = "src/deployment_overrides.rs"
71+
72+[[bin]]
73+name = "deployment_keys"
74+path = "src/deployment_keys.rs"
75+
76+[[bin]]
77+name = "app_keys"
78+path = "src/app_keys.rs"
79+
80+[[bin]]
81+name = "unarchive"
82+path = "src/unarchive.rs"
helm-control-scripts/Dockerfile+6−5
@@ -1,17 +1,13 @@
11FROM alpine:3
22ARG HELM_BIN=/utils/helm
3−ARG KUBECTL_BIN=/utils/kubectl
43ENV HELM_BIN=$HELM_BIN
5−ENV KUBECTL_BIN=$KUBECTL_BIN
64
75ADD https://get.helm.sh/helm-v3.6.0-linux-amd64.tar.gz /tmp/helm.tar.gz
8−ADD https://dl.k8s.io/release/v1.21.0/bin/linux/amd64/kubectl $KUBECTL_BIN
96
107RUN tar -xf /tmp/helm.tar.gz -C /tmp &&\
118 mkdir -p /utils &&\
129 cp /tmp/linux-amd64/helm $HELM_BIN &&\
1310 chmod +x $HELM_BIN &&\
14− chmod +x $KUBECTL_BIN &&\
1511 rm -r /tmp/*
1612
1713ADD target/x86_64-unknown-linux-musl/release/archive /utils/
@@ -22,4 +18,9 @@ ADD target/x86_64-unknown-linux-musl/release/create /utils/
2218ADD target/x86_64-unknown-linux-musl/release/info /utils/
2319ADD target/x86_64-unknown-linux-musl/release/update /utils/
2420ADD target/x86_64-unknown-linux-musl/release/init /utils/
25−ADD target/x86_64-unknown-linux-musl/release/tag_check /utils/
21+ADD target/x86_64-unknown-linux-musl/release/config_check /utils/
22+ADD target/x86_64-unknown-linux-musl/release/app_overrides /utils/
23+ADD target/x86_64-unknown-linux-musl/release/app_keys /utils/
24+ADD target/x86_64-unknown-linux-musl/release/deployment_overrides /utils/
25+ADD target/x86_64-unknown-linux-musl/release/deployment_keys /utils/
26+ADD target/x86_64-unknown-linux-musl/release/unarchive /utils/
helm-control-scripts/README.md+40−7
@@ -1,19 +1,24 @@
11# Generic scripts
22
33This is a complete suite of Octopod control scripts to deploy helm charts.
4−You can get pre-built docker images from [hub.docker.com/r/typeable/octopod-generic-utils](https://hub.docker.com/r/typeable/octopod-generic-utils).
4+You can get pre-built docker images from [typeable/octopod-helm-control-scripts](https://hub.docker.com/repository/docker/typeable/octopod-helm-control-scripts).
55
66### Contents
77
88- create – a script to install a helm release
99- update – a script to upgrade a helm release
10−- archive – a script to uninstall a helm release
10+- archive – a script to scale to zero all deployments and statefulsets
11+- archive_check – a script to check if all resources scaled to zero correctly
12+- unarchive - scale all deployments and statefulsets back (right now it will only scale to 1 and not to previous value)
1113- info – a script to print comma-separated pairs which will be placed as links in the Octopod Web UI
1214- cleanup – a script to delete dangling PVCs and letsencrypt certs left after `helm uninstall` command
1315- check – a script to check if resources created by helm are healthy. Right now only deployments and statefuls sets are checked
14−- archive_check – a script to check if `helm uninstall` really deleted the release
1516- init – a script to initialize something. This is the only script not run by Octopod, but as an init container.
16−- tag_check – a script to check that images in helm release are present in registries before invoking create script
17+- config_check – a script to check that configuration (such as image tag name) passed to Octopod is correct. This script is invoked before deployment creation.
18+- app_overrides - a scirpt which returns a list of default app overrides wichh were passed in default_overrides parameter described below.
19+- deployment_overrides - a scirpt which returns a list of default deployment overrides from default paramaters option.
20+- deployment_keys - a scripts which returns a list of the possible deployment overrides keys user can use.
21+- app_keys - a scripts which returns a list of the possible app overrides keys. These are parsed from an output of the `helm show values` command.
1722
1823### Parameters
1924
@@ -22,16 +27,44 @@ All scipts accept common parametes which are passed by Octopod when it invokes t
2227Also, several environment variables are used to parametrize the default behavior:
2328
2429- HELM_BIN – the path to the `helm` executable
25−- KUBECTL_BIN – the path to the `kubectl` executable
2630- HELM_USER – the (optional) user for a private helm registry
2731- HELM_PASS – the (optional) password for a private helm registry
2832- DEFAUTLS – the json with the default parameters (described below)
33+- HELM_ON_INIT_ONLY - run helm add and update repository as a part of init script execution only. Otherwise `helm repo add` and `helm repo update` will be executed every time before any other helm command.
34+- INGRESS_HOST_KEY - key name which will be populated with domain name generated for the Octopod deployment. Defaults to `ingress.hostname`.
2935
3036#### Default parameters
3137- default_overrides – an array with key-value pairs which will be passed as a `--set` flags for helm for each deployment
3238- chart_repo_url – the url for a chart repo
33−- chart_repo_name – the name of a chart repo (could be any string)
3439- chart_version – the version of a chart you want to install
3540- chart_name – the name of a chart you want to install
3641
37−These parameters, if set up in the `DEFAULTS` variable, will be passed to every deployment unless overridden in the "deployment overrides" section of an Octopod deployment configuration.
42+These parameters, if set up in the `DEFAULTS` variable, will be passed to every deployment unless overridden in the "deployment overrides" section of an Octopod deployment configuration.
43+
44+### Authorization notes
45+
46+Most of the control scripts require that credentials for various services be passed to them to function properly. Some credentials, like the Helm ones, are quite obvious, but others are not.
47+
48+`config_check`, in addition to the Helm credentials, requires access to your docker registry to check the availability of the docker image tags.
49+
50+Right now only plain docker registries and ECR are supported. ECR clients need to have valid AWS credentials set via [environment variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). You can provide any environment variables to Octopod by adding them to [env map](../charts/octopod/values.yaml#L86) in the Octopod helm chart.
51+
52+Note that if you are using EKS you don't need to add any additional configuration.
53+
54+Private registries, like Harbor with authorization haven't been tested with this set of scripts. If you are using a private registry and have troubles with the `config_check` script, please file an issue.
55+
56+#### Note about CRD support
57+
58+Right now these control scripts support `Posgresql` CRD from [postgres operator](https://github.com/zalando/postgres-operator) and `Kafka` CRD from [Strimzi](https://strimzi.io/). Explicit support is needed for the proper scaling of the resources, created by those CRDs. For example: just scaling stateful sets created by Kafka CRD won't work because the controller will keep scaling them back again.
59+
60+Explicit support for every CRD is necessary because the configurations are not standardized, and every CRD is implemented slightly differently.
61+
62+We'll keep supporting those CRDs, because we're using them in-house. If you want to support another CRD, you can file an issue or even a PR.
63+
64+<br />
65+
66+<p align="center">
67+ <i>Star the project of you like it</i>
68+</p>
69+
70+<p align="center"><a href="https://typeable.io"><img src="../img/typeable_logo.svg" width="177px"></img></a></p>
helm-control-scripts/src/app_keys.rsnew file+31
@@ -0,0 +1,31 @@
1+use helm_control_scripts::lib::*;
2+
3+fn main() {
4+ let mut log_builder = Builder::from_default_env();
5+ log_builder.target(Target::Stderr).filter(None, LevelFilter::Info).init();
6+ info!("Utils version {}", env!("CARGO_PKG_VERSION"));
7+ let envs = EnvVars::parse();
8+ info!("Env variables received {:?}", &envs);
9+ let cli_opts = CliOpts::from_args();
10+ info!("Cli options received {:?}", &cli_opts);
11+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
12+ helm_init(&envs, &deployment_parameters);
13+ let helm_values = HelmCmd {
14+ name: envs.helm_bin,
15+ mode: HelmMode::ShowValues,
16+ release_name: String::from(""),
17+ namespace: String::from(""),
18+ deployment_parameters: deployment_parameters,
19+ overrides: vec![],
20+ };
21+ info!("Generated Helm args: {:?}", &helm_values.args());
22+ match helm_values.run_stdout() {
23+ Ok(status) => {
24+ print!("{}", print_keys(helm_values_as_keys(status)));
25+ }
26+ Err(status) => {
27+ error!("Error during helm execution");
28+ panic!("{:?}", status);
29+ }
30+ }
31+}
helm-control-scripts/src/app_overrides.rsnew file+11
@@ -0,0 +1,11 @@
1+use helm_control_scripts::lib::*;
2+
3+fn main() {
4+ let mut log_builder = Builder::from_default_env();
5+ log_builder.target(Target::Stderr).filter(None, LevelFilter::Info).init();
6+ info!("Utils version {}", env!("CARGO_PKG_VERSION"));
7+ let envs = EnvVars::parse();
8+ info!("Env variables received {:?}", &envs);
9+ let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
10+ print!("{}", print_kv(default_values.app_overrides()));
11+}
helm-control-scripts/src/archive.rs+33−12
@@ -6,24 +6,45 @@ fn main() {
66 info!("Utils version {}", env!("CARGO_PKG_VERSION"));
77 let envs = EnvVars::parse();
88 info!("Env variables received {:?}", &envs);
9− let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
109 let cli_opts = CliOpts::from_args();
1110 info!("Cli options received {:?}", &cli_opts);
12− let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &default_values, &envs);
13− let helm_cmd = HelmCmd {
11+ let overrides = match overrides(&cli_opts, &envs) {
12+ Some(inner) => inner,
13+ None => vec![],
14+ };
15+
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ let namespace = String::from(&cli_opts.namespace);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
20+ None => {
21+ error!("mandatory name argument was not provided");
22+ panic!();
23+ }
24+ };
25+ let helm_template = HelmCmd {
1426 name: envs.helm_bin,
15− mode: HelmMode::Uninstall,
16− release_name: cli_opts.name,
17− release_domain: String::from(""),
27+ mode: HelmMode::Template,
28+ release_name: release_name,
1829 namespace: cli_opts.namespace,
1930 deployment_parameters: deployment_parameters,
20− overrides: vec![],
21− default_values: vec![],
22− image_tag: String::from("")
31+ overrides: overrides,
2332 };
24− info!("Generated Helm args: {:?}", &helm_cmd.args());
25− match helm_cmd.run() {
26− Ok(_status) => info!("Success!"),
33+ info!("Generated Helm args: {:?}", &helm_template.args());
34+ match helm_template.run_stdout() {
35+ Ok(status) => {
36+ let (deployments, statefulsets, _ingresses, _old_ingresses, postgresqls, kafkas) = match parse_to_k8s(status) {
37+ Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
38+ Err(err) => panic!("{}", err)
39+ };
40+ match scale(deployments, statefulsets, postgresqls, kafkas, namespace, 0) {
41+ Ok(_status) => info!("Success!"),
42+ Err(status) => {
43+ error!("Error checking statuses");
44+ panic!("{}", status);
45+ }
46+ }
47+ }
2748 Err(status) => {
2849 error!("Error during helm execution");
2950 panic!("{:?}", status);
helm-control-scripts/src/archive_check.rs+39−17
@@ -4,28 +4,50 @@ fn main() {
44 let mut log_builder = Builder::from_default_env();
55 log_builder.target(Target::Stdout).filter(None, LevelFilter::Info).init();
66 info!("Utils version {}", env!("CARGO_PKG_VERSION"));
7− let cli_opts = CliOpts::from_args();
87 let envs = EnvVars::parse();
98 info!("Env variables received {:?}", &envs);
10− let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
11− let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &default_values, &envs);
12− let helm_cmd = HelmCmd {
9+ let cli_opts = CliOpts::from_args();
10+ info!("Cli options received {:?}", &cli_opts);
11+ let overrides = match overrides(&cli_opts, &envs) {
12+ Some(inner) => inner,
13+ None => vec![],
14+ };
15+
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ let namespace = String::from(&cli_opts.namespace);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
20+ None => {
21+ error!("mandatory name argument was not provided");
22+ panic!();
23+ }
24+ };
25+ let helm_template = HelmCmd {
1326 name: envs.helm_bin,
14− mode: HelmMode::Status,
15− release_name: cli_opts.name,
16− release_domain: String::from(""),
27+ mode: HelmMode::Template,
28+ release_name: release_name,
1729 namespace: cli_opts.namespace,
1830 deployment_parameters: deployment_parameters,
19− overrides: vec![],
20− default_values: vec![],
21− image_tag: String::from("")
31+ overrides: overrides,
2232 };
23− info!("Generated Helm args: {:?}", &helm_cmd.args());
24− match helm_cmd.run() {
25− Ok(_status) => {
26− error!("Release is still present in cluster");
27− panic!();
28− },
29− Err(_status) => info!("Success!"),
33+ info!("Generated Helm args: {:?}", &helm_template.args());
34+ match helm_template.run_stdout() {
35+ Ok(status) => {
36+ let (deployments, statefulsets, _ingresses, _old_ingresses, _postgresqls, _kafkas) = match parse_to_k8s(status) {
37+ Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
38+ Err(err) => panic!("{}", err)
39+ };
40+ match check_all(deployments, statefulsets, namespace) {
41+ Ok(status) => {
42+ error!("Deployment hasn't scaled down correctly");
43+ panic!("{:?}", status);
44+ },
45+ Err(_status) => info!("Success!")
46+ }
47+ }
48+ Err(status) => {
49+ error!("Error during helm execution");
50+ panic!("{:?}", status);
51+ }
3052 }
3153}
helm-control-scripts/src/check.rs+11−17
@@ -6,48 +6,42 @@ fn main() {
66 info!("Utils version {}", env!("CARGO_PKG_VERSION"));
77 let envs = EnvVars::parse();
88 info!("Env variables received {:?}", &envs);
9− let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
109 let cli_opts = CliOpts::from_args();
1110 info!("Cli options received {:?}", &cli_opts);
12− let overrides = match overrides(&cli_opts) {
11+ let overrides = match overrides(&cli_opts, &envs) {
1312 Some(inner) => inner,
1413 None => vec![],
1514 };
16−
17− let domain_name = domain_name(&cli_opts);
18− info!("Domain generated for deployment: {}", &domain_name);
19− let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &default_values, &envs);
20− let image_tag = match cli_opts.tag {
21− Some(tag) => tag,
15+
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ let namespace = String::from(&cli_opts.namespace);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
2220 None => {
23− error!("mandatory tag argument was not provided");
21+ error!("mandatory name argument was not provided");
2422 panic!();
2523 }
2624 };
27− let namespace = String::from(&cli_opts.namespace);
2825 let helm_template = HelmCmd {
2926 name: envs.helm_bin,
3027 mode: HelmMode::Template,
31− release_name: cli_opts.name,
32− release_domain: domain_name,
28+ release_name: release_name,
3329 namespace: cli_opts.namespace,
3430 deployment_parameters: deployment_parameters,
3531 overrides: overrides,
36− default_values: default_values.default_overrides,
37− image_tag: image_tag
3832 };
3933 info!("Generated Helm args: {:?}", &helm_template.args());
4034 match helm_template.run_stdout() {
4135 Ok(status) => {
42− let (deployments, statefulsets, _ingresses, _old_ingresses) = match parse_to_k8s(status) {
43− Ok((deployments, statefulsets, ingresses, old_ingresses)) => (deployments, statefulsets, ingresses, old_ingresses),
36+ let (deployments, statefulsets, _ingresses, _old_ingresses, _postgresqls, _kafkas) = match parse_to_k8s(status) {
37+ Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
4438 Err(err) => panic!("{}", err)
4539 };
4640 match check_all(deployments, statefulsets, namespace) {
4741 Ok(_status) => info!("Success!"),
4842 Err(status) => {
4943 error!("Error checking statuses");
50− panic!("{}", status);
44+ panic!("{:?}", status);
5145 }
5246 }
5347 }
helm-control-scripts/src/cleanup.rs+57−7
@@ -6,16 +6,66 @@ fn main() {
66 info!("Utils version {}", env!("CARGO_PKG_VERSION"));
77 let envs = EnvVars::parse();
88 info!("Env variables received {:?}", &envs);
9− let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
109 let cli_opts = CliOpts::from_args();
1110 info!("Cli options received {:?}", &cli_opts);
12− let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &default_values, &envs);
13− let kubectl_cmd = KubectlCmd {
14− name: envs.kubectl_bin,
15− release_name: cli_opts.name,
11+ let overrides = match overrides(&cli_opts, &envs) {
12+ Some(inner) => inner,
13+ None => vec![],
14+ };
15+
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ let namespace = String::from(&cli_opts.namespace);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
20+ None => {
21+ error!("mandatory name argument was not provided");
22+ panic!();
23+ }
24+ };
25+ helm_init(&envs, &deployment_parameters);
26+ let helm_cmd = HelmCmd {
27+ name: envs.helm_bin.clone(),
28+ mode: HelmMode::Uninstall,
29+ release_name: release_name.clone(),
30+ namespace: cli_opts.namespace.clone(),
31+ deployment_parameters: deployment_parameters.clone(),
32+ overrides: vec![],
33+ };
34+ let helm_template = HelmCmd {
35+ name: envs.helm_bin,
36+ mode: HelmMode::Template,
37+ release_name: release_name.clone(),
1638 namespace: cli_opts.namespace,
1739 deployment_parameters: deployment_parameters,
40+ overrides: overrides,
1841 };
19− kubectl_cmd.run();
20− info!("Success!");
42+ info!("Generated Helm args: {:?}", &helm_cmd.args());
43+ match helm_template.run_stdout() {
44+ Ok(status) => {
45+ let (_deployments, _statefulsets, ingresses, old_ingresses, _postgresqls, _kafkas) = match parse_to_k8s(status) {
46+ Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
47+ Err(err) => panic!("{}", err)
48+ };
49+ match helm_cmd.run() {
50+ Ok(_status) => {
51+ match delete_pvcs(&namespace, &format!("app.kubernetes.io/instance={}", release_name)) {
52+ Ok(_status) => info!("All pvcs were deleted successfully"),
53+ Err(err) => {
54+ error!("Error deleting pvcs");
55+ panic!("{:?}", err);
56+ }
57+ }
58+ info!("Success!");
59+ },
60+ Err(status) => {
61+ error!("Error during helm uninstall");
62+ panic!("{:?}", status);
63+ }
64+ }
65+ }
66+ Err(status) => {
67+ error!("Error during helm templating");
68+ panic!("{:?}", status);
69+ }
70+ }
2171}
helm-control-scripts/src/tag_check.rs → helm-control-scripts/src/config_check.rsrenamed+25−20
@@ -2,57 +2,62 @@ use helm_control_scripts::lib::*;
22
33fn main() {
44 let mut log_builder = Builder::from_default_env();
5− log_builder.target(Target::Stdout).filter(None, LevelFilter::Info).init();
5+ log_builder.target(Target::Stderr).filter(None, LevelFilter::Info).init();
66 info!("Utils version {}", env!("CARGO_PKG_VERSION"));
77 let envs = EnvVars::parse();
88 info!("Env variables received {:?}", &envs);
9− let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
109 let cli_opts = CliOpts::from_args();
1110 info!("Cli options received {:?}", &cli_opts);
12− let overrides = match overrides(&cli_opts) {
11+ let overrides = match overrides(&cli_opts, &envs) {
1312 Some(inner) => inner,
1413 None => vec![],
1514 };
1615
17− let domain_name = domain_name(&cli_opts);
18− info!("Domain generated for deployment: {}", &domain_name);
19− let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &default_values, &envs);
20− let image_tag = match cli_opts.tag {
21− Some(tag) => tag,
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ helm_init(&envs, &deployment_parameters);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
2220 None => {
23− error!("mandatory tag argument was not provided");
21+ error!("mandatory name argument was not provided");
2422 panic!();
2523 }
2624 };
27− helm_init(&envs, &deployment_parameters);
2825 let helm_template = HelmCmd {
2926 name: envs.helm_bin,
3027 mode: HelmMode::Template,
31− release_name: cli_opts.name,
32− release_domain: domain_name,
28+ release_name: release_name,
3329 namespace: cli_opts.namespace,
3430 deployment_parameters: deployment_parameters,
3531 overrides: overrides,
36− default_values: default_values.default_overrides,
37− image_tag: image_tag
3832 };
33+ info!("Generated Helm args: {:?}", &helm_template.args());
3934 match helm_template.run_stdout() {
4035 Ok(status) => {
41− let (deployments, statefulsets, _ingresses, _old_ingresses) = match parse_to_k8s(status) {
42− Ok((deployments, statefulsets, ingresses, old_ingresses)) => (deployments, statefulsets, ingresses, old_ingresses),
43− Err(err) => panic!("{}", err)
36+ let (deployments, statefulsets, _ingresses, _old_ingresses, _postgresqls, _kafkas) = match parse_to_k8s(status) {
37+ Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
38+ Err(err) => {
39+ println!("Something went wrong: {}", err);
40+ panic!("{}", err);
41+ }
4442 };
4543 match deployments_statefulsets_to_images(deployments, statefulsets) {
4644 Some(images) => {
4745 match check_images(images) {
4846 Ok(_) => info!("Success!"),
49− Err(err) => panic!("{}", err),
47+ Err(err) => {
48+ println!("Error checking images. Are they present in the registry?");
49+ panic!("{}", err);
50+ }
5051 }
5152 },
52− None => panic!("No images found!"),
53+ None => {
54+ println!("No images found in pods' declarations");
55+ panic!("No images found!");
56+ }
5357 }
5458 }
55− Err(status) => {
59+ Err((status, err)) => {
60+ println!("{:#}", err);
5661 error!("Error during helm execution");
5762 panic!("{:?}", status);
5863 }
helm-control-scripts/src/create_update.rs+8−13
@@ -6,34 +6,29 @@ fn main() {
66 info!("Utils version {}", env!("CARGO_PKG_VERSION"));
77 let envs = EnvVars::parse();
88 info!("Env variables received {:?}", &envs);
9− let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
109 let cli_opts = CliOpts::from_args();
1110 info!("Cli options received {:?}", &cli_opts);
12− let overrides = match overrides(&cli_opts) {
11+ let overrides = match overrides(&cli_opts, &envs) {
1312 Some(inner) => inner,
1413 None => vec![],
1514 };
16− let domain_name = domain_name(&cli_opts);
17− info!("Domain generated for deployment: {}", &domain_name);
18− let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &default_values, &envs);
19− let image_tag = match cli_opts.tag {
20− Some(tag) => tag,
15+
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ helm_init(&envs, &deployment_parameters);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
2120 None => {
22− error!("mandatory tag argument was not provided");
21+ error!("mandatory name argument was not provided");
2322 panic!();
2423 }
2524 };
26− helm_init(&envs, &deployment_parameters);
2725 let helm_cmd = HelmCmd {
2826 name: envs.helm_bin,
2927 mode: HelmMode::UpgradeInstall,
30− release_name: cli_opts.name,
31− release_domain: domain_name,
28+ release_name: release_name,
3229 namespace: cli_opts.namespace,
3330 deployment_parameters: deployment_parameters,
3431 overrides: overrides,
35− default_values: default_values.default_overrides,
36− image_tag: image_tag,
3732 };
3833 info!("Generated Helm args: {:?}", &helm_cmd.args());
3934 match helm_cmd.run() {
helm-control-scripts/src/deployment_keys.rsnew file+11
@@ -0,0 +1,11 @@
1+use helm_control_scripts::lib::*;
2+
3+fn main() {
4+ let mut log_builder = Builder::from_default_env();
5+ log_builder.target(Target::Stderr).filter(None, LevelFilter::Info).init();
6+ info!("Utils version {}", env!("CARGO_PKG_VERSION"));
7+ let envs = EnvVars::parse();
8+ info!("Env variables received {:?}", &envs);
9+ let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
10+ print!("{}", print_keys(Some(default_values.deployment_keys())));
11+}
helm-control-scripts/src/deployment_overrides.rsnew file+11
@@ -0,0 +1,11 @@
1+use helm_control_scripts::lib::*;
2+
3+fn main() {
4+ let mut log_builder = Builder::from_default_env();
5+ log_builder.target(Target::Stderr).filter(None, LevelFilter::Info).init();
6+ info!("Utils version {}", env!("CARGO_PKG_VERSION"));
7+ let envs = EnvVars::parse();
8+ info!("Env variables received {:?}", &envs);
9+ let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
10+ print!("{}", print_kv(Some(default_values.deployment_overrides())));
11+}
helm-control-scripts/src/info.rs+11−17
@@ -6,41 +6,35 @@ fn main() {
66 info!("Utils version {}", env!("CARGO_PKG_VERSION"));
77 let envs = EnvVars::parse();
88 info!("Env variables received {:?}", &envs);
9− let default_values: DefaultValues = serde_json::from_str(&envs.defaults).unwrap();
109 let cli_opts = CliOpts::from_args();
1110 info!("Cli options received {:?}", &cli_opts);
12− let overrides = match overrides(&cli_opts) {
11+ let overrides = match overrides(&cli_opts, &envs) {
1312 Some(inner) => inner,
1413 None => vec![],
1514 };
16−
17− let domain_name = domain_name(&cli_opts);
18− info!("Domain generated for deployment: {}", &domain_name);
19− let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &default_values, &envs);
20− let image_tag = match cli_opts.tag {
21− Some(tag) => tag,
15+
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ helm_init(&envs, &deployment_parameters);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
2220 None => {
23− error!("mandatory tag argument was not provided");
21+ error!("mandatory name argument was not provided");
2422 panic!();
2523 }
2624 };
27− let default_name = String::from(&cli_opts.name);
28− helm_init(&envs, &deployment_parameters);
25+ let default_name = String::from(&release_name);
2926 let helm_template = HelmCmd {
3027 name: envs.helm_bin,
3128 mode: HelmMode::Template,
32− release_name: cli_opts.name,
33− release_domain: domain_name,
29+ release_name: release_name,
3430 namespace: cli_opts.namespace,
3531 deployment_parameters: deployment_parameters,
3632 overrides: overrides,
37− default_values: default_values.default_overrides,
38− image_tag: image_tag
3933 };
4034 match helm_template.run_stdout() {
4135 Ok(status) => {
42− let (_deployments, _statefulsets, ingresses, old_ingresses) = match parse_to_k8s(status) {
43− Ok((deployments, statefulsets, ingresses, old_ingresses)) => (deployments, statefulsets, ingresses, old_ingresses),
36+ let (_deployments, _statefulsets, ingresses, old_ingresses, _postgresqls, _kafkas) = match parse_to_k8s(status) {
37+ Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
4438 Err(err) => panic!("{}", err)
4539 };
4640 print!("{}", print_kv(ingresses_to_hosts(ingresses, old_ingresses, default_name)));
helm-control-scripts/src/lib.rs+583−187
@@ -1,27 +1,35 @@
11pub mod lib {
22 pub use std::io::prelude::*;
33 pub use std::process::{Command, ExitStatus, Stdio};
4+ pub use std::error::Error;
5+ pub use std::fmt;
46 pub use structopt::StructOpt;
57 pub use env_logger::{Builder, Target};
68 pub use log::{LevelFilter, info, warn, error};
79 pub use serde::{Serialize, Deserialize};
10+ pub use serde_json::json;
811 pub use std::error;
9− pub use yaml_rust::{YamlLoader, YamlEmitter};
12+ pub use yaml_rust::{YamlLoader, YamlEmitter, Yaml};
1013 pub use k8s_openapi::api::{
1114 apps::v1::{Deployment, StatefulSet},
1215 networking::v1::Ingress,
13− networking::v1beta1::Ingress as OldIngress
16+ networking::v1beta1::Ingress as OldIngress,
17+ core::v1::{PersistentVolumeClaim, Secret}
1418 };
1519 pub use kube::{
16− api::{Api, ListParams, ResourceExt},
17− Client,
20+ api::{Api, ListParams, ResourceExt, Patch, PatchParams, DeleteParams, PostParams},
21+ Client,CustomResource
1822 };
1923 pub use dkregistry::{
2024 v2::Client as RegClient,
21− errors::Error
25+ errors::Error as RegError,
2226 };
2327 pub use regex::Regex;
24−
28+ pub use rusoto_core::{Region, RusotoError};
29+ pub use rusoto_ecr::{Ecr, EcrClient, DescribeImagesRequest, ImageIdentifier, DescribeImagesError};
30+ pub use schemars::JsonSchema;
31+ pub use tokio::task;
32+
2533 #[derive(Debug,StructOpt)]
2634 #[structopt(rename_all = "kebab-case")]
2735 pub struct CliOpts {
@@ -32,25 +40,25 @@ pub mod lib {
3240 #[structopt(long)]
3341 pub namespace: String,
3442 #[structopt(long)]
35− pub name: String,
43+ pub name: Option<String>,
3644 #[structopt(long)]
3745 pub tag: Option<String>,
3846 #[structopt(long)]
39− pub app_env_override: Vec<String>,
47+ pub application_config: Vec<String>,
4048 #[structopt(long)]
41− pub deployment_override: Vec<String>,
49+ pub deployment_config: Vec<String>,
4250 }
4351
4452 #[derive(Deserialize, Debug)]
4553 pub struct EnvVars {
4654 pub helm_bin: String,
47− pub kubectl_bin: String,
4855 pub defaults: String, //json of DefaultValues
4956 #[serde(default)]
5057 pub helm_user: String,
5158 #[serde(default)]
5259 pub helm_pass: String,
5360 pub helm_on_init_only: Option<bool>,
61+ pub ingress_host_key: Option<String>,
5462 }
5563
5664 impl EnvVars {
@@ -66,19 +74,56 @@ pub mod lib {
6674 }
6775 }
6876
77+ #[derive(Debug, Serialize, Deserialize)]
78+ struct HelmRepo {
79+ name: String,
80+ url: String
81+ }
82+
6983 #[derive(Deserialize, Debug)]
7084 pub struct DefaultValues {
7185 pub default_overrides: Vec<String>,
7286 pub chart_repo_url: String,
73− pub chart_repo_name: String,
7487 pub chart_version: String,
7588 pub chart_name: String,
7689 }
7790
78− #[derive(Debug, Clone)]
91+ impl DefaultValues {
92+ pub fn app_overrides(&self) -> Option<Vec<(String,String)>> {
93+ if self.default_overrides.is_empty() {
94+ None
95+ } else {
96+ let mut overrides: Vec<(String,String)> = Vec::new();
97+ for app_override in &self.default_overrides {
98+ let split_override = app_override.split('=').collect::<Vec<_>>();
99+ overrides.push((split_override.first().unwrap().to_string(), split_override.last().unwrap().to_string()));
100+ }
101+ Some(overrides)
102+ }
103+ }
104+ pub fn deployment_overrides(&self) -> Vec<(String,String)> {
105+ vec![
106+ (String::from("chart_repo_url"), self.chart_repo_url.clone()),
107+ (String::from("chart_version"), self.chart_version.clone()),
108+ (String::from("chart_name"), self.chart_name.clone()),
109+ ]
110+ }
111+ pub fn deployment_keys(&self) -> Vec<String> {
112+ let mut keys: Vec<String> = vec![
113+ String::from("chart_repo_url"),
114+ String::from("chart_version"),
115+ String::from("chart_name"),
116+ String::from("chart_repo_user"),
117+ String::from("chart_repo_pass"),
118+ ];
119+ keys.sort();
120+ keys
121+ }
122+ }
123+
124+ #[derive(Debug, Clone, Default)]
79125 pub struct HelmDeploymentParameters {
80126 pub chart_repo_url: String,
81− pub chart_repo_name: String,
82127 pub chart_repo_user: String,
83128 pub chart_repo_pass: String,
84129 pub chart_version: String,
@@ -86,35 +131,26 @@ pub mod lib {
86131 }
87132
88133 impl HelmDeploymentParameters {
89− pub fn new(cli_opts: &CliOpts, default_values: &DefaultValues, envs: &EnvVars) -> Self {
90− let mut default_parameters = Self {
91− chart_repo_url: default_values.chart_repo_url.clone(),
92− chart_repo_name: default_values.chart_repo_name.clone(),
93− chart_repo_user: envs.helm_user.clone(),
94− chart_repo_pass: envs.helm_pass.clone(),
95− chart_version: default_values.chart_version.clone(),
96− chart_name: default_values.chart_name.clone(),
97− };
98− if cli_opts.deployment_override.is_empty() {
99− return default_parameters;
100− } else {
101− for deployment_override in cli_opts.deployment_override.clone().into_iter() {
102− let split_override = deployment_override.split('=').collect::<Vec<_>>();
103− match split_override.first().unwrap().as_ref() {
104− "chart_repo_url" => default_parameters.chart_repo_url = split_override.last().unwrap().to_string(),
105− "chart_repo_name" => default_parameters.chart_repo_name = split_override.last().unwrap().to_string(),
106− "chart_version" => default_parameters.chart_version = split_override.last().unwrap().to_string(),
107− "chart_name" => default_parameters.chart_name = split_override.last().unwrap().to_string(),
108− _ => continue,
109− }
134+ pub fn new(cli_opts: &CliOpts, envs: &EnvVars) -> Self {
135+ let mut deployment_parameters = Self::default();
136+ deployment_parameters.chart_repo_user = envs.helm_user.clone();
137+ deployment_parameters.chart_repo_pass = envs.helm_pass.clone();
138+ for deployment_override in cli_opts.deployment_config.clone().into_iter() {
139+ let split_override = deployment_override.split('=').collect::<Vec<_>>();
140+ match split_override.first().unwrap().as_ref() {
141+ "chart_repo_url" => deployment_parameters.chart_repo_url = split_override.last().unwrap().to_string(),
142+ "chart_version" => deployment_parameters.chart_version = split_override.last().unwrap().to_string(),
143+ "chart_name" => deployment_parameters.chart_name = split_override.last().unwrap().to_string(),
144+ "chart_repo_user" => deployment_parameters.chart_repo_user = split_override.last().unwrap().to_string(),
145+ "chart_repo_pass" => deployment_parameters.chart_repo_pass = split_override.last().unwrap().to_string(),
146+ _ => continue,
110147 }
111− return default_parameters;
112148 }
149+ return deployment_parameters;
113150 }
114151 pub fn new_env_only(default_values: &DefaultValues, envs: &EnvVars) -> Self {
115152 Self {
116153 chart_repo_url: default_values.chart_repo_url.clone(),
117− chart_repo_name: default_values.chart_repo_name.clone(),
118154 chart_repo_user: envs.helm_user.clone(),
119155 chart_repo_pass: envs.helm_pass.clone(),
120156 chart_version: default_values.chart_version.clone(),
@@ -126,76 +162,78 @@ pub mod lib {
126162 pub name: String,
127163 pub mode: HelmMode,
128164 pub release_name: String,
129− pub release_domain: String,
130165 pub namespace: String,
131166 pub deployment_parameters: HelmDeploymentParameters,
132167 pub overrides: Vec<String>,
133− pub default_values: Vec<String>,
134− pub image_tag: String,
135168 }
136169
137170 impl HelmCmd {
138171 pub fn args(&self) -> Vec<String> {
139− let mut args: Vec<String> = Vec::new();
140− let chart_location = format!("{}/{}", self.deployment_parameters.chart_repo_name, self.deployment_parameters.chart_name);
141− let chart_version = vec![String::from("--version"), self.deployment_parameters.chart_version.clone()];
142− let namespace = vec![String::from("-n"), String::from(&self.namespace),];
143− let common_args = vec![
144− String::from("--debug"),
145− ];
146− match self.mode {
147− HelmMode::Uninstall => {
148− args.extend(namespace);
149− args.push(String::from("uninstall"));
150− args.push(String::from(&self.release_name));
151− },
152− HelmMode::UpgradeInstall => {
153− args.extend(namespace);
154− args.push(String::from("upgrade"));
155− args.push(String::from("-i"));
156− args.push(String::from(&self.release_name));
157− args.push(chart_location);
158− args.extend(chart_version);
159− args.extend(self.set_flag_values());
160− args.extend(common_args);
161− },
162− HelmMode::Template => {
163− args.extend(namespace);
164− args.push(String::from("template"));
165− args.push(String::from(&self.release_name));
166− args.push(chart_location);
167− args.extend(chart_version);
168− args.extend(self.set_flag_values());
169− },
170− HelmMode::RepoAdd => {
171− args.push(String::from("repo"));
172− args.push(String::from("add"));
173− args.push(String::from("--force-update"));
174− args.push(String::from(&self.deployment_parameters.chart_repo_name));
175− args.push(String::from(&self.deployment_parameters.chart_repo_url));
176− args.extend(vec![String::from("--username"), String::from(&self.deployment_parameters.chart_repo_user)]);
177− args.extend(vec![String::from("--password"), String::from(&self.deployment_parameters.chart_repo_pass)]);
178− },
179− HelmMode::RepoUpdate => {
180− args.push(String::from("repo"));
181− args.push(String::from("update"));
182− },
183− HelmMode::Status => {
184− args.extend(namespace);
185− args.push(String::from("status"));
186− args.push(String::from(&self.release_name));
187− },
188− _ => panic!("This helm mode is not expected"),
189− }
172+ let mut args: Vec<String> = Vec::new();
173+ let chart_repo_name = url_to_name(&self.deployment_parameters.chart_repo_url);
174+ let chart_location = format!("{}/{}", chart_repo_name, self.deployment_parameters.chart_name);
175+ let chart_version = vec![String::from("--version"), self.deployment_parameters.chart_version.clone()];
176+ let namespace = vec![String::from("-n"), String::from(&self.namespace),];
177+ let common_args = vec![
178+ String::from("--debug"),
179+ ];
180+ match self.mode {
181+ HelmMode::Uninstall => {
182+ args.extend(namespace);
183+ args.push(String::from("uninstall"));
184+ args.push(String::from(&self.release_name));
185+ },
186+ HelmMode::UpgradeInstall => {
187+ args.extend(namespace);
188+ args.push(String::from("upgrade"));
189+ args.push(String::from("-i"));
190+ args.push(String::from(&self.release_name));
191+ args.push(chart_location);
192+ args.extend(chart_version);
193+ args.extend(self.set_flag_values());
194+ args.extend(common_args);
195+ },
196+ HelmMode::Template => {
197+ args.extend(namespace);
198+ args.push(String::from("template"));
199+ args.push(String::from(&self.release_name));
200+ args.push(chart_location);
201+ args.extend(chart_version);
202+ args.extend(self.set_flag_values());
203+ },
204+ HelmMode::ShowValues => {
205+ args.extend(namespace);
206+ args.push(String::from("show"));
207+ args.push(String::from("values"));
208+ args.push(chart_location);
209+ args.extend(chart_version);
210+ },
211+ HelmMode::RepoAdd => {
212+ args.push(String::from("repo"));
213+ args.push(String::from("add"));
214+ args.push(String::from("--force-update"));
215+ args.push(String::from(&chart_repo_name));
216+ args.push(String::from(&self.deployment_parameters.chart_repo_url));
217+ args.extend(vec![String::from("--username"), String::from(&self.deployment_parameters.chart_repo_user)]);
218+ args.extend(vec![String::from("--password"), String::from(&self.deployment_parameters.chart_repo_pass)]);
219+ },
220+ HelmMode::RepoUpdate => {
221+ args.push(String::from("repo"));
222+ args.push(String::from("update"));
223+ },
224+ HelmMode::RepoList => {
225+ args.push(String::from("repo"));
226+ args.push(String::from("list"));
227+ args.push(String::from("-o"));
228+ args.push(String::from("json"));
229+ },
230+ }
190231
191− return args;
232+ return args;
192233 }
193234 fn set_flag_values(&self) -> Vec<String> {
194235 let mut values = Vec::new();
195236 let mut set_values = Vec::new();
196− values.push(format!("image.tag={}", &self.image_tag));
197− values.push(format!("ingress.host={}", &self.release_domain));
198− values.extend(self.default_values.clone());
199237 values.extend(self.overrides.clone());
200238 for value in values.into_iter() {
201239 set_values.push(String::from("--set"));
@@ -216,7 +254,7 @@ pub mod lib {
216254 Err(helm.status)
217255 }
218256 }
219− pub fn run_stdout(&self) -> Result<String, ExitStatus> {
257+ pub fn run_stdout(&self) -> Result<String, (ExitStatus, String)> {
220258 let helm = Command::new(&self.name)
221259 .args(&self.args())
222260 .output()
@@ -224,9 +262,11 @@ pub mod lib {
224262 if helm.status.success() {
225263 Ok(String::from_utf8(helm.stdout).unwrap())
226264 } else {
227− info!("helm stdout:\n {}", String::from_utf8(helm.stdout).unwrap());
228− info!("helm stderr:\n {}", String::from_utf8(helm.stderr).unwrap());
229− Err(helm.status)
265+ let stdout = String::from_utf8(helm.stdout).unwrap();
266+ let stderr = String::from_utf8(helm.stderr).unwrap();
267+ info!("helm stdout:\n {}", stdout);
268+ info!("helm stderr:\n {}", stderr);
269+ Err((helm.status, stderr))
230270 }
231271 }
232272 }
@@ -236,71 +276,43 @@ pub mod lib {
236276 Template,
237277 RepoAdd,
238278 RepoUpdate,
239− Status,
279+ RepoList,
280+ ShowValues,
240281 }
241−
242− pub struct KubectlCmd {
243− pub name: String,
244− pub release_name: String,
245− pub namespace: String,
246− pub deployment_parameters: HelmDeploymentParameters,
282+ #[derive(CustomResource, Debug, Serialize, Deserialize, Default, Clone, JsonSchema)]
283+ #[kube(group = "acid.zalan.do", version = "v1", kind = "Postgresql", namespaced)]
284+ pub struct PosgresqlSpec {
285+ numberOfInstances: i32
247286 }
248−
249− impl KubectlCmd {
250− pub fn args(&self, cmd_type: &str) -> Vec<String> {
251− let mut args: Vec<String> = Vec::new();
252− let namespace = vec![String::from("-n"), String::from(&self.namespace),];
253− let cert_secret_name = format!("{}-{}-tls", &self.release_name, &self.deployment_parameters.chart_name);
254− let label = vec![String::from("-l"), format!("app.kubernetes.io/instance={}", &self.release_name)];
255− args.extend(namespace);
256− args.push(String::from("delete"));
257− match cmd_type {
258− "pvc" => {
259− args.push(String::from("pvc"));
260− args.extend(label);
261− return args;
262− },
263− "cert" => {
264− args.push(String::from("secret"));
265− args.push(cert_secret_name);
266− return args;
267− },
268− _ => unreachable!(),
269− }
270− }
271− pub fn run(&self) {
272− info!("Starting pvc cleanup");
273− info!("kubectl args: {:?}", &self.args("pvc"));
274− let kubectl_pvc = Command::new(&self.name)
275− .args(&self.args("pvc"))
276− .output()
277− .expect("Failed to run");
278− info!("kubectl stdout:\n {}", String::from_utf8(kubectl_pvc.stdout).unwrap());
279− info!("kubectl stderr:\n {}", String::from_utf8(kubectl_pvc.stderr).unwrap());
280− info!("Starting certificates cleanup");
281− info!("kubectl args: {:?}", &self.args("pvc"));
282− let kubectl_cert = Command::new(&self.name)
283− .args(&self.args("cert"))
284− .output()
285− .expect("Failed to run");
286− info!("kubectl stdout:\n {}", String::from_utf8(kubectl_cert.stdout).unwrap());
287− info!("kubectl stderr:\n {}", String::from_utf8(kubectl_cert.stderr).unwrap());
288− assert!(kubectl_pvc.status.success());
289− assert!(kubectl_cert.status.success());
290− }
287+ #[derive(CustomResource, Debug, Serialize, Deserialize, Default, Clone, JsonSchema)]
288+ #[kube(group = "kafka.strimzi.io", version = "v1beta2", kind = "Kafka", namespaced)]
289+ pub struct KafkaSpec {
290+ kafka: KafkaConfig,
291+ zookeeper: ZookeeperConfig
291292 }
292−
293+ #[derive(Debug, Serialize, Deserialize, Default, Clone, JsonSchema)]
294+ struct KafkaConfig {
295+ replicas: i32
296+ }
297+ #[derive(Debug, Serialize, Deserialize, Default, Clone, JsonSchema)]
298+ struct ZookeeperConfig {
299+ replicas: i32
300+ }
293301 fn check_value(value: String) -> Result<String, String> {
294− let split_value = value.split('=').collect::<Vec<_>>();
295− if split_value.len() == 2 {
296− Ok(value)
297− } else {
298− Err(format!("Override value {} is malformed", value))
302+ let re = Regex::new(r"^([^=]*)=(.*)$").unwrap();
303+ match re.captures(&value) {
304+ Some(_) => return Ok(value),
305+ None => return Err(format!("Override value {} is malformed", value)),
299306 }
300307 }
301− pub fn overrides(cli_opts: &CliOpts) -> Option<Vec<String>> {
308+ pub fn overrides(cli_opts: &CliOpts, envs: &EnvVars) -> Option<Vec<String>> {
302309 let mut overrides_opts = Vec::new();
303− overrides_opts.extend(&cli_opts.app_env_override);
310+ overrides_opts.extend(&cli_opts.application_config);
311+ let ingress_override = match &envs.ingress_host_key {
312+ Some(key) => format!("{}={}", &key, domain_name(&cli_opts)),
313+ None => format!("ingress.hostname={}", domain_name(&cli_opts)),
314+ };
315+ overrides_opts.push(&ingress_override);
304316 if overrides_opts.is_empty() {
305317 None
306318 } else {
@@ -311,15 +323,22 @@ pub mod lib {
311323 )
312324 }
313325 }
314− pub fn domain_name(cli_opts: &CliOpts) -> String {
315− format!("{}.{}", &cli_opts.name, &cli_opts.base_domain)
326+ fn domain_name(cli_opts: &CliOpts) -> String {
327+ match &cli_opts.name {
328+ Some(name) => {
329+ return format!("{}.{}", name, &cli_opts.base_domain);
330+ },
331+ None => panic!("No name argument provided")
332+ }
316333 }
317− pub fn parse_to_k8s(yaml: String) -> Result<(Vec<Deployment>, Vec<StatefulSet>, Vec<Ingress>, Vec<OldIngress>), serde_yaml::Error> {
334+ pub fn parse_to_k8s(yaml: String) -> Result<(Vec<Deployment>, Vec<StatefulSet>, Vec<Ingress>, Vec<OldIngress>, Vec<Postgresql>, Vec<Kafka>), serde_yaml::Error> {
318335 let docs = YamlLoader::load_from_str(&yaml).unwrap();
319336 let mut deployments: Vec<Deployment> = Vec::new();
320337 let mut statefulsets: Vec<StatefulSet> = Vec::new();
321338 let mut ingresses: Vec<Ingress> = Vec::new();
322339 let mut old_ingresses: Vec<OldIngress> = Vec::new();
340+ let mut postgresqls: Vec<Postgresql> = Vec::new();
341+ let mut kafkas: Vec<Kafka> = Vec::new();
323342 for doc in docs {
324343 match doc["kind"].as_str().unwrap() {
325344 "Deployment" => {
@@ -348,16 +367,59 @@ pub mod lib {
348367 ingresses.push(ingress);
349368 }
350369 },
370+ "postgresql" => {
371+ let mut postgresql_str = String::new();
372+ let mut emitter = YamlEmitter::new(&mut postgresql_str);
373+ emitter.dump(&doc).unwrap();
374+ let postgresql: Postgresql = serde_yaml::from_str(&postgresql_str)?;
375+ postgresqls.push(postgresql);
376+ },
377+ "Kafka" => {
378+ let mut kafka_str = String::new();
379+ let mut emitter = YamlEmitter::new(&mut kafka_str);
380+ emitter.dump(&doc).unwrap();
381+ let kafka: Kafka = serde_yaml::from_str(&kafka_str)?;
382+ kafkas.push(kafka);
383+ },
351384 _ => continue,
352385 }
353386 }
354387 if deployments.is_empty() && statefulsets.is_empty() {
355388 panic!("Got no deployment or stateful sets. Nothing to check");
356389 }
357− return Ok((deployments, statefulsets, ingresses, old_ingresses));
390+ return Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas));
358391 }
392+
393+ #[derive(Debug)]
394+ pub struct NoReplicasError(String);
395+
396+ impl fmt::Display for NoReplicasError {
397+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
398+ write!(f, "Deployment {} doesn't have any replicas", self.0)
399+ }
400+ }
401+ impl Error for NoReplicasError {}
402+
403+ #[derive(Debug)]
404+ pub enum KubeError {
405+ NoReplicasError(NoReplicasError),
406+ KubeApiError(kube::Error),
407+ }
408+
409+ impl From<NoReplicasError> for KubeError {
410+ fn from(error: NoReplicasError) -> Self {
411+ KubeError::NoReplicasError(error)
412+ }
413+ }
414+
415+ impl From<kube::Error> for KubeError {
416+ fn from(error: kube::Error) -> Self {
417+ KubeError::KubeApiError(error)
418+ }
419+ }
420+
359421 #[tokio::main]
360− async fn check_deployment(namespace: &str, name: &str) -> Result<(), kube::Error> {
422+ async fn check_deployment(namespace: &str, name: &str) -> Result<(), KubeError> {
361423 let client = Client::try_default().await?;
362424 let api: Api<Deployment> = Api::namespaced(client, &namespace);
363425 let deployment = api.get(&name).await?;
@@ -366,22 +428,22 @@ pub mod lib {
366428 match status.available_replicas {
367429 Some(replicas) => {
368430 if !replicas >= 1 {
369− panic!("Deployment {} doesn't have any replicas", &name);
431+ return Err(KubeError::NoReplicasError(NoReplicasError(name.to_string())));
370432 }
371433 },
372434 None => {
373− panic!("Deployment {} doesn't have any replicas", &name);
435+ return Err(KubeError::NoReplicasError(NoReplicasError(name.to_string())));
374436 }
375437 }
376438 },
377439 None => {
378− panic!("Unable to get status for deployment {}", &name);
440+ return Err(KubeError::NoReplicasError(NoReplicasError(name.to_string())));
379441 }
380442 }
381443 Ok(())
382444 }
383445 #[tokio::main]
384− async fn check_statefulset(namespace: &str, name: &str) -> Result<(), kube::Error> {
446+ async fn check_statefulset(namespace: &str, name: &str) -> Result<(), KubeError> {
385447 let client = Client::try_default().await?;
386448 let api: Api<StatefulSet> = Api::namespaced(client, &namespace);
387449 let statefulset = api.get(&name).await?;
@@ -390,21 +452,21 @@ pub mod lib {
390452 match status.current_replicas {
391453 Some(replicas) => {
392454 if !replicas >= 1 {
393− panic!("StatefulSet {} doesn't have any replicas", &name);
455+ return Err(KubeError::NoReplicasError(NoReplicasError(name.to_string())));
394456 }
395457 },
396458 None => {
397− panic!("StatefulSet {} doesn't have any replicas", &name);
459+ return Err(KubeError::NoReplicasError(NoReplicasError(name.to_string())));
398460 }
399461 }
400462 },
401463 None => {
402− panic!("Unable to get status for StatefulSet {}", &name);
464+ return Err(KubeError::NoReplicasError(NoReplicasError(name.to_string())));
403465 }
404466 }
405467 Ok(())
406468 }
407− pub fn check_all(deployments: Vec<Deployment>, statefulsets: Vec<StatefulSet>, namespace: String) -> Result<(), Box<dyn error::Error>> {
469+ pub fn check_all(deployments: Vec<Deployment>, statefulsets: Vec<StatefulSet>, namespace: String) -> Result<(), KubeError> {
408470 if deployments.is_empty() {
409471 info!("No deployments to check");
410472 }else{
@@ -412,7 +474,7 @@ pub mod lib {
412474 match check_deployment(&namespace, &deploy.name()) {
413475 Ok(status) => status,
414476 Err(err) => {
415− return Err(Box::new(err));
477+ return Err(err);
416478 }
417479 }
418480 }
@@ -425,7 +487,7 @@ pub mod lib {
425487 match check_statefulset(&namespace, &statefulset.name()) {
426488 Ok(status) => status,
427489 Err(err) => {
428− return Err(Box::new(err));
490+ return Err(err);
429491 }
430492 }
431493 }
@@ -506,6 +568,18 @@ pub mod lib {
506568 }
507569 return out_string;
508570 }
571+ pub fn print_keys(vals: Option<Vec<String>>) -> String {
572+ let mut out_string = String::from("");
573+ match vals {
574+ Some(values) => {
575+ for value in values {
576+ out_string.push_str(&format!("{}\n", value));
577+ }
578+ },
579+ None => return out_string
580+ }
581+ return out_string;
582+ }
509583 //TODO: Generics!!!
510584 pub fn deployments_statefulsets_to_images(deployments: Vec<Deployment>, statefulsets: Vec<StatefulSet>) -> Option<Vec<String>> {
511585 let mut images: Vec<String> = Vec::new();
@@ -564,7 +638,7 @@ pub mod lib {
564638 }
565639 }
566640 #[tokio::main]
567− async fn check_image(registry: &str, repository: &str, tag: &str) -> Result<(), dkregistry::errors::Error> {
641+ async fn check_image(registry: &str, repository: &str, tag: &str) -> Result<(), RegError> {
568642 let client = RegClient::configure()
569643 .insecure_registry(false)
570644 .registry(registry)
@@ -575,6 +649,30 @@ pub mod lib {
575649 None => panic!("Image not found: {} {} {}", registry, repository, tag),
576650 }
577651 }
652+ #[tokio::main]
653+ pub async fn check_ecr_image(registry: &str, repository: &str, tag: &str) -> Result<(), DescribeImagesError>{
654+ let re = Regex::new(r"^[0-9]*.dkr.ecr.(.*).amazonaws.com$").unwrap();
655+ let captures = re.captures(&registry).unwrap();
656+ let region_string = String::from(&captures[1]);
657+ let region: Region = region_string.parse().unwrap();
658+ let client = EcrClient::new(region);
659+ let image_id = ImageIdentifier {
660+ image_tag: Some(String::from(tag)),
661+ image_digest: None,
662+ };
663+ let describe_request = DescribeImagesRequest {
664+ repository_name: String::from(repository),
665+ image_ids: Some(vec![image_id]),
666+ filter: None,
667+ max_results: None,
668+ next_token: None,
669+ registry_id: None,
670+ };
671+ match client.describe_images(describe_request).await {
672+ Ok(_) => return Ok(()),
673+ Err(err) => panic!("Image not found in ECR: {} {} {}\n {}", registry, repository, tag, err),
674+ }
675+ }
578676 fn parse_image_name(image_name: &str) -> (String, String, String) {
579677 let mut registry = String::new();
580678 let mut repository = String::new();
@@ -585,8 +683,8 @@ pub mod lib {
585683 registry = String::from("registry-1.docker.io");
586684 repository = format!("library/{}", &captures[1])
587685 } else if &captures[2] == "" {
588− registry = String::from("registry-1.docker.io");
589− repository = format!("{}/{}", &captures[1], &captures[3]);
686+ registry = String::from(&captures[1]);
687+ repository = String::from(&captures[3]);
590688 } else {
591689 if &captures[1] == "docker.io" {
592690 registry = String::from("registry-1.docker.io");
@@ -598,12 +696,19 @@ pub mod lib {
598696 info!("Checking image: {} {} {}", registry, repository, tag);
599697 (registry, repository, tag)
600698 }
601− pub fn check_images(images: Vec<String>) -> Result<(), dkregistry::errors::Error> {
699+ pub fn check_images(images: Vec<String>) -> Result<(), Box<dyn error::Error>> {
602700 for image in images {
603701 let (registry, repository, tag) = parse_image_name(&image);
604− match check_image(&registry, &repository, &tag) {
605− Ok(_) => (),
606− Err(err) => return Err(err),
702+ if registry.contains("dkr.ecr") {
703+ match check_ecr_image(&registry, &repository, &tag) {
704+ Ok(_) => (),
705+ Err(err) => return Err(Box::new(err)),
706+ }
707+ } else {
708+ match check_image(&registry, &repository, &tag) {
709+ Ok(_) => (),
710+ Err(err) => return Err(Box::new(err)),
711+ }
607712 }
608713 }
609714 Ok(())
@@ -613,27 +718,22 @@ pub mod lib {
613718 name: String::from(&envs.helm_bin),
614719 mode: HelmMode::RepoAdd,
615720 release_name: String::from(""),
616− release_domain: String::from(""),
617721 namespace: String::from(""),
618722 deployment_parameters: deployment_parameters.clone(),
619723 overrides: vec![],
620− default_values: vec![],
621− image_tag: String::from(""),
622724 };
623725 let helm_repo_update = HelmCmd {
624726 name: String::from(&envs.helm_bin),
625727 mode: HelmMode::RepoUpdate,
626728 release_name: String::from(""),
627− release_domain: String::from(""),
628729 namespace: String::from(""),
629730 deployment_parameters: deployment_parameters.clone(),
630731 overrides: vec![],
631− default_values: vec![],
632− image_tag: String::from(""),
633732 };
634733 match helm_repo_add.run() {
635734 Ok(_status) => info!("Repo add success!"),
636735 Err(status) => {
736+ println!("Couldn't add repo.");
637737 error!("Error during helm execution");
638738 panic!("{:?}", status);
639739 }
@@ -641,6 +741,7 @@ pub mod lib {
641741 match helm_repo_update.run() {
642742 Ok(_status) => info!("Repo update success!"),
643743 Err(status) => {
744+ println!("Couldn't update repo.");
644745 error!("Error during helm execution");
645746 panic!("{:?}", status);
646747 }
@@ -651,15 +752,310 @@ pub mod lib {
651752 Some(enabled) => {
652753 if *enabled {
653754 info!("Skipping helm initialization, it must be initialied on init");
755+ } else {
756+ if helm_repo_exists(&envs, &deployment_parameters) {
757+ info!("Skipping helm initialization, because requested repo was already added");
758+ } else {
759+ info!("Starting helm initialization");
760+ helm_repo_add_update(&envs, &deployment_parameters);
761+ }
762+ }
763+ },
764+ None => {
765+ if helm_repo_exists(&envs, &deployment_parameters) {
766+ info!("Skipping helm initialization, because requested repo was already added");
654767 } else {
655768 info!("Starting helm initialization");
656769 helm_repo_add_update(&envs, &deployment_parameters);
657770 }
771+ }
772+ }
773+ }
774+
775+ fn helm_repo_exists(envs: &EnvVars, deployment_parameters: &HelmDeploymentParameters) -> bool {
776+ let chart_repo_url = String::from(&deployment_parameters.chart_repo_url);
777+ let chart_repo_name = url_to_name(&deployment_parameters.chart_repo_url);
778+ let helm_repo_list = HelmCmd {
779+ name: String::from(&envs.helm_bin),
780+ mode: HelmMode::RepoList,
781+ release_name: String::from(""),
782+ namespace: String::from(""),
783+ deployment_parameters: deployment_parameters.clone(),
784+ overrides: vec![],
785+ };
786+ match helm_repo_list.run_stdout() {
787+ Ok(list) => {
788+ let helm_repos: Vec<HelmRepo> = serde_json::from_str(&list).unwrap();
789+ for repo in helm_repos {
790+ if repo.name == chart_repo_name && repo.url == chart_repo_url {
791+ return true;
792+ } else {
793+ continue
794+ }
795+ }
658796 },
659− None => {
660− info!("Starting helm initialization");
661− helm_repo_add_update(&envs, &deployment_parameters);
797+ Err(_err) => {
798+ return false;
799+ }
800+ }
801+ false
802+ }
803+ pub fn helm_values_as_keys(yaml: String) -> Option<Vec<String>> {
804+ let mut keys: Vec<String> = Vec::new();
805+ let docs = YamlLoader::load_from_str(&yaml).unwrap();
806+ let docs_hash = &docs[0].clone().into_hash().unwrap();
807+ for doc in docs_hash {
808+ match doc.1.as_hash() {
809+ Some(hash) => {
810+ if hash.is_empty() {
811+ keys.push(format!("{}", doc.0.as_str().unwrap()));
812+ }else{
813+ for doc_2 in hash {
814+ match doc_2.1.as_hash() {
815+ Some(hash_2) => {
816+ if hash_2.is_empty() {
817+ keys.push(format!("{}.{}", doc.0.as_str().unwrap(), doc_2.0.as_str().unwrap()));
818+ }else{
819+ for key in hash_2.keys() {
820+ keys.push(format!("{}.{}.{}", doc.0.as_str().unwrap(), doc_2.0.as_str().unwrap(), key.as_str().unwrap()));
821+ }
822+ }
823+ },
824+ None => keys.push(format!("{}.{}", doc.0.as_str().unwrap(), doc_2.0.as_str().unwrap()))
825+ }
826+ }
827+ }
828+ },
829+ None => keys.push(format!("{}", doc.0.as_str().unwrap())),
830+ }
831+ }
832+ if keys.is_empty() {
833+ None
834+ }else{
835+ keys.sort();
836+ Some(keys)
837+ }
838+ }
839+ #[tokio::main]
840+ async fn scale_deployment(namespace: &str, name: &str, replicas: i32) -> Result<(), kube::Error> {
841+ let client = Client::try_default().await?;
842+ let api: Api<Deployment> = Api::namespaced(client, &namespace);
843+ let deployment = api.get(&name).await?;
844+ let mut spec = match deployment.spec {
845+ Some(spec) => spec,
846+ None => panic!("Deployment without any spec!")
847+ };
848+ spec.replicas = Some(replicas);
849+ let patched_deployment = Deployment {
850+ metadata: deployment.metadata,
851+ spec: Some(spec),
852+ status: None
853+ };
854+ let patch = Patch::Merge(patched_deployment);
855+ let patch_params = PatchParams::apply("octopod");
856+ api.patch(&name, &patch_params, &patch).await?;
857+ Ok(())
858+ }
859+ #[tokio::main]
860+ async fn scale_statefulset(namespace: &str, name: &str, replicas: i32) -> Result<(), kube::Error> {
861+ let client = Client::try_default().await?;
862+ let api: Api<StatefulSet> = Api::namespaced(client, &namespace);
863+ let statefulset = api.get(&name).await?;
864+ let mut spec = match statefulset.spec {
865+ Some(spec) => spec,
866+ None => panic!("StatefulSet without any spec!")
867+ };
868+ spec.replicas = Some(replicas);
869+ let patched_statefulset = StatefulSet {
870+ metadata: statefulset.metadata,
871+ spec: Some(spec),
872+ status: None
873+ };
874+ let patch = Patch::Merge(patched_statefulset);
875+ let patch_params = PatchParams::apply("octopod");
876+ api.patch(&name, &patch_params, &patch).await?;
877+ Ok(())
878+ }
879+ #[tokio::main]
880+ async fn scale_postgresql(namespace: &str, name: &str, replicas: i32) -> Result<(), kube::Error> {
881+ let client = Client::try_default().await?;
882+ let api: Api<Postgresql> = Api::namespaced(client, &namespace);
883+ let postgres = api.get(&name).await?;
884+ println!("{:#?}", postgres);
885+ let mut spec = postgres.spec;
886+ spec.numberOfInstances = replicas;
887+ let patched_postgres = Postgresql {
888+ api_version: postgres.api_version,
889+ kind: postgres.kind,
890+ metadata: postgres.metadata,
891+ spec: spec,
892+ };
893+ let patch = Patch::Merge(patched_postgres);
894+ let patch_params = PatchParams::apply("octopod");
895+ api.patch(&name, &patch_params, &patch).await?;
896+ Ok(())
897+ }
898+ #[tokio::main]
899+ async fn scale_kafka(namespace: &str, name: &str, replicas: i32) -> Result<(), kube::Error> {
900+ let client = Client::try_default().await?;
901+ let api: Api<Kafka> = Api::namespaced(client.clone(), &namespace);
902+ let kafka = api.get(&name).await?;
903+ let metadata = kafka.metadata;
904+ if replicas > 0 {
905+ if metadata.annotations.contains_key("strimzi.io/pause-reconciliation") {
906+ println!("deleting pause-reconciliation annotation");
907+ let remove_op = json_patch::RemoveOperation {
908+ path: String::from("/metadata/annotations/strimzi.io~1pause-reconciliation")
909+ };
910+ let patch_op = json_patch::PatchOperation::Remove(remove_op);
911+ let json_patch = json_patch::Patch(vec![patch_op]);
912+ let patch = Patch::Json::<()>(json_patch);
913+ let patch_params = PatchParams::apply("octopod");
914+ api.patch(&name, &patch_params, &patch).await?;
915+ }
916+ } else {
917+ if !metadata.annotations.contains_key("strimzi.io/pause-reconciliation") {
918+ println!("adding pause-reconciliation annotation");
919+ let add_op = json_patch::AddOperation {
920+ path: String::from("/metadata/annotations/strimzi.io~1pause-reconciliation"),
921+ value: serde_json::value::Value::String("true".to_string())
922+ };
923+ let patch_op = json_patch::PatchOperation::Add(add_op);
924+ let json_patch = json_patch::Patch(vec![patch_op]);
925+ let patch = Patch::Json::<()>(json_patch);
926+ let patch_params = PatchParams::apply("octopod");
927+ api.patch(&name, &patch_params, &patch).await?;
928+ }
929+ }
930+
931+ let kafka_sts: Api<StatefulSet> = Api::namespaced(client, &namespace);
932+ let sts_list = ListParams::default().labels(&format!("strimzi.io/cluster={}", name));
933+ for sts in kafka_sts.list(&sts_list).await? {
934+ let sts_namespace = String::from(namespace);
935+ let sts_replicas = replicas.clone();
936+ task::spawn_blocking(move || {
937+ scale_statefulset(&sts_namespace, &sts.name(), sts_replicas);
938+ }).await.expect("Failed to run thread")
939+ }
940+ Ok(())
941+ }
942+ pub fn scale(deployments: Vec<Deployment>, statefulsets: Vec<StatefulSet>, postgresqls: Vec<Postgresql>, kafkas: Vec<Kafka>, namespace: String, replicas: i32) -> Result<(), Box<dyn error::Error>> {
943+ if deployments.is_empty() {
944+ info!("No deployments to check");
945+ }else{
946+ for deploy in deployments {
947+ match scale_deployment(&namespace, &deploy.name(), replicas) {
948+ Ok(status) => status,
949+ Err(err) => {
950+ return Err(Box::new(err));
951+ }
952+ }
953+ }
954+ }
955+
956+ if statefulsets.is_empty() {
957+ info!("No statefulsets to check");
958+ }else{
959+ for statefulset in statefulsets {
960+ match scale_statefulset(&namespace, &statefulset.name(), replicas) {
961+ Ok(status) => status,
962+ Err(err) => {
963+ return Err(Box::new(err));
964+ }
965+ }
662966 }
663967 }
968+
969+ if postgresqls.is_empty() {
970+ info!("No postgresqls to check");
971+ }else{
972+ for postgres in postgresqls {
973+ match scale_postgresql(&namespace, &postgres.name(), replicas) {
974+ Ok(status) => status,
975+ Err(err) => {
976+ return Err(Box::new(err));
977+ }
978+ }
979+ }
980+ }
981+
982+ if kafkas.is_empty() {
983+ info!("No kafkas to check");
984+ }else{
985+ for kafka in kafkas {
986+ match scale_kafka(&namespace, &kafka.name(), replicas) {
987+ Ok(status) => status,
988+ Err(err) => {
989+ return Err(Box::new(err));
990+ }
991+ }
992+ }
993+ }
994+ Ok(())
995+ }
996+ pub fn ingresses_to_secrets(new_ingresses: Vec<Ingress>, old_ingresses: Vec<OldIngress>) -> Option<Vec<String>> {
997+ let mut secrets: Vec<String> = Vec::new();
998+ if new_ingresses.is_empty() {
999+ for ingress in old_ingresses {
1000+ match &ingress.spec {
1001+ Some(spec) => {
1002+ if spec.tls.is_empty() {
1003+ continue
1004+ }else{
1005+ for tls in &spec.tls {
1006+ match &tls.secret_name {
1007+ Some(secret) => secrets.push(String::from(secret)),
1008+ None => continue
1009+ }
1010+ }
1011+ }
1012+ },
1013+ None => continue
1014+ }
1015+ }
1016+ }else{
1017+ for ingress in new_ingresses {
1018+ match &ingress.spec {
1019+ Some(spec) => {
1020+ if spec.tls.is_empty() {
1021+ continue
1022+ }else{
1023+ for tls in &spec.tls {
1024+ match &tls.secret_name {
1025+ Some(secret) => secrets.push(String::from(secret)),
1026+ None => continue
1027+ }
1028+ }
1029+ }
1030+ },
1031+ None => continue
1032+ }
1033+ }
1034+ }
1035+ if secrets.is_empty() {
1036+ None
1037+ }else{
1038+ Some(secrets)
1039+ }
1040+ }
1041+ #[tokio::main]
1042+ pub async fn delete_pvcs(namespace: &str, selector: &str) -> Result<(), kube::Error> {
1043+ let client = Client::try_default().await?;
1044+ let api: Api<PersistentVolumeClaim> = Api::namespaced(client, &namespace);
1045+ let pvc_list = ListParams::default().labels(selector);
1046+ api.delete_collection(&DeleteParams::default(), &pvc_list).await?;
1047+ Ok(())
1048+ }
1049+ #[tokio::main]
1050+ pub async fn delete_secret(namespace: &str, secret: &str) -> Result<(), kube::Error> {
1051+ let client = Client::try_default().await?;
1052+ let api: Api<Secret> = Api::namespaced(client, &namespace);
1053+ api.delete(&secret, &DeleteParams::default()).await?;
1054+ Ok(())
1055+ }
1056+ fn url_to_name(url: &str) -> String {
1057+ let re_not_letters = Regex::new(r"\W").unwrap();
1058+ let name: String = re_not_letters.replace_all(url, "-").to_string();
1059+ return name;
6641060 }
6651061}
helm-control-scripts/src/unarchive.rsnew file+53
@@ -0,0 +1,53 @@
1+use helm_control_scripts::lib::*;
2+
3+fn main() {
4+ let mut log_builder = Builder::from_default_env();
5+ log_builder.target(Target::Stdout).filter(None, LevelFilter::Info).init();
6+ info!("Utils version {}", env!("CARGO_PKG_VERSION"));
7+ let envs = EnvVars::parse();
8+ info!("Env variables received {:?}", &envs);
9+ let cli_opts = CliOpts::from_args();
10+ info!("Cli options received {:?}", &cli_opts);
11+ let overrides = match overrides(&cli_opts, &envs) {
12+ Some(inner) => inner,
13+ None => vec![],
14+ };
15+
16+ let deployment_parameters = HelmDeploymentParameters::new(&cli_opts, &envs);
17+ let namespace = String::from(&cli_opts.namespace);
18+ let release_name = match cli_opts.name {
19+ Some(name) => name,
20+ None => {
21+ error!("mandatory name argument was not provided");
22+ panic!();
23+ }
24+ };
25+ let helm_template = HelmCmd {
26+ name: envs.helm_bin,
27+ mode: HelmMode::Template,
28+ release_name: release_name,
29+ namespace: cli_opts.namespace,
30+ deployment_parameters: deployment_parameters,
31+ overrides: overrides,
32+ };
33+ info!("Generated Helm args: {:?}", &helm_template.args());
34+ match helm_template.run_stdout() {
35+ Ok(status) => {
36+ let (deployments, statefulsets, _ingresses, _old_ingresses, postgresqls, kafkas) = match parse_to_k8s(status) {
37+ Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
38+ Err(err) => panic!("{}", err)
39+ };
40+ match scale(deployments, statefulsets, postgresqls, kafkas, namespace, 1) {
41+ Ok(_status) => info!("Success!"),
42+ Err(status) => {
43+ error!("Error checking statuses");
44+ panic!("{}", status);
45+ }
46+ }
47+ }
48+ Err(status) => {
49+ error!("Error during helm execution");
50+ panic!("{:?}", status);
51+ }
52+ }
53+}
img/demo.gif

Binary file not shown.

img/logo.svg+1−1
@@ -1 +1 @@
1−<svg enable-background="new -4.029 0.071 127.559 53.858" height="53.858" viewBox="-4.029 .071 127.559 53.858" width="127.559" xmlns="http://www.w3.org/2000/svg"><path d="m-4.03.071h127.559v53.858h-127.559z" fill="#fff"/><g fill="#8594f8"><path d="m17.184 22.049c.257-.063.487-.224.624-.474.062-.113.096-.232.11-.354l1.659-.771 1.239-2.479-3.467 2.25c-.463-.209-1.015-.041-1.263.412-.198.363-.14.794.109 1.095l-.213 1.025-.219 1.618-1.121 1.104.624 1.172.31.076-.352-1.188.858-.447.85.49-.289 1.174.323-.062.562-1.145-1-.965.435-1.483zm-.455-.555c-.205-.111-.28-.37-.168-.575.111-.206.368-.28.574-.169.206.114.28.371.169.577-.112.205-.369.281-.575.167z"/><path d="m32.373 23.294-.447-1.879-.394-1.177c.002-.003.004-.004.005-.006.305-.557.1-1.254-.456-1.559-.36-.198-.771-.163-1.104.024l-2.715-1.247h.908v-1.383h.003s0-1.656 0-2.174c0-2.001-1.441-3.656-3.34-4.005v-.519c0-.142-.114-.257-.256-.257h-.203v-1.684l.432-.431c.083-.083.083-.218 0-.3l-.563-.564c-.082-.083-.217-.083-.301 0l-.562.564c-.085.082-.085.217 0 .3l.43.431v1.687h-.201c-.143 0-.258.115-.258.257v.52c-1.897.35-3.338 2.005-3.338 4.004v2.15h-.001v1.406h1.677l-.566.969-1.51 3.115c-.436.058-.841.311-1.066.727-.378.691-.124 1.56.567 1.937.032.018.065.027.099.041l.486 1.6.355.811-1.863.316-.493 1.832.325.34.852-1.684 1.84.168 1.15 1.412-.912 1.783.513.023 1.24-1.576-1.415-2.809-.172-.957-.436-1.436c.146-.115.271-.26.366-.434.231-.423.224-.91.026-1.313l1.899-2.698.706-1.206.617 1.283 2.123 3.862c-.326.525-.303 1.215.135 1.695.116.129.255.219.399.295l.007 1.5.206 1.877-1.958 1.455.393 1.855.442.154v-1.893l1.713-.678 1.66.678v2.061l.469-.211.39-1.967-2.03-1.49.174-1.748-.008-1.695c.052-.035.111-.051.159-.094.583-.529.627-1.432.099-2.016-.203-.224-.467-.342-.741-.405l-1.214-3.117-1.614-1.464 1.442.504 2.86.89c.022.382.215.746.575.942.091.05.188.065.283.089l.341 1.018.763 1.782-1.283 1.168.804 1.455.418.088-.435-1.48 1.183-.59 1.132.59-.434 1.48.443-.088.801-1.453zm-12.056-.097c-.164.299-.537.409-.836.245-.299-.163-.409-.538-.246-.837.164-.298.538-.408.837-.244s.408.538.245.836zm6.899.612c.252-.229.642-.211.87.043.229.25.21.639-.043.869-.253.229-.642.209-.87-.043-.227-.252-.209-.643.043-.869zm-3.372-6.335-.038-.022h.086zm.248-1.691c-1.048 0-1.896-.849-1.896-1.896s.849-1.896 1.896-1.896 1.896.849 1.896 1.896-.849 1.896-1.896 1.896zm6.885 4.161c-.132.243-.433.33-.672.198-.243-.131-.33-.433-.198-.673.131-.24.432-.329.674-.198.24.133.326.433.196.673z"/><path d="m24.802 13.997c-.345 0-.626-.281-.626-.627 0-.199.095-.374.239-.489-.113-.037-.231-.062-.358-.062-.636 0-1.151.516-1.151 1.151s.516 1.151 1.151 1.151 1.152-.516 1.152-1.151c0-.04-.008-.078-.012-.117-.106.088-.243.144-.395.144z"/><path d="m10.575 34.135h2.993v2.992h-2.993z"/><path d="m35.583 34.135h2.993v2.992h-2.993z"/><path d="m31.296 34.135h2.993v2.992h-2.993z"/><path d="m39.812 34.135h2.995v2.992h-2.995z"/><path d="m35.583 30.029h2.993v2.994h-2.993z"/><path d="m39.812 30.029h2.995v2.994h-2.995z"/><path d="m39.812 25.928h2.995v2.994h-2.995z"/><path d="m22.574 34.135h2.993v2.992h-2.993z"/><path d="m18.287 28.261h2.728v2.729h-2.728z" transform="matrix(-.8895 -.4569 .4569 -.8895 23.5969 64.9572)"/><path d="m26.348 30.656h2.729v2.729h-2.729z"/><path d="m14.948 34.135h2.993v2.992h-2.993z"/></g><path d="m51.382 28.279c-.709-.398-1.502-.6-2.358-.6-1.364 0-2.517.459-3.427 1.367-.326.369-.583.715-.762 1.025-.397.66-.599 1.455-.599 2.357-.044 1.326.404 2.467 1.333 3.395.346.348.692.615 1.029.797.661.375 1.398.576 2.196.6h.394.002c1.28-.045 2.353-.504 3.189-1.365.328-.324.586-.662.765-.996.398-.686.601-1.479.601-2.357.021-1.348-.429-2.498-1.334-3.428-.349-.326-.694-.594-1.029-.795zm-.57 4.221c0 .52-.18.965-.533 1.32-.316.314-.704.477-1.152.477-.054 0-.108-.002-.171-.008-.521 0-.966-.18-1.319-.533-.358-.338-.512-.768-.469-1.32 0-.521.179-.965.532-1.322.315-.311.728-.469 1.224-.469h.1c.519 0 .965.182 1.317.533.333.351.492.797.471 1.322z" fill="#201600"/><path d="m60.856 32.463c-.043.566-.224 1.012-.536 1.326-.312.312-.733.471-1.254.471-.031 0-.063 0-.099-.002-.479 0-.914-.168-1.286-.498-.354-.396-.524-.854-.508-1.338.024-.094.037-.188.037-.283 0-.074.02-.15.063-.244.062-.27.212-.545.438-.812.336-.336.776-.484 1.351-.469l.078.002v-2.934l-.166-.002c-1.305 0-2.427.459-3.331 1.363-.332.311-.601.645-.8 1-.309.596-.499 1.211-.563 1.82-.023.109-.033.211-.033.291v.246c-.022 1.344.435 2.494 1.358 3.422.159.18.323.33.487.449.157.119.329.234.513.35.687.398 1.479.602 2.362.602 1.362 0 2.516-.459 3.417-1.363.178-.15.328-.314.453-.484.123-.166.24-.34.35-.516.396-.686.598-1.488.598-2.393v-.076h-2.923z" fill="#201600"/><path d="m67.161 25.719h-2.932v6.945c0 .598.123 1.18.362 1.727.178.486.502.971.968 1.436.351.352.698.617 1.032.797.66.375 1.397.576 2.194.6h.239v-2.932h-.104c-.547-.043-.981-.223-1.293-.531-.295-.318-.449-.678-.47-1.096v-2.148h1.865v-2.93h-1.865v-1.867h.004z" fill="#201600"/><path d="m76.608 28.279c-.71-.398-1.503-.6-2.36-.6-1.365 0-2.517.459-3.427 1.367-.328.371-.583.719-.759 1.025-.398.662-.602 1.457-.602 2.359-.043 1.324.405 2.465 1.332 3.393.348.35.694.617 1.029.797.663.375 1.4.576 2.197.6h.393.002c1.28-.045 2.354-.504 3.19-1.365.333-.33.59-.666.766-.996.397-.686.599-1.479.599-2.357.021-1.346-.424-2.498-1.332-3.428-.35-.326-.694-.594-1.028-.795zm-.569 4.221c0 .518-.179.963-.531 1.32-.319.314-.707.477-1.153.477-.054 0-.108-.002-.171-.008-.522 0-.965-.18-1.321-.533-.358-.338-.511-.77-.469-1.32 0-.52.18-.965.532-1.322.32-.316.722-.469 1.227-.469h.098c.521 0 .965.182 1.319.533.33.351.487.797.469 1.322z" fill="#201600"/><path d="m87.746 29.045c-.902-.906-2.023-1.365-3.328-1.365l-.093.002c-.88 0-1.675.199-2.358.596-.379.203-.715.475-1 .801-.928.928-1.385 2.08-1.361 3.424v6.941h2.932v-2.547c.539.217 1.113.328 1.703.328.053 0 .104-.002.15-.002.902 0 1.699-.203 2.357-.6.336-.182.684-.447 1.03-.799.905-.883 1.365-2.023 1.365-3.391 0-.881-.201-1.686-.604-2.398-.242-.353-.509-.683-.793-.99zm-1.566 3.393c.021.549-.135.982-.471 1.32-.311.311-.756.49-1.324.531-.539.049-.981-.123-1.317-.504-.312-.289-.492-.725-.531-1.287-.022-.572.132-1.014.466-1.354.336-.332.781-.5 1.328-.5.028 0 .059-.002.09-.002.479 0 .894.158 1.226.471.355.358.533.801.533 1.325z" fill="#201600"/><path d="m96.641 28.279c-.709-.398-1.502-.6-2.359-.6-1.363 0-2.516.459-3.426 1.367-.326.371-.584.717-.762 1.025-.397.664-.6 1.459-.6 2.357-.043 1.326.404 2.465 1.332 3.395.344.346.689.613 1.029.797.664.375 1.402.576 2.195.6h.396.002c1.279-.045 2.354-.504 3.19-1.365.328-.328.588-.664.766-.996.396-.686.598-1.479.598-2.357.023-1.346-.425-2.498-1.332-3.428-.352-.328-.697-.595-1.029-.795zm-.57 4.221c0 .521-.181.967-.534 1.32-.316.314-.701.477-1.15.477-.053 0-.107-.002-.17-.008-.521 0-.965-.18-1.32-.533-.359-.336-.514-.768-.471-1.32 0-.529.176-.959.535-1.322.314-.311.726-.469 1.224-.469h.098c.522 0 .968.182 1.32.533.331.351.489.797.468 1.322z" fill="#201600"/><path d="m105.992 25.459v2.545c-.541-.217-1.113-.326-1.701-.326-.052 0-.104.002-.154.004-.9 0-1.707.199-2.396.6-.161.104-.33.215-.514.348-.161.119-.318.256-.481.416-.796.797-1.242 1.748-1.328 2.818-.021.107-.033.213-.033.311v.295c0 .902.201 1.709.6 2.393.104.168.216.336.348.516.121.166.273.33.447.48.908.906 2.059 1.365 3.424 1.365.859 0 1.653-.201 2.36-.6.18-.111.353-.23.513-.35.166-.121.331-.271.482-.447.928-.928 1.387-2.08 1.365-3.424v-6.943h-2.932zm-.466 8.295c-.378.336-.825.504-1.33.504-.548.043-.978-.111-1.315-.469-.354-.355-.533-.799-.535-1.336-.021-.104-.025-.191-.016-.271.012-.084.028-.182.049-.285.062-.268.209-.521.438-.75.312-.312.761-.492 1.324-.533.029-.002.062-.004.091-.004.481 0 .896.16 1.224.469.229.27.391.547.475.818.017.072.035.158.047.238s.018.17.018.27c.014.525-.144.978-.47 1.349z" fill="#201600"/><path d="m64.229 21.738h2.995v2.995h-2.995z" fill="#8594f8"/></svg>
1+<svg xmlns="http://www.w3.org/2000/svg" width="139" height="48.096" viewBox="-5.553 0 139 48.096"><path fill="#FFF" d="M-5.5-.183h138.559v48.549H-5.5V-.183z"/><path fill="#8594F8" d="M21.214 17.557a.953.953 0 0 0 .734-.828l1.659-.771 1.238-2.479-3.467 2.25a.976.976 0 0 0-1.263.412.98.98 0 0 0 .108 1.095l-.213 1.025-.219 1.618-1.121 1.104.624 1.172.311.076-.353-1.188.858-.447.85.49-.289 1.174.323-.062.562-1.145-1-.965.436-1.483.222-1.048zm-.455-.554a.425.425 0 0 1 .406-.744c.206.114.28.371.169.577a.423.423 0 0 1-.575.167z"/><path fill="#8594F8" d="M36.404 18.802l-.447-1.879-.394-1.178.005-.006c.305-.557.1-1.254-.456-1.559a1.127 1.127 0 0 0-1.104.024l-2.715-1.247h.907v-1.384h.003V9.399a4.074 4.074 0 0 0-3.34-4.005v-.516a.256.256 0 0 0-.256-.258h-.203V2.936l.432-.431a.212.212 0 0 0 0-.3l-.562-.564a.214.214 0 0 0-.302 0l-.562.565a.208.208 0 0 0 0 .3l.43.431v1.688h-.201a.257.257 0 0 0-.258.257V5.4a4.077 4.077 0 0 0-3.338 4.004v2.15h-.001v1.406h1.677l-.565.969-1.511 3.115a1.412 1.412 0 0 0-1.065.727 1.43 1.43 0 0 0 .566 1.938c.032.018.065.026.1.041l.486 1.6.354.811-1.863.316-.492 1.832.324.34.853-1.684 1.84.168 1.15 1.412-.912 1.782.513.023 1.24-1.576-1.415-2.809-.172-.957-.437-1.437a1.425 1.425 0 0 0 .392-1.747l1.898-2.697.706-1.206.617 1.282 2.123 3.862c-.326.525-.303 1.215.135 1.695.116.129.255.219.399.295l.007 1.5.206 1.877-1.958 1.455.393 1.855.442.154v-1.893l1.713-.679 1.66.679v2.061l.469-.211.39-1.967-2.029-1.49.174-1.748-.008-1.695c.052-.034.11-.051.158-.094a1.43 1.43 0 0 0 .1-2.016c-.203-.225-.467-.342-.741-.405l-1.214-3.117-1.614-1.464 1.442.504 2.859.891c.022.382.216.746.575.941.091.05.188.065.283.089l.341 1.019.764 1.782-1.283 1.168.804 1.455.418.088-.435-1.48 1.183-.59 1.132.59-.434 1.48.442-.088.802-1.453-1.62-1.451zm-12.057-.096a.617.617 0 1 1-.245-.837c.3.164.408.538.245.837zm6.899.611a.614.614 0 0 1 .87.043.615.615 0 1 1-.87-.043zm-3.372-6.335l-.038-.021h.086l-.048.021zm.248-1.69a1.896 1.896 0 1 1 0-3.792 1.896 1.896 0 0 1 0 3.792zm6.885 4.16a.493.493 0 0 1-.672.198.493.493 0 0 1-.198-.673.498.498 0 0 1 .674-.198c.24.133.326.434.196.673z"/><path fill="#8594F8" d="M28.832 9.506a.625.625 0 1 1-.387-1.116 1.14 1.14 0 0 0-.358-.062 1.151 1.151 0 1 0 1.152 1.151c0-.04-.008-.078-.013-.117a.615.615 0 0 1-.394.144zM14.606 29.644h2.992v2.992h-2.992v-2.992zm25.008 0h2.993v2.992h-2.993v-2.992zm-4.288 0h2.993v2.992h-2.993v-2.992zm8.516 0h2.995v2.992h-2.995v-2.992zm-4.228-4.107h2.993v2.994h-2.993v-2.994zm4.228 0h2.995v2.994h-2.995v-2.994zm0-4.101h2.995v2.994h-2.995v-2.994zm-17.237 8.208h2.992v2.992h-2.992v-2.992zm-2.331-2.672l-2.427-1.247 1.247-2.427 2.426 1.246-1.246 2.428zm6.104-.807h2.729v2.729h-2.729v-2.729zm-11.399 3.479h2.992v2.992h-2.992v-2.992z"/><path fill="#201600" d="M55.412 23.787a4.766 4.766 0 0 0-2.358-.6c-1.363 0-2.517.459-3.427 1.367a6.587 6.587 0 0 0-.762 1.024c-.397.66-.6 1.455-.6 2.358-.044 1.326.404 2.467 1.333 3.395.347.348.692.615 1.029.797a4.68 4.68 0 0 0 2.196.6h.396c1.28-.045 2.354-.504 3.189-1.365.328-.322.586-.662.765-.996.398-.686.602-1.479.602-2.355.021-1.349-.43-2.499-1.334-3.428a6.204 6.204 0 0 0-1.029-.797zm-.57 4.222c0 .52-.18.965-.533 1.318a1.583 1.583 0 0 1-1.151.479c-.054 0-.108-.002-.171-.008a1.81 1.81 0 0 1-1.319-.533c-.358-.338-.512-.77-.469-1.32 0-.521.179-.965.532-1.322.314-.312.728-.47 1.224-.47h.1c.52 0 .965.183 1.317.533.332.351.492.798.47 1.323zm10.044-.037c-.043.564-.224 1.012-.536 1.326-.312.311-.732.471-1.254.471-.03 0-.062 0-.099-.002-.479 0-.914-.168-1.286-.498-.354-.396-.523-.854-.508-1.338.023-.094.037-.189.037-.283a.6.6 0 0 1 .062-.245c.062-.27.212-.545.438-.812.336-.336.775-.484 1.351-.469l.078.002V23.19l-.166-.002c-1.305 0-2.427.459-3.331 1.363a4.183 4.183 0 0 0-.8 1 5.128 5.128 0 0 0-.563 1.819 1.53 1.53 0 0 0-.032.291v.246c-.022 1.346.435 2.494 1.357 3.424.159.18.323.328.487.447.157.119.329.234.513.352.688.396 1.479.602 2.362.602 1.362 0 2.516-.459 3.417-1.363.178-.15.328-.314.453-.484.123-.166.24-.34.35-.516.396-.686.599-1.488.599-2.393V27.9h-2.924l-.005.072zm6.306-6.745h-2.933v6.946c0 .598.123 1.18.362 1.727.178.486.502.971.968 1.436.352.352.698.617 1.032.797.66.375 1.396.576 2.194.6h.238V29.8h-.103c-.547-.043-.981-.223-1.293-.531a1.682 1.682 0 0 1-.47-1.096v-2.148h1.864v-2.93h-1.864v-1.867l.005-.001zm9.446 2.56a4.772 4.772 0 0 0-2.359-.6c-1.365 0-2.518.459-3.428 1.367a6.572 6.572 0 0 0-.759 1.024c-.397.662-.602 1.457-.602 2.36-.043 1.324.405 2.465 1.332 3.393.348.35.693.617 1.028.797.663.375 1.4.576 2.197.6h.396c1.279-.045 2.354-.504 3.189-1.365.333-.33.59-.666.767-.996.396-.686.599-1.479.599-2.355.021-1.346-.424-2.499-1.332-3.428a6.24 6.24 0 0 0-1.028-.797zm-.568 4.222c0 .518-.18.963-.531 1.318a1.598 1.598 0 0 1-1.324.471 1.82 1.82 0 0 1-1.321-.533c-.357-.338-.511-.771-.469-1.32 0-.52.181-.965.532-1.322.32-.316.722-.47 1.227-.47h.099c.521 0 .965.183 1.318.533.329.351.486.798.469 1.323zm11.707-3.456c-.902-.906-2.023-1.365-3.328-1.365l-.093.002c-.881 0-1.676.199-2.358.596a3.568 3.568 0 0 0-1 .802c-.928.928-1.385 2.08-1.361 3.423v6.941h2.933v-2.547a4.54 4.54 0 0 0 1.703.328l.149-.002c.902 0 1.699-.203 2.357-.6a4.455 4.455 0 0 0 1.029-.799c.905-.885 1.365-2.023 1.365-3.393 0-.88-.201-1.685-.604-2.397a8.91 8.91 0 0 0-.792-.989zm-1.567 3.394c.021.549-.135.982-.471 1.32-.312.311-.756.49-1.324.531-.539.049-.981-.123-1.317-.504-.312-.289-.491-.727-.53-1.287-.022-.572.132-1.015.466-1.355.336-.332.781-.5 1.328-.5l.09-.002c.479 0 .894.158 1.226.471.354.358.532.802.532 1.326zm10.461-4.16a4.766 4.766 0 0 0-2.358-.6c-1.363 0-2.517.459-3.427 1.367a6.714 6.714 0 0 0-.762 1.024c-.397.664-.6 1.46-.6 2.358-.043 1.326.403 2.465 1.332 3.395a4.5 4.5 0 0 0 1.028.797 4.702 4.702 0 0 0 2.195.6h.399c1.278-.045 2.354-.504 3.189-1.365.328-.328.588-.664.766-.996.396-.686.599-1.479.599-2.355.022-1.346-.425-2.499-1.332-3.428a6.29 6.29 0 0 0-1.029-.797zm-.57 4.222c0 .52-.181.967-.533 1.318a1.581 1.581 0 0 1-1.15.479c-.053 0-.107-.002-.17-.008-.521 0-.965-.182-1.32-.533-.358-.336-.514-.77-.471-1.32 0-.529.176-.959.535-1.322.313-.312.726-.47 1.224-.47h.099c.521 0 .968.183 1.319.533.33.351.488.798.467 1.323zm9.922-7.041v2.545a4.546 4.546 0 0 0-1.855-.322c-.9 0-1.707.199-2.396.601-.161.104-.33.215-.515.348a4.533 4.533 0 0 0-.48.416c-.796.797-1.242 1.748-1.328 2.818a1.628 1.628 0 0 0-.033.31v.295c0 .902.201 1.709.6 2.393.104.17.217.338.349.518.121.166.272.33.446.479.908.906 2.06 1.365 3.425 1.365.858 0 1.652-.201 2.359-.6.181-.111.354-.23.514-.35.166-.121.331-.271.481-.447.928-.928 1.387-2.08 1.365-3.424V20.97l-2.932-.002zm-.467 8.295a1.943 1.943 0 0 1-1.33.504c-.548.043-.978-.111-1.314-.469-.354-.355-.533-.799-.535-1.336a.863.863 0 0 1-.016-.271 4.32 4.32 0 0 1 .049-.286c.062-.268.209-.521.438-.75.312-.312.762-.491 1.324-.532a1.733 1.733 0 0 1 1.316.465c.229.27.391.547.475.817.017.073.035.159.047.239s.019.17.019.27c.011.525-.147.978-.473 1.349z"/><path fill="#8594F8" d="M68.259 17.246h2.995v2.995h-2.995v-2.995z"/><g><path fill="#8594F8" d="M102.093 45.353a1.568 1.568 0 0 0-.075-.133 1.04 1.04 0 0 1-.411.41c-.576.301-1.162-.161-1.433-.679-.267-.509-.548-1.354.15-1.718.158-.083.344-.124.514-.114-.127-.3-.291-.646-.455-.961-.059-.112-.115-.221-.172-.316.129-.04.232-.07.387-.071l1.761 3.5-.266.082zm-1.018-1.813a.507.507 0 0 0-.528-.023c-.49.257-.188.988.01 1.366.182.347.458.712.895.483.202-.105.396-.282.378-.416l-.755-1.41zm1.137-1.31c.604-.315 1.28.178 1.575.741.252.482.432 1.289-.162 1.655-.596.22-1.117-.075-1.402-.62-.209-.399-.569-1.485-.011-1.776zm.364 1.673c.167.319.585.536.841.402.383-.2.206-.944.046-1.25-.238-.456-.673-.772-1.104-.61-.322.22.074 1.182.217 1.458zm2.454-.195a.362.362 0 0 1-.139.119.826.826 0 0 1-.17.061c-.333-.716-.835-1.599-1.146-2.326l.221-.058.145.239a.843.843 0 0 1 .405-.526c.56-.293.786-.154 1.88 1.885-.022.022-.059.041-.095.061a.719.719 0 0 1-.27.087 1.165 1.165 0 0 1-.141-.224c-.094-.18-.209-.435-.337-.678-.256-.491-.581-.97-.938-.787-.297.155-.31.23-.345.415l.93 1.732zm.149-3.264c.169-.181.232-.322.159-.462-.017-.032-.044-.063-.071-.096-.029.021-.054.041-.081.055-.126.066-.3-.132-.354-.234-.062-.121-.054-.247.045-.299.249-.13.561.171.674.388.137.262.083.564-.296.786l-.076-.138zm2.383-.496c-.232.146-.451.333-.685.479.432.594.693 1.645 1.319 1.317.099-.052.193-.125.299-.23.073.053.124.095.243.17a2.398 2.398 0 0 1-.637.493c-.459.24-.87-.416-1.191-1.029-.115-.22-.22-.429-.307-.596-.076.051-.312.17-.391.216-.079-.119-.138-.198-.191-.29a.35.35 0 0 0 .104-.042c.08-.042.164-.103.253-.149.032-.017.065-.029.088-.041-.119-.248-.264-.55-.373-.803l.221-.058c.133.229.264.48.393.718.239-.103.467-.228.706-.328l.149.173zm2.936-1.266c-.182.137-.416.26-.595.398.266.508.591 1.13.884 1.625.024.137-.02.228-.087.264-.076.039-.188.012-.265-.08-.17-.324-.516-1.062-.817-1.638l-.344.164c-.027-.043-.051-.089-.159-.296.097-.033.256-.101.354-.133-.059-.147-.13-.281-.196-.409-.312-.599-.427-.883.028-1.121.198-.104.453-.173.619-.163l.056.172c-.129.062-.262.113-.369.17-.212.11-.334.25-.167.57.066.126.106.19.139.253.045.086.085.163.181.357.135-.07.266-.126.384-.188.075-.04.143-.079.209-.127l.145.182zm.536-1.064c.604-.315 1.28.178 1.575.741.252.482.433 1.288-.162 1.655-.596.22-1.116-.076-1.401-.62-.209-.4-.57-1.485-.012-1.776zm.365 1.673c.167.319.585.536.842.401.382-.199.206-.944.046-1.25-.238-.455-.673-.771-1.104-.61-.322.221.072 1.184.216 1.459zm2.521-2.355c-.167-.148-.313-.152-.439-.087-.208.108-.316.339-.427.567-.003.006.791 1.525.852 1.584.021.116-.02.184-.082.216-.104.055-.269.013-.359-.095-.31-.691-.682-1.512-.967-2.21l.222-.06.166.285c.074-.176.202-.427.396-.528.293-.152.641-.167.796.13.016.031.035.067.045.107l-.203.091zm1.594-1.632c.031.06.068.13.113.205.176.314.437.769.685 1.242.105.202.229.419.342.635.367.702.626 1.428-.152 1.834-.225.118-.472.207-.711.297-.058-.089-.098-.12-.128-.179-.014-.027-.033-.063-.04-.11.231-.093.47-.166.663-.267.355-.187.544-.474.173-1.203a1.03 1.03 0 0 1-.387.356c-.685.357-1.198-.286-1.499-.862-.302-.576-.345-1.241.264-1.559.152-.08.343-.122.57-.104a2.458 2.458 0 0 0-.066-.137l.173-.148zm.007.594c-.1-.091-.355-.032-.557.073-.335.175-.316.623-.114 1.012.179.341.62 1.046 1.116.786a.51.51 0 0 0 .282-.445l-.727-1.426zm3.295.329c-.186.214-.75.702-1.013.839-.536.28-1.082-.425-1.308-.857-.335-.64-.313-1.236.42-1.619.298-.155.709-.105.885.231.284.545-.613 1.054-.983 1.356.204.358.48.753.939.513.36-.188.861-.6.919-.634l.141.171zm-1.335-1.205c-.133-.153-.233-.228-.382-.149-.351.184-.548.63-.407 1.02.221-.213.6-.618.789-.871zm1.874-1.238c-.234.146-.451.333-.686.479.433.594.694 1.645 1.32 1.317.099-.052.193-.125.298-.23.073.054.124.096.244.17a2.377 2.377 0 0 1-.637.493c-.459.24-.872-.415-1.192-1.028-.115-.221-.219-.43-.306-.597-.076.051-.312.17-.392.216-.08-.118-.138-.197-.19-.29a.344.344 0 0 0 .104-.042c.081-.042.163-.102.254-.149.032-.017.064-.029.088-.041-.119-.248-.264-.55-.374-.802l.222-.059c.132.229.264.48.393.718.239-.102.467-.228.705-.328l.149.173zm2.897-1.513c-.233.145-.451.332-.685.478.431.595.693 1.645 1.318 1.317.1-.052.194-.124.299-.23.073.054.125.096.243.171a2.366 2.366 0 0 1-.637.493c-.459.239-.87-.416-1.19-1.029-.115-.221-.22-.43-.307-.596-.076.051-.312.169-.391.215-.08-.119-.138-.197-.192-.289.037-.007.067-.023.104-.042.081-.042.164-.104.254-.15.031-.017.065-.029.087-.041-.117-.248-.264-.549-.372-.802l.222-.059c.131.229.263.481.393.718.238-.102.466-.227.704-.328l.15.174zm.715-.89c.603-.315 1.28.179 1.574.741.252.482.433 1.289-.161 1.655-.597.22-1.116-.075-1.401-.62-.209-.4-.571-1.484-.012-1.776zm.364 1.673c.167.319.584.536.84.402.384-.2.206-.944.046-1.25-.237-.455-.673-.771-1.104-.61-.32.22.075 1.183.218 1.458zm3.701-3.3c-.247.021-.525.051-.754.17-.262.137-.356.251-.206.538.317.608 1.374-.224 1.77.532.185.355.085.854-.372 1.1a1.165 1.165 0 0 1-.971.054l.056-.2c.307-.028.439.004.713-.139.271-.142.405-.366.286-.596-.283-.541-1.28.314-1.74-.564-.171-.328-.142-.71.224-.9.288-.15.688-.166.947-.193l.047.198zm1.649-.844c-.232.145-.451.332-.684.477.431.595.693 1.646 1.318 1.318.1-.052.193-.124.299-.23.073.054.124.095.243.17a2.362 2.362 0 0 1-.637.493c-.459.24-.87-.416-1.19-1.029-.115-.22-.219-.43-.306-.596-.078.052-.313.17-.393.216-.079-.119-.138-.197-.191-.289a.382.382 0 0 0 .104-.043c.081-.041.164-.103.253-.149.032-.017.066-.029.088-.041-.117-.249-.264-.55-.372-.802l.222-.059c.131.229.263.48.392.717.239-.102.467-.227.705-.327l.149.174zm.361-.437c.073-.232.275-.459.506-.578.652-.341 1.132.354 1.414.895.181.348.323.709.502 1.051.025.112-.007.183-.065.213-.1.052-.278-.017-.347-.146-.008-.015-.017-.031-.019-.047-.082.248-.239.463-.441.568a.657.657 0 0 1-.881-.28c-.284-.544-.058-1.327.483-1.61.036-.018.073-.033.116-.049-.147-.24-.363-.465-.674-.303-.153.08-.284.247-.392.469l-.202-.183zm1.607.605c-.065-.125-.136-.248-.201-.363l-.037.019c-.394.212-.646.769-.411 1.219.101.193.299.233.452.153.188-.099.332-.364.434-.555-.076-.152-.155-.314-.237-.473zm1.615-1.731c-.168-.146-.313-.151-.44-.085-.207.108-.316.337-.426.566-.003.007.791 1.525.852 1.584.02.115-.02.184-.082.216-.104.055-.27.013-.359-.095-.311-.691-.682-1.512-.967-2.21l.221-.06.167.286c.074-.176.201-.426.396-.527.292-.154.64-.169.795.128a.444.444 0 0 1 .045.108l-.202.089zm-20.759 17.415c-.234.146-.451.332-.686.478.431.595.692 1.644 1.319 1.316.1-.052.193-.123.298-.229.074.053.125.095.245.169a2.401 2.401 0 0 1-.637.494c-.461.24-.872-.416-1.192-1.029-.115-.22-.219-.43-.306-.596-.076.051-.313.17-.392.216-.08-.119-.138-.198-.192-.29a.355.355 0 0 0 .104-.042c.082-.042.164-.104.254-.151.031-.016.065-.027.089-.04-.119-.248-.266-.549-.374-.802l.222-.059c.131.229.263.481.393.718.239-.102.466-.227.705-.328l.15.175zm.026-1.516c.196.321.429.777.622 1.103a.87.87 0 0 1 .436-.549c.379-.198.887-.103 1.144.389l.755 1.445a.439.439 0 0 1-.134.104.886.886 0 0 1-.186.073c-.038-.072-.07-.146-.107-.218-.201-.384-.431-.757-.703-1.279-.205-.271-.434-.289-.617-.193a.736.736 0 0 0-.366.54c.303.514.593 1.113.89 1.624-.051.037-.108.079-.172.112a.656.656 0 0 1-.197.069c-.105-.221-.226-.451-.348-.685-.416-.797-.9-1.657-1.274-2.472l.257-.063zm5.018 1.039c-.187.214-.751.702-1.012.839-.536.28-1.083-.426-1.309-.857-.334-.641-.313-1.235.421-1.619.297-.155.707-.104.883.231.285.545-.611 1.054-.981 1.356.204.357.479.753.939.513.359-.188.861-.6.918-.635l.141.172zm-1.335-1.205c-.132-.154-.233-.228-.382-.149-.352.184-.548.631-.407 1.02.222-.212.6-.618.789-.871zm2.008-1.736c.023.045.047.09.074.132.114-.197.258-.329.41-.409.577-.301 1.163.161 1.434.679.266.509.547 1.354-.151 1.718a1.025 1.025 0 0 1-.514.114c.207.374.415.753.629 1.129.021.113-.019.181-.082.214-.104.054-.268.014-.358-.094l-1.709-3.4.267-.083zm1.017 1.813a.512.512 0 0 0 .527.022c.49-.257.188-.987-.009-1.365-.183-.348-.459-.713-.896-.484-.202.105-.396.282-.378.416l.756 1.411zm2.242-2.778c-.168-.146-.313-.151-.44-.085-.206.107-.315.338-.425.566-.003.006.791 1.525.852 1.584.02.116-.02.184-.084.217-.103.054-.268.012-.357-.095-.311-.692-.682-1.513-.967-2.211l.221-.06.166.285c.075-.176.201-.426.396-.527.292-.153.64-.168.795.129a.407.407 0 0 1 .044.108l-.201.089zm.763-1.223c.604-.316 1.28.179 1.574.741.252.482.433 1.287-.16 1.654-.597.221-1.117-.074-1.402-.619-.209-.401-.57-1.484-.012-1.776zm.365 1.672c.167.318.584.536.842.402.382-.2.206-.945.046-1.251-.238-.455-.673-.771-1.104-.61-.322.22.072 1.184.216 1.459zm1.095-2.923c.063.121.049.248-.059.305-.171.09-.355-.032-.441-.198-.064-.122-.055-.247.045-.299.166-.088.368.025.455.192zm.858 3.857c.49-.412.19-1.062-.09-1.599-.19-.365-.391-.725-.785-1.471-.057-.142-.025-.233.042-.269.086-.044.228-.005.312.135.128.244.262.488.403.759.023.046.072.129.136.25.873 1.671.745 2.042.273 2.288a.622.622 0 0 1-.207.067l-.084-.16zm2.62-2.512c-.186.214-.751.703-1.012.839-.536.28-1.083-.425-1.309-.857-.335-.64-.312-1.235.421-1.619.297-.155.708-.105.884.231.285.545-.613 1.055-.983 1.356.204.358.479.753.94.513.359-.188.861-.6.918-.634l.141.171zm-1.336-1.205c-.132-.153-.232-.228-.382-.149-.351.184-.548.631-.406 1.021.221-.214.599-.619.788-.872zm2.057-1.406a2.477 2.477 0 0 0-.33.138c-.644.337-.524 1.06-.237 1.609.171.328.424.656.834.441.135-.07.282-.184.45-.333a.453.453 0 0 1 .13-.113c.072-.038.136-.002.255.182a2.744 2.744 0 0 1-.652.495c-.671.351-.996.09-1.339-.567-.396-.756-.543-1.476.263-1.896.189-.1.395-.167.55-.155l.076.199zm1.577-.75c-.233.146-.451.333-.685.479.432.594.693 1.643 1.318 1.315.1-.052.194-.123.3-.229.073.054.123.097.243.171a2.398 2.398 0 0 1-.637.493c-.459.24-.87-.416-1.191-1.029-.114-.221-.219-.429-.306-.596-.077.051-.312.169-.393.216-.079-.119-.138-.197-.19-.29a.344.344 0 0 0 .104-.042c.08-.042.162-.104.253-.15.031-.017.066-.029.088-.04-.118-.248-.264-.55-.374-.802l.222-.059c.133.229.265.48.393.718.24-.103.468-.228.706-.329l.149.174z"/></g></svg>
img/release_workflow.pngnew file

Binary file not shown.

img/typeable.pngdeleted

Binary file not shown.

img/typeable_logo.svgnew file+1
@@ -0,0 +1 @@
1+<svg enable-background="new 0 0 294.907 62.023" height="62.023" viewBox="0 0 294.907 62.023" width="294.907" xmlns="http://www.w3.org/2000/svg"><path d="m15.886 22.642h7.715v23.163h6.721v-23.163h7.827v-5.46h-22.263z"/><path d="m100.99 19.746c-1.914-1.701-4.551-2.563-7.84-2.563h-11.094v28.623h6.721v-8.689h4.373c3.288 0 5.926-.855 7.838-2.543 1.897-1.673 2.877-3.899 2.916-6.615.001-.056.002-1.356.002-1.413 0-2.793-.982-5.08-2.916-6.8zm-4.803 10.849c-.667.703-1.688 1.06-3.037 1.06h-4.373v-9.013h4.373c1.356 0 2.379.366 3.037 1.088.674.739 1.017 1.699 1.017 2.851 0 .055-.001 1.354-.003 1.407-.031 1.064-.364 1.919-1.014 2.607z"/><path d="m124.167 33.762h12.028v-5.46h-12.028v-5.66h13.909v-5.46h-20.629v28.623h20.667v-5.442h-13.947z"/><path d="m159.432 17.182-10.896 28.623h6.633l2.215-5.908h10.924l2.387 5.908h6.725l-11.48-28.623zm6.638 17.179h-6.543l3.146-8.405z"/><path d="m206.916 30.738c.703-.473 1.281-1.053 1.729-1.729.715-1.089 1.078-2.335 1.078-3.706 0-2.684-1-4.742-2.973-6.119-1.902-1.328-4.559-2.001-7.895-2.001h-9.588v28.623h11.207c3.297 0 5.898-.725 7.732-2.152 1.879-1.463 2.834-3.629 2.834-6.439 0-1.684-.479-3.18-1.422-4.448-.669-.907-1.577-1.588-2.702-2.029zm-4.857-2.856c-.678.516-1.691.777-3.016.777h-3.055v-6.018h2.867c1.436 0 2.512.262 3.195.779.641.484.951 1.24.951 2.314.001.963-.308 1.664-.942 2.148zm-6.071 5.806h4.428c1.35 0 2.354.305 2.984.904.623.592.938 1.473.938 2.621 0 1.061-.303 1.826-.924 2.342-.65.537-1.639.809-2.939.809h-4.486v-6.676z"/><path d="m230.91 17.182h-6.721v28.623h18.989v-5.442h-12.268z"/><path d="m261.578 33.762h12.029v-5.46h-12.029v-5.66h13.91v-5.46h-20.629v28.623h20.666v-5.442h-13.947z"/><path d="m58.742 29.827h.119l5.766-12.645h7.276l-9.821 18.769v10.18h-6.701v-10.479l-9.663-18.47h7.277z" fill="#eaa213"/></svg>
migrations/deploy/move_tag_to_overrides.sqlnew file+18
@@ -0,0 +1,18 @@
1+-- Deploy octopod:move_tag_to_overrides to pg
2+
3+BEGIN;
4+
5+
6+UPDATE deployments as d
7+ SET app_overrides = d.app_overrides ||
8+ jsonb_build_array(jsonb_build_array('image.tag', jsonb_build_object('tag', 'ValueAdded', 'contents', d.tag)));
9+
10+ALTER TABLE "deployments" DROP COLUMN "tag";
11+
12+UPDATE deployment_logs as d
13+ SET app_overrides = d.app_overrides ||
14+ jsonb_build_array(jsonb_build_array('image.tag', jsonb_build_object('tag', 'ValueAdded', 'contents', d.tag)));
15+
16+ALTER TABLE "deployment_logs" DROP COLUMN "tag";
17+
18+COMMIT;
migrations/revert/move_tag_to_overrides.sqlnew file+62
@@ -0,0 +1,62 @@
1+-- Revert octopod:move_tag_to_overrides from pg
2+BEGIN;
3+ALTER TABLE deployments
4+ADD COLUMN tag text;
5+UPDATE deployments AS d
6+SET tag = COALESCE(
7+ (
8+ WITH appOvs AS (
9+ SELECT jsonb_array_elements(d."app_overrides") AS res
10+ )
11+ SELECT a.res->1->>'contents'
12+ FROM appOvs AS a
13+ WHERE (a.res->>0) = 'image.tag'
14+ ),
15+ ''
16+ );
17+UPDATE deployments AS d
18+SET app_overrides = COALESCE(
19+ (
20+ WITH appOvs AS (
21+ SELECT jsonb_array_elements(d."app_overrides") AS res
22+ )
23+ SELECT jsonb_agg(a.res)
24+ FROM appOvs AS a
25+ WHERE (a.res->>0) != 'image.tag'
26+ ),
27+ '[]'
28+ );
29+ALTER TABLE deployments
30+ALTER COLUMN tag
31+SET NOT NULL;
32+--
33+ALTER TABLE deployment_logs
34+ADD COLUMN tag text;
35+UPDATE deployment_logs AS d
36+SET tag = COALESCE(
37+ (
38+ WITH appOvs AS (
39+ SELECT jsonb_array_elements(d."app_overrides") AS res
40+ )
41+ SELECT a.res->1->>'contents'
42+ FROM appOvs AS a
43+ WHERE (a.res->>0) = 'image.tag'
44+ ),
45+ ''
46+ );
47+UPDATE deployment_logs AS d
48+SET app_overrides = COALESCE(
49+ (
50+ WITH appOvs AS (
51+ SELECT jsonb_array_elements(d."app_overrides") AS res
52+ )
53+ SELECT jsonb_agg(a.res)
54+ FROM appOvs AS a
55+ WHERE (a.res->>0) != 'image.tag'
56+ ),
57+ '[]'
58+ );
59+ALTER TABLE deployment_logs
60+ALTER COLUMN tag
61+SET NOT NULL;
62+COMMIT;
migrations/sqitch.plan+1
@@ -16,3 +16,4 @@ rename_delete_to_archive_2 2020-11-26T08:28:58Z Typeable LLC <octopod@typeable.i
1616add_detailed_failures 2021-02-04T11:01:15Z Typeable LLC <octopod@typeable.io> # Added more failure states
1717remove_archived_column 2021-01-28T18:44:54Z Typeable LLC <octopod@typeable.io> # Removed 'archived' column
1818migrate_2.0 2021-08-11T17:09:37Z Typeable LLC <octopod@typeable.io> # Migrate to 2.0-style tables. (Less tables)
19+move_tag_to_overrides 2021-09-25T14:29:43Z Typeable LLC <octopod@typeable.io> # Remove tag as a special field, making it an override field.
migrations/verify/move_tag_to_overrides.sqlnew file+7
@@ -0,0 +1,7 @@
1+-- Verify octopod:move_tag_to_overrides on pg
2+
3+BEGIN;
4+
5+-- XXX Add verifications here.
6+
7+ROLLBACK;
nix/ci.nixnew file+6
@@ -0,0 +1,6 @@
1+{ sources ? import ./sources.nix
2+, pkgs ? import sources.nixpkgs { }
3+}:
4+pkgs.mkShell {
5+ packages = [ pkgs.haskellPackages.fourmolu ];
6+}
nix/default.nix+1−1
@@ -1,5 +1,5 @@
11{ sources ? import ./sources.nix
2−, octopod-css ? import ../octopod-css { inherit pkgsSrc; }
2+, octopod-css ? import ../octopod-css { inherit pkgsSrc system; }
33, migrations ? ../migrations
44, system ? builtins.currentSystem
55, hsPkgs ? import ./.. { inherit system prod; }
nix/sources.json+11−11
@@ -1,14 +1,14 @@
11{
22 "haskellNix": {
3− "branch": "master",
3+ "branch": "iko/ghcjs-patch",
44 "description": "Alternative Haskell Infrastructure for Nixpkgs",
55 "homepage": "https://input-output-hk.github.io/haskell.nix",
6− "owner": "input-output-hk",
6+ "owner": "ilyakooo0",
77 "repo": "haskell.nix",
8− "rev": "62f8632b32933cbd2ccf2927ebb449de943b774d",
9− "sha256": "1pkh10lxry7czj3m6xnw6lvd69dspnsasm9ndiigrx7sfjzb4icg",
8+ "rev": "8cdc0f62f31be4f55bc7db88bdc0cec44bfd8ab9",
9+ "sha256": "0l0xg0s2vn0lpc2m1gavrlzk691sgifjf6l6k5sfsnhw2xq77g48",
1010 "type": "tarball",
11− "url": "https://github.com/input-output-hk/haskell.nix/archive/62f8632b32933cbd2ccf2927ebb449de943b774d.tar.gz",
11+ "url": "https://github.com/ilyakooo0/haskell.nix/archive/8cdc0f62f31be4f55bc7db88bdc0cec44bfd8ab9.tar.gz",
1212 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
1313 },
1414 "niv": {
@@ -17,10 +17,10 @@
1717 "homepage": "https://github.com/nmattia/niv",
1818 "owner": "nmattia",
1919 "repo": "niv",
20− "rev": "e0ca65c81a2d7a4d82a189f1e23a48d59ad42070",
21− "sha256": "1pq9nh1d8nn3xvbdny8fafzw87mj7gsmp6pxkdl65w2g18rmcmzx",
20+ "rev": "5830a4dd348d77e39a0f3c4c762ff2663b602d4c",
21+ "sha256": "1d3lsrqvci4qz2hwjrcnd8h5vfkg8aypq3sjd4g3izbc8frwz5sm",
2222 "type": "tarball",
23− "url": "https://github.com/nmattia/niv/archive/e0ca65c81a2d7a4d82a189f1e23a48d59ad42070.tar.gz",
23+ "url": "https://github.com/nmattia/niv/archive/5830a4dd348d77e39a0f3c4c762ff2663b602d4c.tar.gz",
2424 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
2525 },
2626 "nix-filter": {
@@ -41,10 +41,10 @@
4141 "homepage": "",
4242 "owner": "NixOS",
4343 "repo": "nixpkgs",
44− "rev": "da497034d8429b733111cda03b4790743e2272db",
45− "sha256": "0n6p3kd9bbqpl7byyc2ac316qgj465wq4sj0cvn9minxz5p3vqmd",
44+ "rev": "bb9bd465b625bfc971908c5d3d84ce517e1c0691",
45+ "sha256": "1q558hjfxhbcj6n6kbahswvii95kz6gs5bbzh7ky86cmc2b0l38x",
4646 "type": "tarball",
47− "url": "https://github.com/NixOS/nixpkgs/archive/da497034d8429b733111cda03b4790743e2272db.tar.gz",
47+ "url": "https://github.com/NixOS/nixpkgs/archive/bb9bd465b625bfc971908c5d3d84ce517e1c0691.tar.gz",
4848 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
4949 }
5050}
octo-cli/Setup.hs+1
@@ -1,2 +1,3 @@
11import Distribution.Simple
2+
23main = defaultMain
octo-cli/octo-cli.cabal+1−1
@@ -59,7 +59,7 @@ executable octo
5959 transformers,
6060 table-layout ^>= 0.9.0.0,
6161 servant-auth,
62− ordered-containers,
62+ http-types,
6363 hs-source-dirs: src
6464 default-language: Haskell2010
6565 ghc-options:
octo-cli/src/Octopod/CLI.hs+113−102
@@ -2,7 +2,7 @@
22
33module Octopod.CLI (runOcto) where
44
5−import Common.Types
5+import Common.Types hiding (stderr)
66import Common.Utils (dfiName)
77import Control.Exception
88import Control.Lens hiding (List)
@@ -10,11 +10,13 @@ import Control.Monad
1010import Control.Monad.IO.Class
1111import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
1212import Data.Aeson (decode)
13+import qualified Data.ByteString.Char8 as BSC
1314import qualified Data.ByteString.Lazy.Char8 as LBSC
1415import Data.Coerce
16+import qualified Data.ConfigTree as CT
17+import Data.Foldable
1518import Data.Generics.Labels ()
1619import Data.Generics.Product
17−import qualified Data.Map.Ordered.Strict as OM
1820import Data.Maybe
1921import Data.Proxy
2022import Data.Text (Text)
@@ -24,26 +26,15 @@ import Data.Text.Lens
2426import Data.Time
2527import GHC.IO.Encoding
2628import Network.HTTP.Client.TLS
29+import Network.HTTP.Types.Status
2730import Octopod.CLI.Args
2831import Octopod.PowerAPI
2932import Octopod.PowerAPI.Auth.Client
3033import Servant.API
3134import Servant.Client
32− ( BaseUrl (..),
33− ClientEnv,
34− ClientError,
35− ClientM,
36− client,
37− mkClientEnv,
38− runClientM,
39− )
40−import Servant.Client.Core
41− ( ClientError (FailureResponse),
42− ResponseF (..),
43− parseBaseUrl,
44− )
4535import System.Environment (lookupEnv)
4636import System.Exit
37+import System.IO
4738import Text.Layout.Table
4839import Text.Layout.Table.Extras ()
4940import Prelude as P
@@ -60,31 +51,30 @@ runOcto = do
6051 >>= maybe (die "OCTO_AUTHORIZATION_HEADER is not set") pure
6152 args <- parseArgs
6253 env <- getBaseUrl
63− manager <- newTlsManager
64− let clientEnv = mkClientEnv manager env
54+ mngr <- newTlsManager
55+ let clientEnv = mkClientEnv mngr env
6556 flip runReaderT clientEnv $
6657 case args of
67− Create tName tTag tSetAp tSetDep -> do
58+ Create tName tSetAp tSetDep -> do
6859 appOvs <- either dieT pure $ parseSetOverrides tSetAp
6960 depOvs <- either dieT pure $ parseSetOverrides tSetDep
70− handleCreate auth $ Deployment (coerce tName) (coerce tTag) appOvs depOvs
61+ handleCreate auth $ Deployment (coerce tName) appOvs depOvs
7162 List -> handleList auth
7263 Archive tName -> handleArchive auth . coerce $ tName
73− Update tName tTag tSetAp unsetApp tSetD unsetDep -> do
64+ Update tName tSetAp unsetApp tSetD unsetDep -> do
7465 appOvs <- either dieT pure $ parseSetOverrides tSetAp
7566 depOvs <- either dieT pure $ parseSetOverrides tSetD
7667 let tName' = coerce tName
77− tTag' = coerce <$> tTag
78− handleUpdate auth tName' tTag' appOvs unsetApp depOvs unsetDep
68+ handleUpdate auth tName' appOvs unsetApp depOvs unsetDep
7969 Info tName ->
8070 handleInfo auth . coerce $ tName
8171 Cleanup tName ->
8272 handleCleanup auth . coerce $ tName
8373 Restore tName ->
8474 handleRestore auth . coerce $ tName
85− CleanArchive ->
86− handleCleanArchive auth
8775 GetActionLogs aId l -> handleGetActionInfo auth aId l
76+ Delete tName ->
77+ handleDelete auth . coerce $ tName
8878
8979-- | Returns BaseUrl from 'OCTOPOD_URL' environment variable
9080-- or exit with exit_code=1.
@@ -126,38 +116,22 @@ handleArchive auth dName = do
126116handleUpdate ::
127117 AuthContext AuthHeaderAuth ->
128118 DeploymentName ->
129− Maybe DeploymentTag ->
130119 Overrides 'ApplicationLevel ->
131120 [Text] ->
132121 Overrides 'DeploymentLevel ->
133122 [Text] ->
134123 ReaderT ClientEnv IO ()
135−handleUpdate auth dName dTag dNewAppOvs removedAppOvs dNewDepOvs removedDepOvs = do
124+handleUpdate auth dName dNewAppOvs removedAppOvs dNewDepOvs removedDepOvs = do
136125 clientEnv <- ask
137126 dep <- runClientM' (_fullInfoH auth dName) clientEnv
138− let removeAll :: Ord k => [k] -> OM.OMap k v -> Either k (OM.OMap k v)
139− removeAll [] m = Right m
140− removeAll (k : kk) m =
141− if k `OM.member` m
142− then removeAll kk $ OM.delete k m
143− else Left k
144− removeAllM :: MonadIO m => [Text] -> Overrides l -> m (Overrides l)
145− removeAllM ks (Overrides m) =
146− either
147− (\k -> dieT $ "Override " <> k <> " not present in deployment.")
148− (pure . Overrides)
149− $ removeAll ks m
150− appOverrides' <-
151− fmap (<> dNewAppOvs) $
152− removeAllM removedAppOvs $ dep ^. #deployment . #appOverrides
153− deploymentOverrides' <-
154− fmap (<> dNewDepOvs) $
155− removeAllM removedDepOvs $ dep ^. #deployment . #deploymentOverrides
127+ let removeAll :: [Text] -> Overrides l -> (Overrides l)
128+ removeAll ks (Overrides (CT.toFlatList -> m)) = Overrides $ CT.fromFlatList $ filter (flip elem ks . fst) m
129+ appOverrides' = dNewAppOvs <> (removeAll removedAppOvs $ dep ^. #deployment . #appOverrides)
130+ deploymentOverrides' = dNewDepOvs <> (removeAll removedDepOvs $ dep ^. #deployment . #deploymentOverrides)
156131 liftIO $ do
157132 let dUpdate =
158133 DeploymentUpdate
159− { newTag = fromMaybe (dep ^. #deployment . #tag) dTag
160− , appOverrides = appOverrides'
134+ { appOverrides = appOverrides'
161135 , deploymentOverrides = deploymentOverrides'
162136 }
163137 response <- runClientM (updateH auth dName dUpdate) clientEnv
@@ -175,9 +149,9 @@ handleInfo auth dName = do
175149 case res of
176150 Right (i : _) -> printInfo i
177151 Right [] -> putStrLn notFoundMsg
178− Left err -> putStrLn $ "request failed, reason: " ++ show err
152+ other -> handleResponse (const $ pure ()) other
179153 where
180− notFoundMsg = "deployment " ++ T.unpack (coerce dName) ++ " not found"
154+ notFoundMsg = "Deployment " ++ T.unpack (coerce dName) ++ " not found"
181155
182156-- | Handles the 'cleanup' subcommand.
183157handleCleanup :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
@@ -186,6 +160,13 @@ handleCleanup auth dName = do
186160 liftIO $
187161 handleResponse (const $ pure ()) =<< runClientM (cleanupH auth dName) clientEnv
188162
163+-- | Handles the 'delete' subcommand.
164+handleDelete :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
165+handleDelete auth dName = do
166+ clientEnv <- ask
167+ liftIO $
168+ handleResponse (const $ pure ()) =<< runClientM (deleteH auth dName) clientEnv
169+
189170-- | Handles the 'restore' subcommand.
190171handleRestore :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
191172handleRestore auth dName = do
@@ -193,28 +174,19 @@ handleRestore auth dName = do
193174 liftIO $
194175 handleResponse (const $ pure ()) =<< runClientM (restoreH auth dName) clientEnv
195176
196−-- | Handles the 'clean-archive' subcommand.
197−handleCleanArchive :: AuthContext AuthHeaderAuth -> ReaderT ClientEnv IO ()
198−handleCleanArchive auth = do
199− clientEnv <- ask
200− liftIO $
201− handleResponse (const $ pure ()) =<< runClientM (cleanArchiveH auth) clientEnv
202−
203177-- | Handles the 'logs' subcommand.
204178handleGetActionInfo :: AuthContext AuthHeaderAuth -> ActionId -> LogOutput -> ReaderT ClientEnv IO ()
205179handleGetActionInfo auth aId l = do
206180 clientEnv <- ask
207181 liftIO $ do
208− runClientM (getActionInfoH auth aId) clientEnv >>= \case
209− Left err -> print err
210− Right x -> case l of
211− Out -> T.putStrLn . unStdout $ x ^. #stdout
212− Err -> T.putStrLn . unStderr $ x ^. #stderr
213− ErrOut -> do
214− T.putStrLn "\t\tstdout:\n"
215− T.putStrLn . unStdout $ x ^. #stdout
216− T.putStrLn "\t\tstderr:\n"
217− T.putStrLn . unStderr $ x ^. #stderr
182+ runClientM (getActionInfoH auth aId) clientEnv >>= handleResponse \x -> case l of
183+ Out -> T.putStrLn . unStdout $ x ^. #stdout
184+ Err -> T.putStrLn . unStderr $ x ^. #stderr
185+ ErrOut -> do
186+ T.putStrLn "\t\tstdout:\n"
187+ T.putStrLn . unStdout $ x ^. #stdout
188+ T.putStrLn "\t\tstderr:\n"
189+ T.putStrLn . unStderr $ x ^. #stderr
218190
219191listH :: AuthContext AuthHeaderAuth -> ClientM [DeploymentFullInfo]
220192createH :: AuthContext AuthHeaderAuth -> Deployment -> ClientM CommandResponse
@@ -226,7 +198,7 @@ _statusH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CurrentDeplo
226198cleanupH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CommandResponse
227199restoreH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CommandResponse
228200getActionInfoH :: AuthContext AuthHeaderAuth -> ActionId -> ClientM ActionInfo
229−cleanArchiveH :: AuthContext AuthHeaderAuth -> ClientM CommandResponse
201+deleteH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CommandResponse
230202( listH
231203 :<|> createH
232204 :<|> archiveH
@@ -236,9 +208,9 @@ cleanArchiveH :: AuthContext AuthHeaderAuth -> ClientM CommandResponse
236208 :<|> _statusH
237209 :<|> cleanupH
238210 :<|> restoreH
211+ :<|> deleteH
239212 )
240− :<|> getActionInfoH
241− :<|> cleanArchiveH = pushArrowIntoServantAlt $ client (Proxy @PowerAPI)
213+ :<|> getActionInfoH = pushArrowIntoServantAlt $ client (Proxy @PowerAPI)
242214
243215type PushArrowIntoServantAlt a b = PushArrowIntoServantAlt' a b (CanPushArrow a)
244216
@@ -260,33 +232,57 @@ instance CanPushArrow (x -> y) ~ 'False => PushArrowIntoServantAlt' (x -> y) (x
260232-- | Handles response from Octopod Server.
261233handleResponse :: (a -> IO ()) -> Either ClientError a -> IO ()
262234handleResponse f (Right result) = f result
263−handleResponse _ (Left (FailureResponse _req res)) =
264− T.putStrLn . decodeError $ responseBody res
265−handleResponse _ (Left err) =
266− T.putStrLn $ "command failed due to unknown reason: " <> T.pack (show err)
235+handleResponse _ (Left cErr) = do
236+ case cErr of
237+ (ConnectionError exc) -> do
238+ hPutStrLn stderr "Connection error"
239+ hPutStrLn stderr $ displayException exc
240+ (FailureResponse _ res) -> do
241+ showResponseStatus res
242+ case decodeError $ responseBody res of
243+ Just errT -> do T.hPutStrLn stderr errT
244+ Nothing | not . LBSC.null $ responseBody res -> do
245+ LBSC.hPutStrLn stderr $ responseBody res
246+ pure ()
247+ Nothing -> pure ()
248+ (InvalidContentTypeHeader res) -> do
249+ showResponseStatus res
250+ hPutStrLn stderr "Invalid content type header received"
251+ (UnsupportedContentType ctype res) -> do
252+ showResponseStatus res
253+ hPutStrLn stderr $ "Unsupported content type header received: " <> show ctype
254+ (DecodeFailure err res) -> do
255+ showResponseStatus res
256+ hPutStrLn stderr $ "The response body could not be decoded: "
257+ T.hPutStrLn stderr err
258+ exitFailure
267259
268−decodeError :: LBSC.ByteString -> Text
260+showResponseStatus :: Response -> IO ()
261+showResponseStatus Response {responseStatusCode = (Status code body)} = do
262+ hPutStr stderr $ show code <> " "
263+ BSC.hPutStrLn stderr body
264+ pure ()
265+
266+decodeError :: LBSC.ByteString -> Maybe Text
269267decodeError body =
270− case decode body of
271− Just (ValidationError nameErrors tagErrors) ->
272− T.concat ((<> "\n") <$> nameErrors) <> T.concat ((<> "\n") <$> tagErrors)
273− Just (AppError errorMsg) -> errorMsg
274− Just Success -> "ok"
275− _ -> "error: " <> (T.pack . LBSC.unpack $ body)
268+ decode body <&> \case
269+ (ValidationError nameErrors) ->
270+ T.concat ((<> "\n") <$> nameErrors)
271+ (AppError errorMsg) -> errorMsg
272+ Success -> "ok"
276273
277274-- | Pretty-prints the 'info' subcommand result.
278275printInfo :: DeploymentInfo -> IO ()
279−printInfo (DeploymentInfo (Deployment _ dTag dAppOvs dStOvs) (DeploymentMetadata dMeta) dLogs) = do
276+printInfo (DeploymentInfo (Deployment _ dAppOvs dStOvs) (DeploymentMetadata dMeta) dLogs) = do
280277 T.putStrLn "Current settings:"
281− T.putStrLn $ "tag: " <> coerce dTag
282− T.putStrLn $
283− "application overrides: "
284− <> formatOverrides dAppOvs
285− T.putStrLn $
286− "deployment overrides: "
287− <> formatOverrides dStOvs
288− T.putStrLn $ "metadata: "
289− forM_ dMeta $ \m ->
278+ T.putStrLn "Application config:"
279+ putStrLn $ unlines $ formatOverrides False dAppOvs
280+ T.putStrLn ""
281+ T.putStrLn "Deployment config: "
282+ putStrLn $ unlines $ formatOverrides False dStOvs
283+ T.putStrLn ""
284+ T.putStrLn $ "Metadata: "
285+ for_ dMeta $ \m ->
290286 T.putStrLn $
291287 " " <> m ^. #name <> ": " <> m ^. #link
292288 T.putStrLn ""
@@ -297,40 +293,55 @@ ppDeploymentLogs :: [DeploymentLog] -> IO ()
297293ppDeploymentLogs ds =
298294 putStrLn
299295 . tableString
300− [ column expand right noAlign def
296+ [ column expand center noAlign def
297+ , column expand center noAlign def
301298 , column expand center noAlign def
302299 , column expand center noAlign def
303300 , column expand center noAlign def
304− , column expand left (charAlign '=') def
305− , column expand left (charAlign '=') def
306301 , column expand center noAlign def
307302 ]
308− unicodeBoldHeaderS
303+ unicodeDoubleFrameS
309304 ( titlesH
310305 [ "Created at"
311306 , "Action id"
312307 , "Action"
313− , "Tag"
314− , "App overrides"
315− , "Deployment overrides"
308+ , "App config"
309+ , "Deployment config"
316310 , "Exit code"
317311 ]
318312 )
319313 $ ppDeploymentLogRow <$> ds
320314
321315-- | Pretty-prints the deployment log.
322−ppDeploymentLogRow :: DeploymentLog -> RowGroup Text
316+ppDeploymentLogRow :: DeploymentLog -> RowGroup String
323317ppDeploymentLogRow dLog =
324318 colsAllG
325319 top
326320 [
327− [ T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S")) $
321+ [ formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S")) $
328322 dLog ^. field @"createdAt"
329323 ]
330324 , [dLog ^. field @"actionId" . to unActionId . re _Show . packed]
331− , [dLog ^. field @"action" . to actionToText]
332− , [dLog ^. field @"deploymentTag" . coerced]
333− , dLog ^. field @"deploymentAppOverrides" . to formatOverrides'
334− , dLog ^. field @"deploymentDepOverrides" . to formatOverrides'
325+ , [dLog ^. field @"action" . to (T.unpack . actionToText)]
326+ , dLog ^. field @"deploymentAppOverrides" . to (stripBorder . formatOverrides True)
327+ , dLog ^. field @"deploymentDepOverrides" . to (stripBorder . formatOverrides True)
335328 , [dLog ^. field @"exitCode" . re _Show . packed]
336329 ]
330+
331+stripBorder :: [String] -> [String]
332+stripBorder = fmap (init . tail) . init . tail
333+
334+formatOverrides :: Bool -> Overrides l -> [String]
335+formatOverrides splitlines (Overrides m) =
336+ tableLines
337+ [ column expand right noAlign def
338+ , column expand left noAlign def
339+ ]
340+ unicodeS
341+ def
342+ $ showOverride <$> (reverse . CT.toFlatList) m
343+ where
344+ showOverride (k, v) =
345+ colsAllG top $ [if splitlines then T.chunksOf 15 k else [k], showValue v]
346+ showValue (ValueAdded v) = if splitlines then T.chunksOf 25 v else [v]
347+ showValue ValueDeleted = ["<<REMOVED>>"]
octo-cli/src/Octopod/CLI/Args.hs+23−34
@@ -14,8 +14,6 @@ data Args
1414 = Create
1515 { -- | deployment name
1616 name :: Text
17− , -- | deployment tag
18− tag :: Text
1917 , -- | application-level overrides to set
2018 setAppOverrides :: [Text]
2119 , -- | deployment-level overrides to set
@@ -29,8 +27,6 @@ data Args
2927 | Update
3028 { -- | deployment name
3129 name :: Text
32− , -- | deployment tag
33− newTag :: Maybe Text
3430 , -- | application-level overrides to set
3531 setAppOverrides :: [Text]
3632 , -- | application-level overrides to unset
@@ -52,8 +48,10 @@ data Args
5248 { -- | deployment name
5349 name :: Text
5450 }
55− | CleanArchive
5651 | GetActionLogs ActionId LogOutput
52+ | Delete
53+ { name :: Text
54+ }
5755 deriving stock (Show)
5856
5957data LogOutput = Err | Out | ErrOut
@@ -70,7 +68,8 @@ readLogOutput _ = Nothing
7068
7169-- | Parses octo CLI subcommands and arguments.
7270parseArgs :: IO Args
73−parseArgs = execParser $ info (commandArgs <**> helper) fullDesc
71+parseArgs =
72+ customExecParser (prefs showHelpOnError) $ info (commandArgs <**> helper) fullDesc
7473
7574-- | Parses octo CLI subcommands.
7675commandArgs :: Parser Args
@@ -83,30 +82,27 @@ commandArgs =
8382 <> command "info" (info infoArgs (progDesc "get the deployment info"))
8483 <> command "cleanup" (info cleanupArgs (progDesc "cleanup the deployment"))
8584 <> command "restore" (info restoreArgs (progDesc "restore the deployment"))
86− <> command
87− "clean-archive"
88− (info cleanupArchiveArgs (progDesc "cleanup all archived deployments"))
8985 <> command
9086 "logs"
9187 (info actionLogsArgs (progDesc "get deployment logs of a given action"))
88+ <> command "delete" (info deleteArgs (progDesc "delete the deployment from Octopod. Does no cleanup."))
9289 )
9390
9491-- | Parses arguments of 'create' subcommand.
9592createArgs :: Parser Args
9693createArgs =
9794 Create
98− <$> strOption (long "name" <> short 'n' <> help "deployment name")
99− <*> strOption (long "tag" <> short 't' <> help "deployment tag")
95+ <$> deploymentNameArgument
10096 <*> many
10197 ( strOption
102− ( long "set-app-env-override"
98+ ( long "set-app-config"
10399 <> short 'e'
104100 <> help "set application level override"
105101 )
106102 )
107103 <*> many
108104 ( strOption
109− ( long "set-deployment-override"
105+ ( long "set-deployment-config"
110106 <> short 'o'
111107 <> help "set deployment level override"
112108 )
@@ -119,19 +115,16 @@ listArgs =
119115
120116-- | Parses arguments of 'archive' subcommand.
121117archiveArgs :: Parser Args
122−archiveArgs =
123− Archive
124− <$> strOption (long "name" <> short 'n' <> help "deployment name")
118+archiveArgs = Archive <$> deploymentNameArgument
125119
126120-- | Parses arguments of 'update' subcommand.
127121updateArgs :: Parser Args
128122updateArgs =
129123 Update
130− <$> strOption (long "name" <> short 'n' <> help "deployment name")
131− <*> optional (strOption (long "tag" <> short 't' <> help "deployment tag"))
124+ <$> deploymentNameArgument
132125 <*> many
133126 ( strOption
134− ( long "set-app-env-override"
127+ ( long "set-app-config"
135128 <> short 'e'
136129 <> help "set application level override"
137130 )
@@ -145,7 +138,7 @@ updateArgs =
145138 )
146139 <*> many
147140 ( strOption
148− ( long "set-deployment-override"
141+ ( long "set-deployment-config"
149142 <> short 'o'
150143 <> help "set deployment level override"
151144 )
@@ -160,26 +153,19 @@ updateArgs =
160153
161154-- | Parses arguments of 'info' subcommand.
162155infoArgs :: Parser Args
163−infoArgs =
164− Info
165− <$> strOption (long "name" <> short 'n' <> help "deployment name")
156+infoArgs = Info <$> deploymentNameArgument
166157
167158-- | Parses arguments of 'cleanup' subcommand.
168159cleanupArgs :: Parser Args
169−cleanupArgs =
170− Cleanup
171− <$> strOption (long "name" <> short 'n' <> help "deployment name")
160+cleanupArgs = Cleanup <$> deploymentNameArgument
161+
162+-- | Parses arguments of 'delete' subcommand.
163+deleteArgs :: Parser Args
164+deleteArgs = Delete <$> deploymentNameArgument
172165
173166-- | Parses arguments of 'restore' subcommand.
174167restoreArgs :: Parser Args
175−restoreArgs =
176− Restore
177− <$> strOption (long "name" <> short 'n' <> help "deployment name")
178−
179−-- | Parses arguments of 'clean-archive' subcommand.
180−cleanupArchiveArgs :: Parser Args
181−cleanupArchiveArgs =
182− pure CleanArchive
168+restoreArgs = Restore <$> deploymentNameArgument
183169
184170actionLogsArgs :: Parser Args
185171actionLogsArgs =
@@ -193,3 +179,6 @@ actionLogsArgs =
193179 <> value ErrOut
194180 <> completeWith ["stdout", "stderr", "all"]
195181 )
182+
183+deploymentNameArgument :: Parser Text
184+deploymentNameArgument = strOption (long "name" <> short 'n' <> help "deployment name")
octo-cli/src/Text/Layout/Table/Extras.hs+11−9
@@ -2,23 +2,25 @@
22
33module Text.Layout.Table.Extras
44 (
5− ) where
5+ )
6+where
67
7−import Data.Text
8+import Data.Text
89import qualified Data.Text as T
9−import Text.Layout.Table.Cell
10−import Text.Layout.Table.Primitives.AlignInfo
11−import Text.Layout.Table.StringBuilder
10+import Text.Layout.Table.Cell
11+import Text.Layout.Table.Primitives.AlignInfo
12+import Text.Layout.Table.StringBuilder
1213
1314instance Cell Text where
1415 dropLeft = T.drop
1516 dropRight = T.dropEnd
1617 visibleLength = T.length
1718 measureAlignment p xs = case T.break p xs of
18− (ls, rs) -> AlignInfo (T.length ls) $
19− if T.null rs
20− then Nothing
21− else Just $ T.length rs - 1
19+ (ls, rs) ->
20+ AlignInfo (T.length ls) $
21+ if T.null rs
22+ then Nothing
23+ else Just $ T.length rs - 1
2224 buildCell = stringB . T.unpack
2325
2426instance StringBuilder Text where
octopod-api/Setup.hs+1
@@ -1,2 +1,3 @@
11import Distribution.Simple
2+
23main = defaultMain
octopod-api/src/Octopod/API.hs+1−1
@@ -42,7 +42,7 @@ type DefaultApplicationOverrides =
4242 :> ReqBody '[JSON] (Config 'DeploymentLevel)
4343 :> Post '[JSON] (DefaultConfig 'ApplicationLevel)
4444
45−type PingEndpoint = "ping" :> GetNoContent
45+type PingEndpoint = "ping" :> Get '[PlainText] Text
4646type ProjectNameEndpoint =
4747 "project_name" :> Get '[JSON] ProjectName
4848
octopod-api/src/Octopod/API/WebSocket.hs+1
@@ -1,3 +1,4 @@
1+{- ORMOLU_DISABLE -}
12{-# LANGUAGE CPP #-}
23
34module Octopod.API.WebSocket
octopod-api/src/Octopod/PowerAPI.hs+26−27
@@ -36,42 +36,41 @@ type StatusEndpoint c = c :> "status" :> Get '[JSON] CurrentDeploymentStatus
3636type CleanupEndpoint c =
3737 c :> "cleanup" :> Delete '[JSON] CommandResponse
3838
39+type DeleteEndpoint c =
40+ c :> "delete" :> Delete '[JSON] CommandResponse
41+
3942type RestoreEndpoint c =
4043 c :> "restore" :> Patch '[JSON] CommandResponse
4144
4245type GetActionInfoEndpoint =
4346 "log" :> Capture "action_id" ActionId :> Get '[JSON] ActionInfo
4447
45−type CleanArchiveEndpoint =
46− "clean_archive" :> Delete '[JSON] CommandResponse
47−
4848type DeploymentAPI' c =
4949 Auth '[AuthHeaderAuth] () :> "api" :> "v1"
5050 :> ( "deployments"
51− :> ( ListEndpoint
52− -- endpoint to get deployment list
53− :<|> CreateEndpoint
54− -- endpoint to create a new deployment
55− :<|> ArchiveEndpoint c
56− -- endpoint to archive an existing deployment
57− :<|> UpdateEndpoint c
58− -- endpoint to update an existing deployment
59− :<|> InfoEndpoint c
60− -- endpoint to get deployment info
61− :<|> FullInfoEndpoint c
62− -- endpoint to get full deployment info
63− :<|> StatusEndpoint c
64− -- endpoint to get deployment status
65− :<|> CleanupEndpoint c
66− -- endpoint to clean up resources of an archived deployment
67− :<|> RestoreEndpoint c
68− -- endpoint to restore an archived deployment
69− )
70− :<|> GetActionInfoEndpoint
71− -- endpoint to get action logs
72− :<|> CleanArchiveEndpoint
73− -- endpoint to clean up resources of all archived deployments
74− -- according to the archive retention policy
51+ :> ( ListEndpoint
52+ -- endpoint to get deployment list
53+ :<|> CreateEndpoint
54+ -- endpoint to create a new deployment
55+ :<|> ArchiveEndpoint c
56+ -- endpoint to archive an existing deployment
57+ :<|> UpdateEndpoint c
58+ -- endpoint to update an existing deployment
59+ :<|> InfoEndpoint c
60+ -- endpoint to get deployment info
61+ :<|> FullInfoEndpoint c
62+ -- endpoint to get full deployment info
63+ :<|> StatusEndpoint c
64+ -- endpoint to get deployment status
65+ :<|> CleanupEndpoint c
66+ -- endpoint to clean up resources of an archived deployment
67+ :<|> RestoreEndpoint c
68+ -- endpoint to restore an archived deployment
69+ :<|> DeleteEndpoint c
70+ -- endpoint to clean up resources of an archived deployment
71+ )
72+ :<|> GetActionInfoEndpoint
73+ -- endpoint to get action logs
7574 )
7675
7776data AuthHeaderAuth
octopod-backend/Setup.hs+2−1
@@ -1,2 +1,3 @@
1−import Distribution.Simple
1+import Distribution.Simple
2+
23main = defaultMain
octopod-backend/app/Main.hs+5−1
@@ -1,6 +1,10 @@
1+{-# LANGUAGE TemplateHaskell #-}
2+
13module Main (main) where
24
5+import qualified Data.Text as T
6+import Development.GitRev
37import Octopod.Server
48
59main :: IO ()
6−main = runOctopodServer
10+main = runOctopodServer (T.pack $gitHash)
octopod-backend/octopod-backend.cabal+11−3
@@ -55,17 +55,22 @@ library
5555 , QuantifiedConstraints
5656 , DeriveAnyClass
5757 , ViewPatterns
58+ , TupleSections
59+ , GADTs
60+ , TypeSynonymInstances
61+ , FlexibleInstances
5862 exposed-modules:
5963 Octopod.Server
6064 Octopod.Server.Args
6165 Octopod.Server.ControlScriptUtils
62− Octopod.Server.Logger
66+ Octopod.Server.Logging
6367 Octopod.Server.Posix
6468 Orphans
6569 Types
6670 Control.Octopod.DeploymentLock
6771 Octopod.PowerAPI.Auth.Server
6872 Octopod.Schema
73+ Control.CacheMap
6974 hs-source-dirs:
7075 src
7176 build-depends:
@@ -76,7 +81,7 @@ library
7681 , cassava
7782 , conduit
7883 , octopod-common
79− , fast-logger
84+ , katip
8085 , generic-lens
8186 , http-api-data
8287 , lens
@@ -108,8 +113,9 @@ library
108113 , time
109114 , hasql
110115 , hasql-transaction
111− , ordered-containers
116+ , unordered-containers
112117 , vector
118+ , process
113119 default-language: Haskell2010
114120
115121executable octopod-exe
@@ -132,4 +138,6 @@ executable octopod-exe
132138 build-depends:
133139 base >=4.7 && <5
134140 , octopod-backend
141+ , gitrev
142+ , text
135143 default-language: Haskell2010
octopod-backend/src/Control/CacheMap.hsnew file+89
@@ -0,0 +1,89 @@
1+module Control.CacheMap
2+ ( CacheMap,
3+ initCacheMap,
4+ lookupBlocking,
5+ )
6+where
7+
8+import Control.Arrow
9+import Control.Concurrent.Lifted
10+import Control.Monad
11+import Control.Monad.Base
12+import Control.Monad.Except
13+import Control.Monad.Trans.Control
14+import Data.Fixed
15+import Data.Foldable
16+import Data.IORef.Lifted
17+import Data.Map.Strict (Map)
18+import qualified Data.Map.Strict as M
19+import Data.Time
20+import System.Mem.Weak
21+
22+data CacheMap e c k v = CacheMap !(InternalCacheMap e k v) (forall m. c m => k -> m v) !NominalDiffTime
23+
24+type InternalCacheMap e k v = IORef (Map k (UTCTime, MVar (Either e v)))
25+
26+initCacheMap ::
27+ forall c e k v m.
28+ MonadBase IO m =>
29+ -- | Time after which a value is considered "invalid" (removed from cache). Not exact.
30+ NominalDiffTime ->
31+ -- | Time after which the value should be updated: upon query the old value would be returned, but
32+ -- a background computation for the new value would be started.
33+ NominalDiffTime ->
34+ (forall n. c n => k -> n v) ->
35+ m (CacheMap e c k v)
36+initCacheMap invalid update createValue = do
37+ ref <- newIORef M.empty
38+ wRef <- liftBase $ mkWeak ref ref Nothing
39+ void $ liftBase $ fork $ removeInvalidCaches invalid wRef
40+ pure $ CacheMap ref createValue update
41+
42+removeInvalidCaches ::
43+ -- | Time after which a value is considered invalid. Not exact.
44+ NominalDiffTime ->
45+ Weak (InternalCacheMap e k v) ->
46+ IO ()
47+removeInvalidCaches invalid wRef = do
48+ threadDelayMicro (realToFrac $ invalid / 10)
49+ cutoff <- addUTCTime (negate invalid) <$> getCurrentTime
50+ deRefWeak wRef >>= \case
51+ Nothing -> pure ()
52+ Just ref -> do
53+ atomicModifyIORef' ref (M.filter (\(t, _) -> t > cutoff) &&& const ())
54+ removeInvalidCaches invalid wRef
55+ where
56+ threadDelayMicro :: Micro -> IO ()
57+ threadDelayMicro (MkFixed i) = threadDelay (fromInteger i)
58+
59+lookupBlocking ::
60+ (MonadBaseControl IO m, MonadError e m, c m, Ord k) =>
61+ CacheMap e c k v ->
62+ k ->
63+ m v
64+lookupBlocking (CacheMap ref createValue update) k = do
65+ now <- liftBase getCurrentTime
66+ blank <- newEmptyMVar
67+ -- Return the var with an error or result and a bool indicating whether it needs to be updated
68+ -- If there's no var or if needs to be updated, the update goes into 'blank'
69+ mVar <- atomicModifyIORef' ref $ \m -> case M.lookup k m of
70+ Nothing -> (M.insert k (now, blank) m, Nothing)
71+ Just (timeCreated, var) ->
72+ if timeCreated < addUTCTime (negate update) now
73+ then (M.insert k (now, blank) m, Just (var, True))
74+ else (m, Just (var, False))
75+ -- Return the var from where the *current* result should be read,
76+ -- and optionally the var where the recomputation should write to
77+ (rVar, mwVar) <- case mVar of
78+ Nothing -> pure (blank, Just blank)
79+ Just (var, recompute) ->
80+ takeMVar var >>= \case
81+ Left _ -> pure $ if recompute then (blank, Just blank) else (var, Just var)
82+ res@(Right _) -> do
83+ putMVar var res
84+ pure $ (var, if recompute then Just blank else Nothing)
85+ for_ mwVar $ \wVar -> do
86+ fork $ do
87+ v <- (Right <$> createValue k) `catchError` (pure . Left)
88+ putMVar wVar v
89+ readMVar rVar >>= liftEither
octopod-backend/src/Control/Octopod/DeploymentLock.hs+42−47
@@ -1,55 +1,50 @@
11module Control.Octopod.DeploymentLock
2− ( LockedDeployments
3− , initLockedDeployments
4− , withLockedDeployment
5− , isDeploymentLocked
6− ) where
2+ ( LockedDeployments,
3+ initLockedDeployments,
4+ withLockedDeployment,
5+ isDeploymentLocked,
6+ )
7+where
78
8−import Common.Types
9−import Control.Concurrent.STM
10−import Control.Exception.Lifted
11−import Control.Monad.Base
12−import Control.Monad.Reader
13−import Control.Monad.Trans.Control
14−import Data.Generics.Product.Typed
15−import Data.Set (Set)
9+import Common.Types
10+import Control.Arrow
11+import Control.Exception.Lifted
12+import Control.Monad.Base
13+import Control.Monad.Reader
14+import Control.Monad.Trans.Control
15+import Data.Generics.Product.Typed
16+import Data.IORef.Lifted
17+import Data.Set (Set)
1618import qualified Data.Set as S
1719
18−newtype LockedDeployments = LockedDeployments (TVar (Set DeploymentName))
20+newtype LockedDeployments = LockedDeployments (IORef (Set DeploymentName))
1921
20−initLockedDeployments :: IO LockedDeployments
21−initLockedDeployments = LockedDeployments <$> newTVarIO S.empty
22+initLockedDeployments :: MonadBase IO m => m LockedDeployments
23+initLockedDeployments = LockedDeployments <$> newIORef S.empty
2224
23−withLockedDeployment
24− ::
25− ( MonadReader r m
26− , HasType LockedDeployments r
27− , MonadBaseControl IO m
28− )
29− => DeploymentName
30− -> m a
31− -- ^ The conflict handler. Gets called if the deployment is
32− -- already being processed.
33− -> m a
34− -- ^ The actions to be performed when the deployment is locked.
35− -> m a
36−withLockedDeployment dName conflictHandler m = do
37− (LockedDeployments tvar) <- asks getTyped
38− proceed <- liftBase . atomically $ do
39− s <- readTVar tvar
40− if S.member dName s
41− then return False -- Do not proceed
42− else do
43− writeTVar tvar (S.insert dName s)
44− return True -- Proceed
45− if proceed
46− then finally m $ liftBase . atomically $ modifyTVar tvar (S.delete dName)
47− else conflictHandler
25+withLockedDeployment ::
26+ ( MonadReader r m
27+ , HasType LockedDeployments r
28+ , MonadBaseControl IO m
29+ ) =>
30+ DeploymentName ->
31+ -- | The conflict handler. Gets called if the deployment is
32+ -- already being processed.
33+ m a ->
34+ -- | The actions to be performed when the deployment is locked.
35+ m a ->
36+ m a
37+withLockedDeployment dName conflict act = do
38+ LockedDeployments ref <- asks getTyped
39+ bracket
40+ (atomicModifyIORef' ref (S.insert dName &&& S.notMember dName))
41+ (\ok -> when ok $ atomicModifyIORef' ref (S.delete dName &&& const ()))
42+ (\ok -> if ok then act else conflict)
4843
49−isDeploymentLocked
50− :: (MonadReader r m , HasType LockedDeployments r, MonadBase IO m)
51− => DeploymentName -> m Bool
44+isDeploymentLocked ::
45+ (MonadReader r m, HasType LockedDeployments r, MonadBase IO m) =>
46+ DeploymentName ->
47+ m Bool
5248isDeploymentLocked dName = do
53− (LockedDeployments tvar) <- asks getTyped
54− s <- liftBase $ readTVarIO tvar
55− return $ S.member dName s
49+ LockedDeployments ref <- asks getTyped
50+ S.member dName <$> readIORef ref
octopod-backend/src/Octopod/Schema.hs−4
@@ -25,7 +25,6 @@ deploymentSchema =
2525 DeploymentSchema
2626 { id_ = "id"
2727 , name = "name"
28− , tag = "tag"
2928 , appOverrides = "app_overrides"
3029 , deploymentOverrides = "deployment_overrides"
3130 , createdAt = "created_at"
@@ -41,7 +40,6 @@ deploymentSchema =
4140data DeploymentSchema f = DeploymentSchema
4241 { id_ :: Column f DeploymentId
4342 , name :: Column f DeploymentName
44− , tag :: Column f DeploymentTag
4543 , appOverrides :: Column f (Overrides 'ApplicationLevel)
4644 , deploymentOverrides :: Column f (Overrides 'DeploymentLevel)
4745 , createdAt :: Column f UTCTime
@@ -62,7 +60,6 @@ data DeploymentLogSchema f = DeploymentLogSchema
6260 { actionId :: Column f ActionId
6361 , deploymentId :: Column f DeploymentId
6462 , action :: Column f Action
65− , deploymentTag :: Column f DeploymentTag
6663 , exitCode :: Column f Int64
6764 , createdAt :: Column f UTCTime
6865 , archived :: Column f Bool
@@ -85,7 +82,6 @@ deploymentLogSchema =
8582 { actionId = "id"
8683 , deploymentId = "deployment_id"
8784 , action = "action"
88− , deploymentTag = "tag"
8985 , exitCode = "exit_code"
9086 , createdAt = "created_at"
9187 , archived = "archived"
octopod-backend/src/Octopod/Server.hs+478−445
@@ -2,13 +2,12 @@ module Octopod.Server (runOctopodServer) where
22
33import Common.Validation
44import Control.Applicative
5−import Control.Concurrent (threadDelay)
6−import Control.Concurrent.Async (race_)
7−import qualified Control.Concurrent.Lifted as L
8−import Control.Concurrent.MVar
5+import Control.CacheMap (CacheMap)
6+import qualified Control.CacheMap as CM
7+import Control.Concurrent.Async (race)
8+import Control.Concurrent.Lifted hiding (yield)
99import Control.Concurrent.STM
10−import Control.Exception (Exception (displayException), throwIO)
11−import qualified Control.Exception.Lifted as L
10+import Control.Exception.Lifted hiding (Handler)
1211import Control.Lens hiding (Context, each, pre, (<.))
1312import Control.Lens.Extras
1413import Control.Monad
@@ -24,27 +23,32 @@ import qualified Data.ByteString.Lazy as BSL
2423import qualified Data.ByteString.Lazy.Char8 as BSLC
2524import Data.Coerce
2625import Data.Conduit (ConduitT, yield)
26+import qualified Data.ConfigTree as CT
2727import qualified Data.Csv as C
28+import Data.Fixed
2829import Data.Foldable
2930import Data.Functor
31+import Data.Functor.Contravariant
3032import Data.Generics.Labels ()
3133import Data.Generics.Product
32−import Data.IORef
34+import Data.IORef.Lifted
3335import Data.Int
34−import qualified Data.Map.Ordered.Strict as OM
3536import Data.Maybe
3637import Data.Pool
37−import Data.Text (pack, unpack, unwords)
38+import Data.Text (pack)
3839import qualified Data.Text as T
3940import qualified Data.Text.Encoding as T
4041import Data.Time
4142import Data.Traversable
4243import qualified Data.Vector as V
44+import GHC.Exception
45+import GHC.Stack
4346import Hasql.Connection
4447import qualified Hasql.Session as HasQL
4548import Hasql.Statement
4649import Hasql.Transaction
4750import Hasql.Transaction.Sessions
51+import Katip hiding (Namespace)
4852import Network.Wai.Handler.Warp
4953import Octopod.API
5054import Octopod.PowerAPI
@@ -52,7 +56,7 @@ import Octopod.PowerAPI.Auth.Server
5256import Octopod.Schema
5357import Octopod.Server.Args
5458import Octopod.Server.ControlScriptUtils
55−import Octopod.Server.Logger
59+import Octopod.Server.Logging
5660import Octopod.Server.Posix
5761import Options.Generic
5862import Orphans ()
@@ -63,61 +67,66 @@ import Servant
6367import Servant.Auth.Server
6468import System.Environment (lookupEnv)
6569import System.Exit
66−import System.Log.FastLogger
6770import System.Posix.Signals (sigTERM)
71+import Text.Read (readMaybe)
6872import Types
6973import Prelude hiding (lines, log, unlines, unwords)
7074
71−type AppM = ReaderT AppState Handler
75+type AppM = ReaderT AppState (KatipContextT Handler)
76+
77+class (KatipContext m, MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) => AppMConstraints m
78+instance (KatipContext m, MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) => AppMConstraints m
7279
7380-- | Octopod Server state definition.
7481data AppState = AppState
7582 { -- | postgres pool
76− dbPool :: Pool Connection
77− , -- | logger
78− logger :: TimedFastLogger
83+ dbPool :: !(Pool Connection)
7984 , -- | channel for WS events for the frontend
80− eventSink :: TChan WSEvent
85+ eventSink :: !(TChan WSEvent)
8186 , -- | background workers counter
82− bgWorkersCounter :: IORef Int
87+ bgWorkersCounter :: !(IORef Int)
8388 , -- | flag of activating graceful shutdown
84− gracefulShutdownActivated :: IORef Bool
89+ gracefulShutdownActivated :: !(IORef Bool)
8590 , -- | semaphore for graceful shutdown
86− shutdownSem :: MVar ()
91+ shutdownSem :: !(MVar ())
8792 , -- | project name
88− projectName :: ProjectName
93+ projectName :: !ProjectName
8994 , -- | base domain
90− baseDomain :: Domain
95+ baseDomain :: !Domain
9196 , -- | namespace
92− namespace :: Namespace
93− , -- | archive retention
94− archiveRetention :: ArchiveRetention
97+ namespace :: !Namespace
9598 , -- | status update timeout
96− statusUpdateTimeout :: Timeout
99+ statusUpdateTimeout :: !Timeout
97100 , -- | creation command path
98− creationCommand :: Command
101+ creationCommand :: !Command
99102 , -- | update command path
100− updateCommand :: Command
103+ updateCommand :: !Command
101104 , -- | deletion command path
102− archiveCommand :: Command
105+ archiveCommand :: !Command
103106 , -- | checking command path
104− checkingCommand :: Command
107+ checkingCommand :: !Command
105108 , -- | cleanup command path
106− cleanupCommand :: Command
109+ cleanupCommand :: !Command
107110 , -- | archive checking command path
108− archiveCheckingCommand :: Command
111+ archiveCheckingCommand :: !Command
109112 , -- | tag checking command path
110− tagCheckingCommand :: Command
111− , infoCommand :: Command
112− , notificationCommand :: Maybe Command
113− , deploymentOverridesCommand :: Command
114− , deploymentOverrideKeysCommand :: Command
115− , applicationOverridesCommand :: Command
116− , applicationOverrideKeysCommand :: Command
117− , unarchiveCommand :: Command
113+ configCheckingCommand :: !Command
114+ , infoCommand :: !Command
115+ , notificationCommand :: !(Maybe Command)
116+ , deploymentOverridesCommand :: !Command
117+ , deploymentOverrideKeysCommand :: !Command
118+ , applicationOverridesCommand :: !Command
119+ , applicationOverrideKeysCommand :: !Command
120+ , unarchiveCommand :: !Command
118121 , -- | Deployments currently being processed which has not yet been
119122 -- recorded in the database.
120− lockedDeployments :: LockedDeployments
123+ lockedDeployments :: !LockedDeployments
124+ , depOverridesCache :: !(CacheMap ServerError AppMConstraints () (DefaultConfig 'DeploymentLevel))
125+ , depOverrideKeysCache :: !(CacheMap ServerError AppMConstraints () [Text])
126+ , appOverridesCache :: !(CacheMap ServerError AppMConstraints (Config 'DeploymentLevel) (DefaultConfig 'ApplicationLevel))
127+ , appOverrideKeysCache :: !(CacheMap ServerError AppMConstraints (Config 'DeploymentLevel) [Text])
128+ , gitSha :: !Text
129+ , controlScriptTimeout :: !ControlScriptTimeout
121130 }
122131 deriving stock (Generic)
123132
@@ -142,130 +151,215 @@ data FullInfoListType
142151 | FullInfoOnlyForOne DeploymentName
143152 deriving stock (Show)
144153
145−runOctopodServer :: IO ()
146−runOctopodServer = do
147− logger' <- newLogger
148− logInfo logger' "started"
154+runOctopodServer ::
155+ -- | The git SHA
156+ Text ->
157+ IO ()
158+runOctopodServer sha = do
149159 bgWorkersC <- newIORef 0
150160 gracefulShutdownAct <- newIORef False
151161 shutdownS <- newEmptyMVar
152− void $ do
153− let termHandler = terminationHandler bgWorkersC gracefulShutdownAct shutdownS
154− installShutdownHandler logger' [sigTERM] termHandler
155162 opts <- parseArgs
156163 let a ?! e = a >>= maybe (die e) pure
157164 getEnvOrDie eName = lookupEnv eName ?! (eName <> " is not set")
165+ getEnvOrDieWith eName f = fmap (>>= f) (lookupEnv eName) ?! (eName <> " is not set")
158166 projName <- coerce . pack <$> getEnvOrDie "PROJECT_NAME"
159167 domain <- coerce . pack <$> getEnvOrDie "BASE_DOMAIN"
160168 ns <- coerce . pack <$> getEnvOrDie "NAMESPACE"
161− archRetention <- ArchiveRetention . fromIntegral . read @Int <$> getEnvOrDie "ARCHIVE_RETENTION"
162− stUpdateTimeout <- Timeout . CalendarDiffTime 0 . fromIntegral . read @Int <$> getEnvOrDie "STATUS_UPDATE_TIMEOUT"
169+ archRetention <- fromInteger <$> getEnvOrDieWith "ARCHIVE_RETENTION" readMaybe
170+ stUpdateTimeout <- Timeout . CalendarDiffTime 0 . fromInteger <$> getEnvOrDieWith "STATUS_UPDATE_TIMEOUT" readMaybe
171+ scriptTimeout <- ControlScriptTimeout . fromInteger . maybe (60 * 30) read <$> lookupEnv "CONTROL_SCRIPT_TIMEOUT"
163172 creationCmd <- Command . pack <$> getEnvOrDie "CREATION_COMMAND"
164173 updateCmd <- Command . pack <$> getEnvOrDie "UPDATE_COMMAND"
165174 archiveCmd <- Command . pack <$> getEnvOrDie "ARCHIVE_COMMAND"
166175 checkingCmd <- Command . pack <$> getEnvOrDie "CHECKING_COMMAND"
167176 cleanupCmd <- Command . pack <$> getEnvOrDie "CLEANUP_COMMAND"
168177 archiveCheckingCmd <- Command . pack <$> getEnvOrDie "ARCHIVE_CHECKING_COMMAND"
169− tagCheckingCmd <- Command . pack <$> getEnvOrDie "TAG_CHECKING_COMMAND"
178+ tagCheckingCmd <- Command . pack <$> getEnvOrDie "CONFIG_CHECKING_COMMAND"
170179 infoCmd <- Command . pack <$> getEnvOrDie "INFO_COMMAND"
171− dOverridesCmd <- Command . pack <$> getEnvOrDie "DEPLOYMENT_OVERRIDES_COMMAND"
180+ dOverridesCmd <- Command . pack <$> getEnvOrDie "DEPLOYMENT_CONFIG_COMMAND"
172181 dKeysCmd <- Command . pack <$> getEnvOrDie "DEPLOYMENT_KEYS_COMMAND"
173− aOverridesCmd <- Command . pack <$> getEnvOrDie "APPLICATION_OVERRIDES_COMMAND"
182+ aOverridesCmd <- Command . pack <$> getEnvOrDie "APPLICATION_CONFIG_COMMAND"
174183 aKeysCmd <- Command . pack <$> getEnvOrDie "APPLICATION_KEYS_COMMAND"
175184 unarchiveCmd <- Command . pack <$> getEnvOrDie "UNARCHIVE_COMMAND"
176185 powerAuthorizationHeader <- AuthHeader . BSC.pack <$> getEnvOrDie "POWER_AUTHORIZATION_HEADER"
186+ invalidationTime <- fromInteger . maybe (60 * 60 * 24) read <$> lookupEnv "CACHE_INVALIDATION_TIME"
187+ updateTime <- fromInteger . maybe (60 * 10) read <$> lookupEnv "CACHE_UPDATE_TIME"
188+ isDebug <- (== Just "true") <$> lookupEnv "DEBUG"
189+ isProdLogs <- (/= Just "false") <$> lookupEnv "PROD_LOGS"
190+ let cacheMap :: forall a x. (forall m. AppMConstraints m => x -> m a) -> IO (CacheMap ServerError AppMConstraints x a)
191+ cacheMap = CM.initCacheMap invalidationTime updateTime
192+ decodeCSVDefaultConfig :: BSL.ByteString -> Either String (DefaultConfig l)
193+ decodeCSVDefaultConfig bs = do
194+ x <- C.decode C.NoHeader bs
195+ pure $ DefaultConfig . CT.fromFlatList . V.toList $ x
196+ either500S :: (KatipContext m, MonadError ServerError m) => Either String x -> m x
197+ either500S (Right x) = pure x
198+ either500S (Left err) = do
199+ logLocM ErrorS $ logStr err
200+ throwError err500 {errBody = BSLC.pack err}
177201 notificationCmd <-
178202 (fmap . fmap) (Command . pack) $
179203 lookupEnv "NOTIFICATION_COMMAND" <&> \case
180204 Just "" -> Nothing
181205 x -> x
182− dbPool' <-
183− createPool
184− (fmap (either (error . show) id) . acquire $ unDBConnectionString $ octopodDB opts)
185− release
186− 1
187− 30
188− (unDBPoolSize $ octopodDBPoolSize opts)
189− channel <- liftIO . atomically $ newBroadcastTChan
190− lockedDs <- initLockedDeployments
191− let appSt =
192− AppState
193− dbPool'
194− logger'
195− channel
196− bgWorkersC
197− gracefulShutdownAct
198− shutdownS
199− projName
200− domain
201− ns
202− archRetention
203− stUpdateTimeout
204− creationCmd
205− updateCmd
206− archiveCmd
207− checkingCmd
208− cleanupCmd
209− archiveCheckingCmd
210− tagCheckingCmd
211− infoCmd
212− notificationCmd
213− dOverridesCmd
214− dKeysCmd
215− aOverridesCmd
216− aKeysCmd
217− unarchiveCmd
218− lockedDs
219− app' = app appSt
220− wsApp' = wsApp channel
221− serverPort = octopodPort opts
222− uiServerPort = unServerPort $ octopodUIPort opts
223− powerServerPort = unServerPort serverPort
224− wsServerPort = unServerPort $ octopodWSPort opts
225− powerApp' <- powerApp powerAuthorizationHeader appSt
226− (run uiServerPort app')
227− `race_` (run powerServerPort powerApp')
228− `race_` (run wsServerPort wsApp')
229− `race_` (runStatusUpdater appSt)
230− `race_` (runShutdownHandler appSt)
206+ depOverrideKeysCache' <- cacheMap $ \() -> do
207+ (_, Stdout out, _, _) <- deploymentOverrideKeys >>= runCommandArgs deploymentOverrideKeysCommand
208+ pure $ T.lines out
209+ depOverridesCache' <- cacheMap $ \() -> do
210+ (_, Stdout out, _, _) <- defaultDeploymentOverridesArgs >>= runCommandArgs deploymentOverridesCommand
211+ either500S $ decodeCSVDefaultConfig . BSL.fromStrict . T.encodeUtf8 $ out
212+ appOverrideKeysCache' <- cacheMap $ \cfg -> do
213+ (_, Stdout out, _, _) <- applicationOverrideKeys cfg >>= runCommandArgs applicationOverrideKeysCommand
214+ pure $ T.lines out
215+ appOverridesCache' <- cacheMap $ \cfg -> do
216+ (_, Stdout out, _, _) <- defaultApplicationOverridesArgs cfg >>= runCommandArgs applicationOverridesCommand
217+ either500S $ decodeCSVDefaultConfig . BSL.fromStrict . T.encodeUtf8 $ out
218+ let logConfig =
219+ LogConfig
220+ { project = projName
221+ , debug = isDebug
222+ , prodLogs = isProdLogs
223+ }
224+ runLog logConfig $ do
225+ void $ do
226+ let termHandler = terminationHandler bgWorkersC gracefulShutdownAct shutdownS
227+ installShutdownHandler [sigTERM] termHandler
228+ dbPool' <-
229+ liftBase $
230+ createPool
231+ (fmap (either (error . show) id) . acquire $ unDBConnectionString $ octopodDB opts)
232+ release
233+ 1
234+ 30
235+ (unDBPoolSize $ octopodDBPoolSize opts)
236+ channel <- liftBase . atomically $ newBroadcastTChan
237+ lockedDs <- initLockedDeployments
238+ let appSt =
239+ AppState
240+ { dbPool = dbPool'
241+ , eventSink = channel
242+ , bgWorkersCounter = bgWorkersC
243+ , gracefulShutdownActivated = gracefulShutdownAct
244+ , shutdownSem = shutdownS
245+ , projectName = projName
246+ , baseDomain = domain
247+ , namespace = ns
248+ , statusUpdateTimeout = stUpdateTimeout
249+ , creationCommand = creationCmd
250+ , updateCommand = updateCmd
251+ , archiveCommand = archiveCmd
252+ , checkingCommand = checkingCmd
253+ , cleanupCommand = cleanupCmd
254+ , archiveCheckingCommand = archiveCheckingCmd
255+ , configCheckingCommand = tagCheckingCmd
256+ , infoCommand = infoCmd
257+ , notificationCommand = notificationCmd
258+ , deploymentOverridesCommand = dOverridesCmd
259+ , deploymentOverrideKeysCommand = dKeysCmd
260+ , applicationOverridesCommand = aOverridesCmd
261+ , applicationOverrideKeysCommand = aKeysCmd
262+ , unarchiveCommand = unarchiveCmd
263+ , lockedDeployments = lockedDs
264+ , depOverridesCache = depOverridesCache'
265+ , depOverrideKeysCache = depOverrideKeysCache'
266+ , appOverridesCache = appOverridesCache'
267+ , appOverrideKeysCache = appOverrideKeysCache'
268+ , gitSha = sha
269+ , controlScriptTimeout = scriptTimeout
270+ }
271+ serverPort = octopodPort opts
272+ powerServerPort = unServerPort serverPort
273+ uiServerPort = unServerPort $ octopodUIPort opts
274+ wsServerPort = unServerPort $ octopodWSPort opts
275+ (`runReaderT` appSt) $ do
276+ runServer uiServerPort
277+ `raceM_` runPowerServer powerServerPort powerAuthorizationHeader
278+ `raceM_` runWsServer wsServerPort channel
279+ `raceM_` runStatusUpdater
280+ `raceM_` runShutdownHandler
281+ `raceM_` runArchiveCleanup archRetention
282+
283+raceM_ :: MonadBaseControl IO m => m a -> m b -> m ()
284+raceM_ a b = liftBaseWith $ \runInBase ->
285+ race (runInBase a) (runInBase b) >>= \case
286+ Left x -> void $ restoreM x
287+ Right y -> void $ restoreM y
288+
289+runHasQL ::
290+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m) =>
291+ HasQL.Session a ->
292+ m a
293+runHasQL s = do
294+ p <- asks dbPool
295+ liftBaseOp (withResource p) $ \conn ->
296+ liftBase (HasQL.run s conn) >>= \case
297+ Right x -> pure x
298+ Left err -> do
299+ logLocM ErrorS $ logStr $ displayException err
300+ throwIO err
301+
302+runArchiveCleanup ::
303+ forall m.
304+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m) =>
305+ NominalDiffTime ->
306+ m ()
307+runArchiveCleanup retention = do
308+ isShuttingDown >>= \case
309+ True -> pure ()
310+ False -> do
311+ cutoff <- liftBase getCurrentTime <&> addUTCTime (negate retention)
312+ dNames <- runStatement $
313+ select $ do
314+ ds <- each deploymentSchema
315+ where_ $ (ds ^. #status) ==. litExpr Archived
316+ where_ $ ds ^. #archivedAt <. litExpr (Just cutoff)
317+ pure $ ds ^. #name
318+ for_ dNames $ \dName -> (>>= logLeft) . runExceptT $
319+ runDeploymentBgWorker Nothing dName (pure ()) $ \() -> cleanupDeployment dName
320+ threadDelayMicro (realToFrac $ retention / 10)
321+ runArchiveCleanup retention
322+ where
323+ threadDelayMicro :: Micro -> m ()
324+ threadDelayMicro (MkFixed i) = threadDelay (fromInteger i)
231325
232326runTransaction ::
233− (MonadReader AppState m, MonadBaseControl IO m) =>
327+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m) =>
234328 Transaction a ->
235329 m a
236−runTransaction q = do
237− p <- asks dbPool
238− log <- asks logger
239− let logErr = logWarning log
240− handleException e = do
241− liftBase $ logErr . T.pack $ displayException e
242− throwIO e
243− withResource p $ \conn ->
244− liftBase $ HasQL.run (transaction Serializable Write q) conn >>= either handleException pure
330+runTransaction q = katipAddNamespace "transaction" $ runHasQL $ transaction Serializable Write q
245331
246332runStatement ::
247− (MonadReader AppState m, MonadBaseControl IO m) =>
333+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m) =>
248334 Statement () a ->
249335 m a
250−runStatement q = do
251− p <- asks dbPool
252− log <- asks logger
253− let logErr = logWarning log
254− handleException e = do
255− liftBase $ logErr . T.pack $ displayException e
256− throwIO e
257− withResource p $ \conn ->
258− liftBase $ HasQL.run (HasQL.statement () q) conn >>= either handleException pure
259−
260−-- | Helper to run the server.
261−nt :: AppState -> AppM a -> Handler a
262−nt s x = runReaderT x s
263−
264−-- | Application with the Web UI API.
265−app :: AppState -> Application
266−app s = serve api $ hoistServer api (nt s) server
267− where
268− api = Proxy @API
336+runStatement q = katipAddNamespace "statement" $ runHasQL $ HasQL.statement () q
337+
338+runApp ::
339+ forall api context m.
340+ ( HasServer api context
341+ , KatipContext m
342+ , MonadBase IO m
343+ , MonadReader AppState m
344+ , HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters
345+ ) =>
346+ Port ->
347+ Proxy api ->
348+ Servant.Context context ->
349+ ServerT api AppM ->
350+ m ()
351+runApp portNum api ctx srv = do
352+ appstate <- ask
353+ logEnv <- getLogEnv
354+ logCtx <- getKatipContext
355+ ns <- getKatipNamespace
356+ let hoist :: AppM a -> Handler a
357+ hoist act = runKatipContextT logEnv logCtx ns $ runReaderT act appstate
358+ log req respStatus mSize = runKatipContextT logEnv logCtx ns $ do
359+ logLocM DebugS $ show' req <> " " <> show' respStatus <> " " <> show' mSize
360+ liftBase $
361+ runSettings (setPort portNum $ setLogger log defaultSettings) $
362+ serveWithContext api ctx $ hoistServerWithContext api (Proxy @context) hoist srv
269363
270364-- | Request handlers of the Web UI API application.
271365server :: ServerT API AppM
@@ -283,51 +377,37 @@ server =
283377 :<|> pingH
284378 :<|> projectNameH
285379
286−decodeCSVDefaultConfig :: BSL.ByteString -> Either String (DefaultConfig l)
287−decodeCSVDefaultConfig bs = do
288− x <- C.decode C.NoHeader bs
289− pure $ DefaultConfig . OM.fromList . V.toList $ x
380+lookupCache :: (Ord x, AppMConstraints m) => (AppState -> CacheMap ServerError AppMConstraints x y) -> x -> m y
381+lookupCache f x = do
382+ cm <- asks f
383+ CM.lookupBlocking cm x
290384
291−either500S :: MonadError ServerError m => Either String x -> m x
292−either500S (Right x) = pure x
293−either500S (Left err) = throwError err500 {errBody = BSLC.pack err}
385+defaultDeploymentKeysH :: AppMConstraints m => m [Text]
386+defaultDeploymentKeysH = lookupCache depOverrideKeysCache ()
294387
295−defaultDeploymentKeysH :: AppM [Text]
296−defaultDeploymentKeysH = do
297− (_, Stdout out, _) <- deploymentOverrideKeys >>= runCommandArgs deploymentOverrideKeysCommand
298− pure $ T.lines out
388+defaultDeploymentOverridesH :: AppMConstraints m => m (DefaultConfig 'DeploymentLevel)
389+defaultDeploymentOverridesH = lookupCache depOverridesCache ()
299390
300−defaultDeploymentOverridesH :: AppM (DefaultConfig 'DeploymentLevel)
301−defaultDeploymentOverridesH = do
302− (_, Stdout out, _) <- defaultDeploymentOverridesArgs >>= runCommandArgs deploymentOverridesCommand
303− either500S $ decodeCSVDefaultConfig . BSL.fromStrict . T.encodeUtf8 $ out
391+defaultApplicationKeysH :: AppMConstraints m => Config 'DeploymentLevel -> m [Text]
392+defaultApplicationKeysH = lookupCache appOverrideKeysCache
304393
305−defaultApplicationKeysH :: Config 'DeploymentLevel -> AppM [Text]
306−defaultApplicationKeysH cfg = do
307− (_, Stdout out, _) <- applicationOverrideKeys cfg >>= runCommandArgs applicationOverrideKeysCommand
308− pure $ T.lines out
394+defaultApplicationOverridesH :: AppMConstraints m => Config 'DeploymentLevel -> m (DefaultConfig 'ApplicationLevel)
395+defaultApplicationOverridesH = lookupCache appOverridesCache
309396
310−defaultApplicationOverridesH :: Config 'DeploymentLevel -> AppM (DefaultConfig 'ApplicationLevel)
311−defaultApplicationOverridesH cfg = do
312− (_, Stdout out, _) <- defaultApplicationOverridesArgs cfg >>= runCommandArgs applicationOverridesCommand
313− either500S $ decodeCSVDefaultConfig . BSL.fromStrict . T.encodeUtf8 $ out
397+runServer :: (KatipContext m, MonadBase IO m, MonadReader AppState m) => Port -> m ()
398+runServer portNum = katipAddNamespace "app" $ runApp portNum (Proxy @API) EmptyContext server
314399
315400-- | Application with the octo CLI API.
316−powerApp :: AuthHeader -> AppState -> IO Application
317−powerApp h s = do
318− jwk' <- liftIO $ genJWK (RSAGenParam (4096 `div` 8))
401+runPowerServer :: (KatipContext m, MonadBase IO m, MonadReader AppState m) => Port -> AuthHeader -> m ()
402+runPowerServer portNum h = katipAddNamespace "powerApp" $ do
403+ jwk' <- liftBase $ genJWK (RSAGenParam (4096 `div` 8))
319404 let ctx :: Servant.Context Ctx
320405 ctx =
321406 h
322407 :. (defaultJWTSettings jwk' :: JWTSettings)
323408 :. (defaultCookieSettings :: CookieSettings)
324409 :. EmptyContext
325−
326− return $
327− serveWithContext api ctx $
328− hoistServerWithContext api (Proxy @Ctx) (nt s) powerServer
329− where
330− api = Proxy @PowerAPI
410+ runApp portNum (Proxy @PowerAPI) ctx powerServer
331411
332412type Ctx = '[AuthHeader, JWTSettings, CookieSettings]
333413
@@ -340,16 +420,16 @@ powerServer (Authenticated ()) =
340420 :<|> statusH
341421 :<|> cleanupH
342422 :<|> restoreH
423+ :<|> deleteH
343424 )
344425 :<|> getActionInfoH
345− :<|> cleanArchiveH
346426powerServer _ = throwAll err401
347427
348428-- | Application with the WS API.
349−wsApp :: TChan WSEvent -> Application
350−wsApp channel = serve api $ wsServer channel
351− where
352− api = Proxy @WebSocketAPI
429+runWsServer :: (KatipContext m, MonadBase IO m, MonadReader AppState m) => Port -> TChan WSEvent -> m ()
430+runWsServer portNum channel =
431+ katipAddNamespace "wsApp" $
432+ runApp portNum (Proxy @WebSocketAPI) EmptyContext $ wsServer channel
353433
354434-- | Request handlers of the application with the WS API.
355435wsServer :: TChan WSEvent -> Server WebSocketAPI
@@ -373,7 +453,7 @@ powerListH = getFullInfo
373453
374454-- | Handles the 'full_info' request of the Web UI API.
375455fullInfoH ::
376− (MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) =>
456+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) =>
377457 DeploymentName ->
378458 m DeploymentFullInfo
379459fullInfoH dName = do
@@ -382,7 +462,7 @@ fullInfoH dName = do
382462 Nothing ->
383463 throwError
384464 err404
385− { errBody = validationError ["Name not found"] []
465+ { errBody = validationError ["Name not found"]
386466 }
387467
388468-- | Handles the 'full_info' request of the octo CLI API.
@@ -390,27 +470,27 @@ powerFullInfoH :: DeploymentName -> AppM DeploymentFullInfo
390470powerFullInfoH = fullInfoH
391471
392472getSingleFullInfo ::
393− (MonadReader AppState m, MonadBaseControl IO m) =>
473+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m) =>
394474 DeploymentName ->
395475 m (Maybe DeploymentFullInfo)
396476getSingleFullInfo dName = do
397− AppState {logger = l} <- ask
398477 deploymentsSchema <- runStatement . select $ do
399478 d <- each deploymentSchema
400479 where_ $ d ^. #name ==. litExpr dName
401480 pure d
402− deployments <- forM deploymentsSchema extractDeploymentFullInfo
403− liftBase . logInfo l $ "get deployments: " <> (pack . show $ deployments)
481+ deployments <- for deploymentsSchema extractDeploymentFullInfo
482+ logLocM DebugS $ "get deployments: " <> show' deployments
404483 return $ listToMaybe deployments
405484
406485getFullInfo ::
407− (MonadReader AppState m, MonadBaseControl IO m) =>
486+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m) =>
408487 m [DeploymentFullInfo]
409488getFullInfo = do
410− AppState {logger = l} <- ask
411− deploymentsSchema <- runStatement . select $ each deploymentSchema
412− deployments <- forM deploymentsSchema extractDeploymentFullInfo
413− liftBase . logInfo l $ "get deployments: " <> (pack . show $ deployments)
489+ deploymentsSchema <-
490+ runStatement . select . orderBy (view #updatedAt >$< desc) $
491+ each deploymentSchema
492+ deployments <- for deploymentsSchema extractDeploymentFullInfo
493+ logLocM DebugS $ "get deployments: " <> show' deployments
414494 return deployments
415495
416496-- | Handles the 'create' request.
@@ -423,9 +503,8 @@ createH dep = do
423503 \under 17 characters and begin with a letter."
424504 throwError
425505 err400
426− { errBody = validationError [badNameText] []
506+ { errBody = validationError [badNameText]
427507 }
428− t1 <- liftBase getCurrentTime
429508 failIfImageNotFound dep
430509 failIfGracefulShutdownActivated
431510 runDeploymentBgWorker
@@ -433,7 +512,7 @@ createH dep = do
433512 (dep ^. #name)
434513 ( do
435514 res <-
436− L.try $
515+ try $
437516 runStatement $
438517 insert
439518 Insert
@@ -443,13 +522,12 @@ createH dep = do
443522 [ DeploymentSchema
444523 { id_ = unsafeDefault
445524 , name = litExpr $ dep ^. #name
446− , tag = litExpr $ dep ^. #tag
447525 , appOverrides = litExpr $ dep ^. #appOverrides
448526 , deploymentOverrides = litExpr $ dep ^. #deploymentOverrides
449527 , createdAt = now
450528 , updatedAt = now
451529 , archivedAt = litExpr Nothing
452− , status = litExpr Running
530+ , status = litExpr CreatePending
453531 , statusUpdatedAt = now
454532 , checkedAt = now
455533 , metadata = litExpr (DeploymentMetadata [])
@@ -464,94 +542,76 @@ createH dep = do
464542 | code == unique_violation ->
465543 throwError
466544 err400
467− { errBody = validationError ["Deployment already exists"] []
545+ { errBody = validationError ["Deployment already exists"]
468546 }
469547 Left _ ->
470548 throwError err409 {errBody = appError "Some database error"}
471549 )
472550 $ \() -> do
473− st <- ask
474− liftBase $ sendReloadEvent st
551+ sendReloadEvent
475552 updateDeploymentInfo (dep ^. #name)
476− (ec, out, err) <- createDeployment dep
477− t2 <- liftBase getCurrentTime
478− let elTime = elapsedTime t2 t1
553+ (ec, out, err, elTime) <- createDeployment dep
479554 -- calling it directly now is fine since there is no previous status.
480555 createDeploymentLog dep CreateAction ec elTime out err
481− liftBase $ sendReloadEvent st
482− liftBase $ handleExitCode ec
556+ sendReloadEvent
557+ handleExitCode ec
483558 pure Success
484559
485560-- | Updates deployment info.
486561updateDeploymentInfo ::
487− (MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) =>
562+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) =>
488563 DeploymentName ->
489564 m ()
490565updateDeploymentInfo dName = do
491− log <- asks (logWarning . logger)
492566 DeploymentFullInfo {deployment = dep} <- getDeploymentS dName
493− dc <- getDefaultConfig dName
494− (ec, out, err) <- runCommandArgs infoCommand =<< infoCommandArgs dc dep
567+ cfg <- getDeploymentConfig dep
568+ (ec, out, err, _) <- runCommandArgs infoCommand =<< infoCommandArgs cfg dep
495569 case ec of
496570 ExitSuccess -> case parseDeploymentMetadata (unStdout out) of
497571 Right dMeta -> upsertDeploymentMetadatum dName dMeta
498− Left err' -> liftBase $ log $ "could not get deployment info, could not parse CSV: " <> T.pack err'
572+ Left err' -> logLocM ErrorS $ "could not get deployment info, could not parse CSV: " <> logStr err'
499573 ExitFailure _ ->
500− liftBase $
501− log $
502− "could not get deployment info, exit code: " <> (pack . show $ ec)
503− <> ", stdout: "
504− <> coerce out
505− <> "stderr: "
506− <> coerce err
507−
508−getDefaultConfig :: Monad m => DeploymentName -> m FullDefaultConfig
509−getDefaultConfig _ =
574+ logLocM ErrorS $
575+ "could not get deployment info, exit code: " <> show' ec
576+ <> ", stdout: "
577+ <> logStr (unStdout out)
578+ <> "stderr: "
579+ <> logStr (unStderr err)
580+
581+getDeploymentConfig :: AppMConstraints m => Deployment -> m FullConfig
582+getDeploymentConfig dep = do
583+ depCfg <- applyOverrides (dep ^. #deploymentOverrides) <$> defaultDeploymentOverridesH
584+ appCfg <- applyOverrides (dep ^. #appOverrides) <$> defaultApplicationOverridesH depCfg
510585 pure
511− FullDefaultConfig
512− { appDefaultConfig = DefaultConfig OM.empty
513− , depDefaultConfig = DefaultConfig OM.empty
586+ FullConfig
587+ { appConfig = appCfg
588+ , depConfig = depCfg
514589 }
515590
516591-- | Helper to create a new deployment.
517592createDeployment ::
518− (MonadBaseControl IO m, MonadReader AppState m) =>
593+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m, MonadError ServerError m) =>
519594 Deployment ->
520− m (ExitCode, Stdout, Stderr)
595+ m (ExitCode, Stdout, Stderr, Duration)
521596createDeployment dep = do
522− st <- ask
523− dCfg <- getDefaultConfig $ dep ^. #name
524− let log :: Text -> IO ()
525− log = logInfo (logger st)
526− args =
527− ControlScriptArgs
528− [ "--project-name"
529− , T.unpack . coerce $ projectName st
530− , "--base-domain"
531− , T.unpack . coerce $ baseDomain st
532− , "--namespace"
533− , T.unpack . coerce $ namespace st
534− , "--name"
535− , T.unpack . coerce $ dep ^. #name
536− , "--tag"
537− , T.unpack . coerce $ dep ^. #tag
538− ]
539− <> fullConfigArgs dCfg dep
540− (ec, out, err) <- runCommandArgs creationCommand args
541− liftBase . log $ "deployment created, deployment: " <> (pack . show $ dep)
542− pure (ec, out, err)
597+ cfg <- getDeploymentConfig dep
598+ res <- runCommandArgs creationCommand =<< createCommandArgs cfg dep
599+ logLocM InfoS $ "deployment created, deployment: " <> show' dep
600+ pure res
543601
544602-- | Helper to get deployment logs.
545603selectDeploymentLogs ::
546− (MonadBaseControl IO m, MonadReader AppState m) =>
604+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m) =>
547605 DeploymentName ->
548606 m [DeploymentLog]
549−selectDeploymentLogs dName = (fmap . fmap) extractDeploymentLog . runStatement . select $ do
550− dls <- each deploymentLogSchema
551− ds <- each deploymentSchema
552− where_ $ ds ^. #name ==. litExpr dName
553− where_ $ dls ^. #deploymentId ==. ds ^. #id_
554− pure dls
607+selectDeploymentLogs dName = (fmap . fmap) extractDeploymentLog . runStatement . select
608+ . orderBy (view #createdAt >$< desc)
609+ $ do
610+ dls <- each deploymentLogSchema
611+ ds <- each deploymentSchema
612+ where_ $ ds ^. #name ==. litExpr dName
613+ where_ $ dls ^. #deploymentId ==. ds ^. #id_
614+ pure dls
555615
556616data StatusTransitionProcessOutput = StatusTransitionProcessOutput
557617 { exitCode :: ExitCode
@@ -569,6 +629,7 @@ data DeploymentStatusTransition
569629 | TransitionArchivePending StatusTransitionProcessOutput
570630 | TransitionUpdatePending StatusTransitionProcessOutput
571631 | TransitionCreatePending StatusTransitionProcessOutput
632+ | TransitionCleanupFailed StatusTransitionProcessOutput
572633 | TransitionFailure FailureType
573634 deriving stock (Show)
574635
@@ -580,6 +641,7 @@ transitionStatus TransitionRestore {} = CreatePending
580641transitionStatus TransitionArchivePending {} = ArchivePending
581642transitionStatus TransitionUpdatePending {} = UpdatePending
582643transitionStatus TransitionCreatePending {} = CreatePending
644+transitionStatus TransitionCleanupFailed {} = CleanupFailed
583645transitionStatus (TransitionFailure t) = Failure t
584646
585647processOutput :: DeploymentStatusTransition -> Maybe (StatusTransitionProcessOutput, Action)
@@ -590,10 +652,11 @@ processOutput (TransitionRestore x) = Just (x, RestoreAction)
590652processOutput (TransitionArchivePending x) = Just (x, ArchiveAction)
591653processOutput (TransitionUpdatePending x) = Just (x, UpdateAction)
592654processOutput (TransitionCreatePending x) = Just (x, CreateAction)
655+processOutput (TransitionCleanupFailed x) = Just (x, CleanupAction)
593656processOutput TransitionFailure {} = Nothing
594657
595658transitionToStatusS ::
596− (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m) =>
659+ (KatipContext m, MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m) =>
597660 DeploymentName ->
598661 DeploymentStatusTransition ->
599662 m ()
@@ -627,7 +690,7 @@ transitionErrorToServerError = \case
627690 show'' = BSLC.pack . show
628691
629692getDeploymentS ::
630− (MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) =>
693+ (KatipContext m, MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) =>
631694 DeploymentName ->
632695 m DeploymentFullInfo
633696getDeploymentS dName =
@@ -636,15 +699,13 @@ getDeploymentS dName =
636699 Nothing -> throwError err404 {errBody = "Deployment not found."}
637700
638701transitionToStatus ::
639− (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m) =>
702+ (KatipContext m, MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m) =>
640703 DeploymentName ->
641704 DeploymentStatusTransition ->
642705 ExceptT StatusTransitionError m ()
643706transitionToStatus dName s = do
644− st <- ask
645− let log = liftBase . logInfo (logger st)
646− newS = transitionStatus s
647− log $ "Transitioning deployment " <> (show' . unDeploymentName) dName <> " " <> show' s
707+ let newS = transitionStatus s
708+ logLocM InfoS $ "Transitioning deployment " <> show' (unDeploymentName dName) <> " " <> show' s
648709 res <- runTransaction . runExceptT $ do
649710 oldS <-
650711 (lift . statement () . select)
@@ -669,6 +730,7 @@ transitionToStatus dName s = do
669730 dep
670731 & #status .~ litExpr newS
671732 & #updatedAt .~ now
733+ & #statusUpdatedAt .~ now
672734 & if newS == Archived then #archivedAt .~ nullify now else id
673735 , updateWhere = \() dep -> dep ^. #name ==. litExpr dName
674736 , returning = Projection id
@@ -677,7 +739,7 @@ transitionToStatus dName s = do
677739 (deps, oldS) <- either throwError pure res
678740 dep <- lift $ ensureOne deps
679741 lift $
680− forM_ (processOutput s) $ \(output, act) ->
742+ for_ (processOutput s) $ \(output, act) ->
681743 createDeploymentLog
682744 dep
683745 act
@@ -686,11 +748,11 @@ transitionToStatus dName s = do
686748 (output ^. #stdout)
687749 (output ^. #stderr)
688750 notificationCmd <- asks notificationCommand
689− forM_ notificationCmd $ \nCmd ->
751+ for_ notificationCmd $ \nCmd ->
690752 runBgWorker . void $
691753 runCommandArgs' nCmd
692− =<< notificationCommandArgs dName (dep ^. #tag) oldS newS
693− liftBase $ sendReloadEvent st
754+ =<< notificationCommandArgs dName oldS newS
755+ sendReloadEvent
694756
695757assertStatusTransitionPossible ::
696758 MonadError StatusTransitionError m =>
@@ -704,7 +766,7 @@ assertStatusTransitionPossible dName old new =
704766 (throwError $ InvalidStatusTransition dName old new)
705767
706768assertDeploymentTransitionPossible ::
707− (MonadError StatusTransitionError m, MonadReader AppState m, MonadBaseControl IO m) =>
769+ (KatipContext m, MonadError StatusTransitionError m, MonadReader AppState m, MonadBaseControl IO m) =>
708770 DeploymentName ->
709771 DeploymentStatus ->
710772 m ()
@@ -716,7 +778,7 @@ assertDeploymentTransitionPossible dName new = do
716778 assertStatusTransitionPossible dName (recordedStatus $ dep ^. #status) new
717779
718780assertDeploymentTransitionPossibleS ::
719− (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m) =>
781+ (KatipContext m, MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m) =>
720782 DeploymentName ->
721783 DeploymentStatus ->
722784 m ()
@@ -732,9 +794,10 @@ possibleTransitions :: DeploymentStatus -> DeploymentStatus -> Bool
732794possibleTransitions CreatePending = anyPred [is #_Running, is #_Failure]
733795possibleTransitions Running = anyPred [is #_Failure, is #_ArchivePending, is #_UpdatePending]
734796possibleTransitions UpdatePending = anyPred [is #_Failure, is #_Running]
735−possibleTransitions (Failure _) = anyPred [is #_UpdatePending, is #_Running, is #_ArchivePending]
797+possibleTransitions (Failure _) = anyPred [is #_UpdatePending, is #_Running, is #_ArchivePending, is #_CleanupFailed]
736798possibleTransitions ArchivePending = anyPred [is #_ArchivePending, is #_Archived]
737−possibleTransitions Archived = anyPred [is #_CreatePending]
799+possibleTransitions Archived = anyPred [is #_CreatePending, is #_CleanupFailed]
800+possibleTransitions CleanupFailed = const False
738801
739802anyPred :: [a -> Bool] -> a -> Bool
740803anyPred preds x = any ($ x) preds
@@ -743,25 +806,10 @@ anyPred preds x = any ($ x) preds
743806archiveH :: DeploymentName -> AppM CommandResponse
744807archiveH dName = do
745808 failIfGracefulShutdownActivated
746− t1 <- liftBase getCurrentTime
747− st <- ask
748− let log = liftBase . logInfo (logger st)
749− args =
750− [ "--project-name"
751− , coerce $ projectName st
752− , "--base-domain"
753− , coerce $ baseDomain st
754− , "--namespace"
755− , coerce $ namespace st
756− , "--name"
757− , coerce dName
758− ]
759− cmd = coerce $ archiveCommand st
760809 runDeploymentBgWorker (Just ArchivePending) dName (pure ()) $ \() -> do
761− log $ "call " <> unwords (cmd : args)
762− (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args)
763− t2 <- liftBase getCurrentTime
764− let elTime = elapsedTime t2 t1
810+ (view #deployment -> dep) <- getDeploymentS dName
811+ cfg <- getDeploymentConfig dep
812+ (ec, out, err, elTime) <- runCommandArgs archiveCommand =<< archiveCommandArgs cfg dep
765813 transitionToStatusS dName $
766814 TransitionArchivePending
767815 StatusTransitionProcessOutput
@@ -777,9 +825,6 @@ archiveH dName = do
777825updateH :: DeploymentName -> DeploymentUpdate -> AppM CommandResponse
778826updateH dName dUpdate = do
779827 failIfGracefulShutdownActivated
780− t1 <- liftIO getCurrentTime
781− st <- ask
782− let log = logInfo (logger st)
783828 olDep <- getDeploymentS dName <&> (^. #deployment)
784829 failIfImageNotFound
785830 ( olDep
@@ -798,7 +843,6 @@ updateH dName dUpdate = do
798843 , set = \() ds ->
799844 ds & #appOverrides .~ litExpr (dUpdate ^. #appOverrides)
800845 & #deploymentOverrides .~ litExpr (dUpdate ^. #deploymentOverrides)
801− & #tag .~ litExpr (dUpdate ^. #newTag)
802846 , updateWhere = \() ds -> ds ^. #name ==. litExpr dName
803847 , returning = Projection id
804848 }
@@ -806,30 +850,13 @@ updateH dName dUpdate = do
806850 >>= ensureOne
807851
808852 updateDeploymentInfo dName
809− liftBase $ sendReloadEvent st
810− dCfg <- getDefaultConfig dName
811− let args =
812− ControlScriptArgs
813− [ "--project-name"
814− , T.unpack . coerce $ projectName st
815− , "--base-domain"
816− , T.unpack . coerce $ baseDomain st
817− , "--namespace"
818− , T.unpack . coerce $ namespace st
819− , "--name"
820− , T.unpack . coerce $ dName
821− , "--tag"
822− , T.unpack . coerce $ dep ^. #tag
823− ]
824− <> fullConfigArgs dCfg dep
825− (ec, out, err) <- runCommandArgs updateCommand args
826− liftBase . log $
827− "deployment updated, name: "
828− <> coerce dName
829− <> ", tag: "
830− <> coerce (dep ^. #tag)
831− t2 <- liftBase getCurrentTime
832− let elTime = elapsedTime t2 t1
853+ sendReloadEvent
854+ cfg <- getDeploymentConfig dep
855+ (ec, out, err, elTime) <- runCommandArgs updateCommand =<< updateCommandArgs cfg dep
856+ logLocM DebugS $
857+ logStr $
858+ "deployment updated, name: "
859+ <> unDeploymentName dName
833860 transitionToStatusS dName $
834861 TransitionUpdatePending
835862 StatusTransitionProcessOutput
@@ -844,19 +871,15 @@ updateH dName dUpdate = do
844871-- | Handles the 'info' request of the Web UI API.
845872infoH :: DeploymentName -> AppM [DeploymentInfo]
846873infoH dName = do
847− st <- ask
848874 dInfo <- getInfo dName
849− liftIO . logInfo (logger st) $
850− "get deployment info: " <> (pack . show $ dInfo)
875+ logLocM DebugS $ "get deployment info: " <> show' dInfo
851876 pure [dInfo]
852877
853878-- | Handles the 'info' request of the octo CLI API.
854879powerInfoH :: DeploymentName -> AppM [DeploymentInfo]
855880powerInfoH dName = do
856− st <- ask
857881 dInfo <- getInfo dName
858− liftIO . logInfo (logger st) $
859− "get deployment info: " <> (pack . show $ dInfo)
882+ logLocM DebugS $ "get deployment info: " <> show' dInfo
860883 pure [dInfo]
861884
862885-- | Helper to get deployment info from the database.
@@ -872,10 +895,10 @@ getInfo dName = do
872895 }
873896
874897-- | Handles the 'ping' request.
875−pingH :: AppM NoContent
898+pingH :: AppM Text
876899pingH = do
877900 _ <- runStatement $ select $ pure $ litExpr True
878− pure NoContent
901+ asks gitSha
879902
880903-- | Handles the 'project_name' request.
881904projectNameH :: AppM ProjectName
@@ -884,9 +907,9 @@ projectNameH = asks projectName
884907-- | Handles the 'status' request.
885908statusH :: DeploymentName -> AppM CurrentDeploymentStatus
886909statusH dName = do
887− dep <- getDeploymentS dName
888− dCfg <- getDefaultConfig dName
889− (ec, _, _) <- runCommandArgs checkingCommand =<< checkCommandArgs dCfg (dep ^. #deployment)
910+ (view #deployment -> dep) <- getDeploymentS dName
911+ cfg <- getDeploymentConfig dep
912+ (ec, _, _, _) <- runCommandArgs checkingCommand =<< checkCommandArgs cfg dep
890913 pure . CurrentDeploymentStatus $
891914 case ec of
892915 ExitSuccess -> Ok
@@ -899,37 +922,55 @@ cleanupH dName = do
899922 runDeploymentBgWorker Nothing dName (pure ()) $ \() -> cleanupDeployment dName
900923 pure Success
901924
925+-- | Deletes the deployment from the DB.
926+deleteH :: DeploymentName -> AppM CommandResponse
927+deleteH dName = do
928+ failIfGracefulShutdownActivated
929+ runDeploymentBgWorker Nothing dName (pure ()) $ \() -> deleteAllDeployment dName
930+ pure Success
931+
902932-- | Helper to cleanup deployment.
903933cleanupDeployment ::
904− (MonadBaseControl IO m, MonadReader AppState m) =>
934+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m, MonadError ServerError m) =>
905935 DeploymentName ->
906936 m ()
907937cleanupDeployment dName = do
908− st <- ask
909− let log = logInfo (logger st)
910− args =
911− [ "--project-name"
912− , coerce $ projectName st
913− , "--base-domain"
914− , coerce $ baseDomain st
915− , "--namespace"
916− , coerce $ namespace st
917− , "--name"
918− , coerce dName
919− ]
920− cmd = coerce $ cleanupCommand st
921− liftBase . log $ "call " <> unwords (cmd : args)
922− (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args)
923− liftBase $ print out >> print err
938+ (view #deployment -> dep) <- getDeploymentS dName
939+ cfg <- getDeploymentConfig dep
940+ (ec, out, err, elTime) <- runCommandArgs cleanupCommand =<< cleanupCommandArgs cfg dep
941+ katipAddContext (FilePayload "stdout" $ T.encodeUtf8 $ unStdout out) $
942+ katipAddContext (FilePayload "stdout" $ T.encodeUtf8 $ unStderr err) $
943+ logLocM DebugS "deployment destroyed"
944+ case ec of
945+ ExitSuccess -> do
946+ deleteDeploymentLogs dName
947+ deleteDeployment dName
948+ logLocM InfoS $ logStr $ "deployment destroyed, name: " <> unDeploymentName dName
949+ ExitFailure _ -> do
950+ transitionToStatusS dName $
951+ TransitionCleanupFailed
952+ StatusTransitionProcessOutput
953+ { exitCode = ec
954+ , duration = elTime
955+ , stdout = out
956+ , stderr = err
957+ }
958+ pure ()
959+ sendReloadEvent
960+ handleExitCode ec
961+
962+deleteAllDeployment ::
963+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m) =>
964+ DeploymentName ->
965+ m ()
966+deleteAllDeployment dName = do
924967 deleteDeploymentLogs dName
925968 deleteDeployment dName
926− liftBase $ log $ "deployment destroyed, name: " <> coerce dName
927− liftBase $ sendReloadEvent st
928− handleExitCode ec
969+ logLocM InfoS $ logStr $ "deployment destroyed, name: " <> unDeploymentName dName
929970
930971-- | Helper to delete deployment logs.
931972deleteDeploymentLogs ::
932− (MonadBaseControl IO m, MonadReader AppState m) =>
973+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m) =>
933974 DeploymentName ->
934975 m ()
935976deleteDeploymentLogs dName =
@@ -944,7 +985,7 @@ deleteDeploymentLogs dName =
944985
945986-- | Helper to delete a deployment.
946987deleteDeployment ::
947− (MonadBaseControl IO m, MonadReader AppState m) =>
988+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m) =>
948989 DeploymentName ->
949990 m ()
950991deleteDeployment dName =
@@ -957,40 +998,18 @@ deleteDeployment dName =
957998 , returning = pure ()
958999 }
9591000
960−-- | Handles the 'clean-archive' request.
961−cleanArchiveH :: AppM CommandResponse
962−cleanArchiveH = do
963− failIfGracefulShutdownActivated
964− st <- ask
965− let archRetention = unArchiveRetention . archiveRetention $ st
966− cutoff <- liftBase getCurrentTime <&> addUTCTime (negate archRetention)
967− dNames <- runStatement $
968− select $ do
969− ds <- each deploymentSchema
970− where_ $ (ds ^. #status) `in_` (litExpr <$> archivedStatuses)
971− where_ $ ds ^. #archivedAt <. litExpr (Just cutoff)
972− pure $ ds ^. #name
973− runBgWorker . void $
974− for dNames $ \dName ->
975− runDeploymentBgWorker Nothing dName (pure ()) $ \() -> cleanupDeployment dName
976−
977− pure Success
978−
9791001-- | Handles the 'restore' request.
9801002restoreH :: DeploymentName -> AppM CommandResponse
9811003restoreH dName = do
9821004 failIfGracefulShutdownActivated
983− t1 <- liftBase getCurrentTime
9841005 dep <- getDeploymentS dName
9851006 failIfImageNotFound $ dep ^. #deployment
9861007 failIfGracefulShutdownActivated
9871008 runDeploymentBgWorker (Just CreatePending) dName (pure ()) $ \() -> do
988− dep' <- getDeploymentS dName
1009+ (view #deployment -> dep') <- getDeploymentS dName
9891010 updateDeploymentInfo dName
990− dCfg <- getDefaultConfig dName
991− (ec, out, err) <- runCommandArgs unarchiveCommand =<< unarchiveCommandArgs dCfg (dep' ^. #deployment)
992− t2 <- liftBase getCurrentTime
993− let elTime = elapsedTime t2 t1
1011+ cfg <- getDeploymentConfig dep'
1012+ (ec, out, err, elTime) <- runCommandArgs unarchiveCommand =<< unarchiveCommandArgs cfg dep'
9941013 transitionToStatusS dName $
9951014 TransitionRestore
9961015 StatusTransitionProcessOutput
@@ -1016,13 +1035,13 @@ getActionInfoH aId = do
10161035 throwError err400 {errBody = appError "Action not found"}
10171036
10181037-- | Helper to handle exit code.
1019−handleExitCode :: MonadBaseControl IO m => ExitCode -> m ()
1038+handleExitCode :: MonadBase IO m => ExitCode -> m ()
10201039handleExitCode ExitSuccess = return ()
1021−handleExitCode (ExitFailure c) = liftBase . throwIO $ DeploymentFailed c
1040+handleExitCode (ExitFailure c) = throwIO $ DeploymentFailed c
10221041
10231042-- | Helper to log a deployment action.
10241043createDeploymentLog ::
1025− (MonadBaseControl IO m, MonadReader AppState m) =>
1044+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m) =>
10261045 Deployment ->
10271046 Action ->
10281047 ExitCode ->
@@ -1047,7 +1066,6 @@ createDeploymentLog dep act ec dur out err = do
10471066 { actionId = unsafeDefault
10481067 , deploymentId = litExpr dId
10491068 , action = litExpr act
1050− , deploymentTag = litExpr $ dep ^. #tag
10511069 , exitCode = litExpr $ case ec of
10521070 ExitSuccess -> 0
10531071 ExitFailure errCode -> fromIntegral errCode
@@ -1070,7 +1088,7 @@ ensureOne _ = throwError err500
10701088
10711089-- | Helper to insert or update deployment metadata.
10721090upsertDeploymentMetadatum ::
1073− (MonadBaseControl IO m, MonadReader AppState m) =>
1091+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m) =>
10741092 DeploymentName ->
10751093 DeploymentMetadata ->
10761094 m ()
@@ -1089,60 +1107,58 @@ upsertDeploymentMetadatum dName dMetadata =
10891107-- Returns 404 'Tag not found' response if the deployment tag doesn't exist.
10901108failIfImageNotFound :: Deployment -> AppM ()
10911109failIfImageNotFound dep = do
1092− dCfg <- getDefaultConfig $ dep ^. #name
1093− (ec, _, _) <- runCommandArgs tagCheckingCommand =<< tagCheckCommandArgs dCfg dep
1110+ cfg <- getDeploymentConfig dep
1111+ (ec, Stdout out, _, _) <- runCommandArgs configCheckingCommand =<< configCheckCommandArgs cfg dep
10941112 case ec of
10951113 ExitSuccess -> pure ()
10961114 ExitFailure _ ->
1097− throwError err400 {errBody = validationError [] ["Tag not found"]}
1115+ throwError err400 {errBody = BSL.fromStrict $ T.encodeUtf8 out}
10981116
10991117-- | Helper to create an application-level error.
11001118appError :: Text -> BSL.ByteString
11011119appError = encode . AppError
11021120
11031121-- | Helper to create a validation-level error.
1104−validationError :: [Text] -> [Text] -> BSL.ByteString
1105−validationError nameErrors tagErrors =
1106− encode $ ValidationError nameErrors tagErrors
1122+validationError :: [Text] -> BSL.ByteString
1123+validationError nameErrors =
1124+ encode $ ValidationError nameErrors
11071125
11081126-- | Helper to send an event to the WS event channel.
1109−sendReloadEvent :: AppState -> IO ()
1110−sendReloadEvent state =
1111− atomically $ writeTChan (eventSink state) FrontendPleaseUpdateEverything
1112−
1113−-- | Returns time delta between 2 timestamps.
1114−elapsedTime :: UTCTime -> UTCTime -> Duration
1115−elapsedTime t1 t2 = Duration . calendarTimeTime $ t2 `diffUTCTime` t1
1127+sendReloadEvent :: (MonadBase IO m, MonadReader AppState m) => m ()
1128+sendReloadEvent = do
1129+ sink <- asks eventSink
1130+ liftBase $ atomically $ writeTChan sink FrontendPleaseUpdateEverything
11161131
11171132-- | Runs the status updater.
1118−runStatusUpdater :: AppState -> IO ()
1119−runStatusUpdater state = do
1133+runStatusUpdater :: (KatipContext m, MonadBaseControl IO m, MonadReader AppState m) => m ()
1134+runStatusUpdater = do
11201135 let interval = 15 :: NominalDiffTime
1121− logErr :: MonadBase IO m => Text -> m ()
1122− logErr = liftBase . logWarning (logger state)
11231136
11241137 forever $ do
11251138 currentTime <- liftBase getCurrentTime
11261139 let cutoff = addUTCTime (negate interval) currentTime
1127− flip runReaderT state $ do
1140+ (>>= logLeft) . runExceptT $ do
11281141 rows' <- runStatement . select $ do
11291142 ds <- each deploymentSchema
11301143 where_ $ ds ^. #checkedAt <. litExpr cutoff
1131− where_ $ ds ^. #status /=. litExpr Archived
1132− pure (ds ^. #name, ds ^. #status, now `diffTime` (ds ^. #statusUpdatedAt), ds ^. #tag)
1133− checkResult <- for rows' $ \(dName, dStatus, Timestamp -> ts, _) -> do
1134− let timeout = statusUpdateTimeout state
1135− mEc <- case dStatus of
1136− ArchivePending -> do
1137− (ec, _, _) <- runCommandArgs archiveCheckingCommand =<< archiveCheckArgs dName
1138− pure $ Just ec
1139− _ -> do
1140− getSingleFullInfo dName >>= \case
1141− Nothing -> liftBase (logErr $ "Couldn't find deployment: " <> coerce dName) $> Nothing
1142− Just dep -> do
1143− dCfg <- getDefaultConfig dName
1144− (ec, _, _) <- runCommandArgs checkingCommand =<< checkCommandArgs dCfg (dep ^. #deployment)
1145− pure $ Just ec
1144+ where_ $ not_ $ (ds ^. #status) `in_` (litExpr <$> [Archived, CleanupFailed])
1145+ pure (ds ^. #name, ds ^. #status, now `diffTime` (ds ^. #statusUpdatedAt))
1146+ checkResult <- for rows' $ \(dName, dStatus, Timestamp -> ts) -> do
1147+ timeout <- asks statusUpdateTimeout
1148+ mEc <-
1149+ getSingleFullInfo dName >>= \case
1150+ Nothing -> do
1151+ logLocM ErrorS $ logStr $ "Couldn't find deployment: " <> unDeploymentName dName
1152+ pure Nothing
1153+ Just (view #deployment -> dep) -> do
1154+ cfg <- getDeploymentConfig dep
1155+ case dStatus of
1156+ ArchivePending -> do
1157+ (ec, _, _, _) <- runCommandArgs archiveCheckingCommand =<< archiveCheckArgs cfg dep
1158+ pure $ Just ec
1159+ _ -> do
1160+ (ec, _, _, _) <- runCommandArgs checkingCommand =<< checkCommandArgs cfg dep
1161+ pure $ Just ec
11461162 pure $ mEc <&> \ec -> (dName, statusTransition ec dStatus ts timeout, dStatus)
11471163 updated <-
11481164 for (catMaybes checkResult) $ \(dName, transitionM, dStatus) ->
@@ -1162,10 +1178,14 @@ runStatusUpdater state = do
11621178 ($> True) $
11631179 (runExceptT . runExceptT) (transitionToStatus dName transition) >>= \case
11641180 Right (Right ()) -> updateCheckedAt (transitionStatus transition)
1165− Left e -> logErr $ show' e
1166− Right (Left e) -> logErr $ show' e
1167− when (Prelude.or updated) $ liftBase $ sendReloadEvent state
1168− liftBase $ threadDelay 2000000
1181+ Left e -> logLocM ErrorS $ show' e
1182+ Right (Left e) -> logLocM ErrorS $ show' e
1183+ when (Prelude.or updated) sendReloadEvent
1184+ threadDelay 2000000
1185+
1186+logLeft :: (HasCallStack, KatipContext m, Exception e) => Either e () -> m ()
1187+logLeft (Left err) = logLocM ErrorS $ logStr $ displayException err
1188+logLeft (Right ()) = pure ()
11691189
11701190-- | Returns the new deployment status.
11711191statusTransition ::
@@ -1196,16 +1216,20 @@ failureStatusType _ = GenericFailure
11961216-- if graceful shutdown has been activated.
11971217failIfGracefulShutdownActivated :: AppM ()
11981218failIfGracefulShutdownActivated = do
1199− gracefulShutdownAct <- gracefulShutdownActivated <$> ask
1200− gracefulShutdown <- liftIO . readIORef $ gracefulShutdownAct
1219+ gracefulShutdown <- isShuttingDown
12011220 if gracefulShutdown
12021221 then throwError err405 {errBody = appError "Graceful shutdown activated"}
12031222 else pure ()
12041223
1224+isShuttingDown :: (MonadReader AppState m, MonadBase IO m) => m Bool
1225+isShuttingDown = do
1226+ gracefulShutdownAct <- gracefulShutdownActivated <$> ask
1227+ readIORef $ gracefulShutdownAct
1228+
12051229-- | Handles the graceful shutdown signal.
12061230-- Sends a signal to the 'shutdownSem' semaphore
12071231-- if the background worker counter is 0.
1208−terminationHandler :: IORef Int -> IORef Bool -> MVar () -> IO ()
1232+terminationHandler :: MonadBase IO m => IORef Int -> IORef Bool -> MVar () -> m ()
12091233terminationHandler bgWorkersC gracefulShudownAct shutdownS = do
12101234 atomicWriteIORef gracefulShudownAct True
12111235 c <- readIORef bgWorkersC
@@ -1215,8 +1239,10 @@ terminationHandler bgWorkersC gracefulShudownAct shutdownS = do
12151239
12161240-- | Waits on the 'shutdownSem' semaphore to determine when the
12171241-- server can shut down.
1218−runShutdownHandler :: AppState -> IO ()
1219−runShutdownHandler = takeMVar . shutdownSem
1242+runShutdownHandler :: (MonadBase IO m, MonadReader AppState m) => m ()
1243+runShutdownHandler = do
1244+ sem <- asks shutdownSem
1245+ takeMVar sem
12201246
12211247-- | Runs background the worker and increases the background worker counter.
12221248-- Decreases background worker counter
@@ -1225,24 +1251,46 @@ runShutdownHandler = takeMVar . shutdownSem
12251251-- if the background worker counter is 0
12261252-- and graceful shutdown is activated.
12271253runBgWorker :: (MonadBaseControl IO m, MonadReader AppState m) => m () -> m ()
1228−runBgWorker act = void $ L.forkFinally act' cleanup
1254+runBgWorker act = void $ forkFinally act' cleanup
12291255 where
12301256 act' = do
12311257 state <- ask
1232− liftBase (atomicModifyIORef' (bgWorkersCounter state) (\c -> (c + 1, c))) >> act
1258+ atomicModifyIORef' (bgWorkersCounter state) $ \c -> (c + 1, ())
1259+ act
12331260 cleanup _ = do
12341261 state <- ask
1235− liftBase $ do
1236− c <- atomicModifyIORef' (bgWorkersCounter state) (\c -> (c - 1, c - 1))
1237− gracefulShutdown <- readIORef (gracefulShutdownActivated state)
1238− if gracefulShutdown && c == 0
1239− then putMVar (shutdownSem state) ()
1240− else pure ()
1262+ c <- atomicModifyIORef' (bgWorkersCounter state) $ \c -> (c - 1, c - 1)
1263+ isShutdown <- readIORef (gracefulShutdownActivated state)
1264+ if isShutdown && c == 0
1265+ then putMVar (shutdownSem state) ()
1266+ else pure ()
1267+
1268+runBgWorkerSync ::
1269+ (MonadBaseControl IO m, MonadReader AppState m) =>
1270+ -- | The computation to run, takes a callback for returning a result (and monadic state)
1271+ ((a -> m ()) -> m ()) ->
1272+ m a
1273+runBgWorkerSync act = do
1274+ result <- newEmptyMVar
1275+ runBgWorker $ do
1276+ let respondSync res = do
1277+ state <- liftBaseWith $ \runInBase -> runInBase $ pure res
1278+ tryPutMVar result (Right state) >>= \case
1279+ True -> pure ()
1280+ False -> error "result already submitted by bg worker"
1281+ void $
1282+ try (liftBaseWith $ \runInBase -> runInBase $ act respondSync) >>= \case
1283+ Left e -> tryPutMVar result (Left e)
1284+ Right _ -> tryPutMVar result $ Left $ errorCallException "bg worker did not submit result"
1285+ takeMVar result >>= \case
1286+ Left exc -> throwIO exc
1287+ Right stm -> restoreM stm
12411288
12421289-- | Same as 'runBgWorker' but also locks the specified deployment.
12431290runDeploymentBgWorker ::
12441291 forall m a.
1245− (MonadBaseControl IO m, MonadReader AppState m, MonadError ServerError m) =>
1292+ (KatipContext m, MonadBaseControl IO m, MonadReader AppState m, MonadError ServerError m) =>
1293+ -- | Status to set after the first part has finished.
12461294 Maybe DeploymentStatus ->
12471295 DeploymentName ->
12481296 -- | Think of this as running synchronously.
@@ -1250,35 +1298,22 @@ runDeploymentBgWorker ::
12501298 m a ->
12511299 -- | The computation to run asynchronously.
12521300 (a -> m ()) ->
1253− m ()
1301+ m a
12541302runDeploymentBgWorker newS dName pre post = do
1255− st <- ask
1256− mainThread <- L.myThreadId
1257− (err :: MVar (Either ServerError (StM m a))) <- L.newEmptyMVar
1258− runBgWorker $
1259− withLockedDeployment
1260− dName
1261− (L.putMVar err $ Left err409 {errBody = "The deployment is currently being processed."})
1262− ( do
1263− L.try (liftBaseWith \runInBase -> runInBase pre) >>= \case
1264− Left (e :: L.SomeException) -> L.throwTo mainThread e
1265− Right x -> do
1266− proceed <- flip L.finally (L.tryPutMVar err (Left err500)) $ case newS of
1267− Just newS' -> do
1268− runExceptT (assertDeploymentTransitionPossibleS dName newS') >>= \case
1269− Left e -> L.putMVar err (Left e) $> False
1270− Right () -> L.putMVar err (Right x) $> True
1271− Nothing -> L.putMVar err (Right x) $> True
1272− liftBase (sendReloadEvent st)
1273− when proceed $ restoreM x >>= post
1274− )
1275− L.readMVar err >>= \case
1276− Left e -> throwError e
1277− Right (stm :: StM m a) -> do
1278− -- This might be a bad idea in general, but with just ReaderT and ExceptT
1279− -- it should be fine.
1280− _ :: a <- restoreM stm
1281− return ()
1303+ liftEither =<< runBgWorkerSync worker
1304+ where
1305+ worker respondSync = do
1306+ withLockedDeployment
1307+ dName
1308+ (respondSync $ Left err409 {errBody = "The deployment is currently being processed."})
1309+ ( do
1310+ let preWithAssert = pre <* for newS (assertDeploymentTransitionPossibleS dName)
1311+ result <- catchError (Right <$> preWithAssert) (pure . Left)
1312+ respondSync result
1313+ liftEither result >>= \res -> do
1314+ sendReloadEvent
1315+ post res
1316+ )
12821317
12831318extractDeploymentFullInfo ::
12841319 (MonadReader AppState m, MonadBase IO m) =>
@@ -1287,7 +1322,6 @@ extractDeploymentFullInfo ::
12871322extractDeploymentFullInfo d = do
12881323 let dName = d ^. #name
12891324 locked <- isDeploymentLocked dName
1290− dCfg <- getDefaultConfig dName
12911325 pure
12921326 DeploymentFullInfo
12931327 { deployment = extractDeployment d
@@ -1298,5 +1332,4 @@ extractDeploymentFullInfo d = do
12981332 , metadata = d ^. #metadata
12991333 , createdAt = d ^. #createdAt
13001334 , updatedAt = d ^. #updatedAt
1301− , deploymentDefaultConfig = dCfg
13021335 }
octopod-backend/src/Octopod/Server/Args.hs+2−1
@@ -8,6 +8,7 @@ module Octopod.Server.Args
88 )
99where
1010
11+import Control.Monad.IO.Class
1112import Data.ByteString (ByteString)
1213import Data.Coerce
1314import Options.Generic
@@ -48,7 +49,7 @@ data OctopodOpts = OctopodOpts
4849 deriving stock (Show)
4950
5051-- | Parses Octopod Server arguments.
51−parseArgs :: IO OctopodOpts
52+parseArgs :: MonadIO m => m OctopodOpts
5253parseArgs = do
5354 args <- getRecord "Octopod.Server"
5455 pure $
octopod-backend/src/Octopod/Server/ControlScriptUtils.hs+94−69
@@ -5,15 +5,18 @@
55--This module contains control script utils.
66module Octopod.Server.ControlScriptUtils
77 ( infoCommandArgs,
8+ createCommandArgs,
9+ updateCommandArgs,
10+ archiveCommandArgs,
811 unarchiveCommandArgs,
12+ cleanupCommandArgs,
913 notificationCommandArgs,
1014 runCommand,
11− runCommandWithoutPipes,
1215 runCommandArgs,
1316 runCommandArgs',
1417 checkCommandArgs,
1518 archiveCheckArgs,
16− tagCheckCommandArgs,
19+ configCheckCommandArgs,
1720
1821 -- * overrides
1922 defaultDeploymentOverridesArgs,
@@ -27,18 +30,22 @@ module Octopod.Server.ControlScriptUtils
2730 )
2831where
2932
33+import Control.Concurrent (forkIO, threadDelay)
34+import Control.Concurrent.STM
3035import Control.Lens
3136import Control.Monad.Base
3237import Control.Monad.Reader
3338import qualified Data.ByteString.Lazy as TL
3439import Data.Coerce
40+import qualified Data.ConfigTree as CT
41+import Data.Fixed
3542import Data.Generics.Product.Typed
36−import qualified Data.Map.Ordered.Strict as MO
3743import qualified Data.Text as T
3844import qualified Data.Text.Encoding as T
39−import Octopod.Server.Logger
45+import Data.Time
46+import Octopod.Server.Logging
4047import System.Exit
41−import System.Log.FastLogger
48+import System.Process (terminateProcess)
4249import System.Process.Typed
4350import Types
4451
@@ -48,12 +55,12 @@ type GenericDeploymentCommandArgs m r =
4855 , HasType ProjectName r
4956 , HasType Domain r
5057 ) =>
51− FullDefaultConfig ->
58+ FullConfig ->
5259 Deployment ->
5360 m ControlScriptArgs
5461
5562genericDeploymentCommandArgs :: GenericDeploymentCommandArgs m r
56−genericDeploymentCommandArgs dCfg dep = do
63+genericDeploymentCommandArgs cfg dep = do
5764 (Namespace namespace) <- asks getTyped
5865 (ProjectName projectName) <- asks getTyped
5966 (Domain domain) <- asks getTyped
@@ -67,10 +74,8 @@ genericDeploymentCommandArgs dCfg dep = do
6774 , T.unpack . coerce $ namespace
6875 , "--name"
6976 , T.unpack . coerce $ dep ^. #name
70− , "--tag"
71− , T.unpack . coerce $ tag dep
7277 ]
73− <> fullConfigArgs dCfg dep
78+ <> fullConfigArgs cfg
7479
7580type GenericDeploymentCommandArgsNoConfig m r =
7681 ( MonadReader r m
@@ -80,9 +85,6 @@ type GenericDeploymentCommandArgsNoConfig m r =
8085 ) =>
8186 m ControlScriptArgs
8287
83−unarchiveCommandArgs :: GenericDeploymentCommandArgs m r
84−unarchiveCommandArgs = genericDeploymentCommandArgs
85−
8688genericDeploymentCommandArgsNoConfig :: GenericDeploymentCommandArgsNoConfig m r
8789genericDeploymentCommandArgsNoConfig = do
8890 (Namespace namespace) <- asks getTyped
@@ -101,11 +103,29 @@ genericDeploymentCommandArgsNoConfig = do
101103infoCommandArgs :: GenericDeploymentCommandArgs m r
102104infoCommandArgs = genericDeploymentCommandArgs
103105
106+unarchiveCommandArgs :: GenericDeploymentCommandArgs m r
107+unarchiveCommandArgs = genericDeploymentCommandArgs
108+
109+createCommandArgs :: GenericDeploymentCommandArgs m r
110+createCommandArgs = genericDeploymentCommandArgs
111+
112+updateCommandArgs :: GenericDeploymentCommandArgs m r
113+updateCommandArgs = genericDeploymentCommandArgs
114+
115+archiveCommandArgs :: GenericDeploymentCommandArgs m r
116+archiveCommandArgs = genericDeploymentCommandArgs
117+
118+cleanupCommandArgs :: GenericDeploymentCommandArgs m r
119+cleanupCommandArgs = genericDeploymentCommandArgs
120+
104121checkCommandArgs :: GenericDeploymentCommandArgs m r
105122checkCommandArgs = genericDeploymentCommandArgs
106123
107−tagCheckCommandArgs :: GenericDeploymentCommandArgs m r
108−tagCheckCommandArgs = genericDeploymentCommandArgs
124+archiveCheckArgs :: GenericDeploymentCommandArgs m r
125+archiveCheckArgs = genericDeploymentCommandArgs
126+
127+configCheckCommandArgs :: GenericDeploymentCommandArgs m r
128+configCheckCommandArgs = genericDeploymentCommandArgs
109129
110130defaultDeploymentOverridesArgs :: GenericDeploymentCommandArgsNoConfig m r
111131defaultDeploymentOverridesArgs = genericDeploymentCommandArgsNoConfig
@@ -132,13 +152,12 @@ notificationCommandArgs ::
132152 , HasType Domain r
133153 ) =>
134154 DeploymentName ->
135− DeploymentTag ->
136155 -- | Previous status
137156 DeploymentStatus ->
138157 -- | New status
139158 DeploymentStatus ->
140159 m ControlScriptArgs
141−notificationCommandArgs dName dTag old new = do
160+notificationCommandArgs dName old new = do
142161 (Namespace namespace) <- asks getTyped
143162 (ProjectName projectName) <- asks getTyped
144163 (Domain domain) <- asks getTyped
@@ -152,89 +171,95 @@ notificationCommandArgs dName dTag old new = do
152171 , T.unpack namespace
153172 , "--name"
154173 , T.unpack . coerce $ dName
155− , "--tag"
156− , T.unpack . coerce $ dTag
157174 , "--old-status"
158175 , T.unpack $ deploymentStatusToText old
159176 , "--new-status"
160177 , T.unpack $ deploymentStatusToText new
161178 ]
162179
163−archiveCheckArgs ::
164− ( MonadReader r m
165− , HasType Namespace r
166− , HasType ProjectName r
167− , HasType Domain r
168− ) =>
169− DeploymentName ->
170− m ControlScriptArgs
171−archiveCheckArgs dName = do
172− (ProjectName projectName) <- asks getTyped
173− (Domain domain) <- asks getTyped
174− (Namespace namespace) <- asks getTyped
175− return $
176− ControlScriptArgs
177− [ "--project-name"
178− , T.unpack projectName
179− , "--base-domain"
180− , T.unpack domain
181− , "--namespace"
182− , T.unpack namespace
183− , "--name"
184− , T.unpack . coerce $ dName
185− ]
186−
187180runCommandArgs ::
188− (MonadReader r m, MonadBase IO m, HasType TimedFastLogger r) =>
181+ ( KatipContext m
182+ , MonadBase IO m
183+ , MonadReader r m
184+ , HasType ControlScriptTimeout r
185+ ) =>
189186 (r -> Command) ->
190187 ControlScriptArgs ->
191− m (ExitCode, Stdout, Stderr)
188+ m (ExitCode, Stdout, Stderr, Duration)
192189runCommandArgs f args = do
193190 cmd <- asks f
194191 runCommandArgs' cmd args
195192
196193runCommandArgs' ::
197− (MonadBase IO m, HasType TimedFastLogger r, MonadReader r m) =>
194+ ( KatipContext m
195+ , MonadBase IO m
196+ , MonadReader r m
197+ , HasType ControlScriptTimeout r
198+ ) =>
198199 Command ->
199200 ControlScriptArgs ->
200− m (ExitCode, Stdout, Stderr)
201+ m (ExitCode, Stdout, Stderr, Duration)
201202runCommandArgs' (Command cmd) (ControlScriptArgs args) = do
202− logger <- asks (getTyped @TimedFastLogger)
203− let logText = T.unwords (cmd : fmap T.pack args)
204− liftBase $ logInfo logger $ "calling: " <> logText
205− res@(ec, _, _) <- runCommand (T.unpack cmd) args
206− liftBase $
207− logInfo logger $
208− "calling `" <> logText <> "` exited with: " <> show' ec
209− return res
203+ t1 <- liftBase getCurrentTime
204+ (ec, out, err) <- runCommand (T.unpack cmd) args
205+ t2 <- liftBase getCurrentTime
206+ let elTime = elapsedTime t2 t1
207+ return (ec, out, err, elTime)
208+
209+-- | Returns time delta between 2 timestamps.
210+elapsedTime :: UTCTime -> UTCTime -> Duration
211+elapsedTime t1 t2 = Duration . calendarTimeTime $ Prelude.abs $ t2 `diffUTCTime` t1
210212
211213-- | Helper to run command with pipes.
212−runCommand :: MonadBase IO m => FilePath -> [String] -> m (ExitCode, Stdout, Stderr)
214+runCommand ::
215+ ( KatipContext m
216+ , MonadBase IO m
217+ , MonadReader r m
218+ , HasType ControlScriptTimeout r
219+ ) =>
220+ FilePath ->
221+ [String] ->
222+ m (ExitCode, Stdout, Stderr)
213223runCommand cmd args = do
214− (ec, out, err) <- liftBase . readProcess $ proc cmd args
224+ logLocM DebugS $ "Calling control script: " <> logStr cmd <> " " <> show' args
225+ ControlScriptTimeout timeout <- asks getTyped
226+ let pc =
227+ proc cmd args
228+ & setStdout byteStringOutput
229+ & setStderr byteStringOutput
230+ (ec, out, err) <- liftBase $
231+ withProcessTerm pc $ \p -> do
232+ void . forkIO $ do
233+ threadDelayMicro $ realToFrac timeout
234+ terminateProcess $ unsafeProcessHandle p
235+ atomically $
236+ (,,)
237+ <$> waitExitCodeSTM p
238+ <*> getStdout p
239+ <*> getStderr p
240+ katipAddContext (FilePayload "stdout" $ TL.toStrict out) $
241+ katipAddContext (FilePayload "stderr" $ TL.toStrict err) $
242+ logLocM (if ec /= ExitSuccess then WarningS else DebugS) $
243+ "Control script " <> logStr cmd <> " " <> show' args <> " exited with: " <> show' ec
215244 pure
216245 ( ec
217246 , Stdout . T.decodeUtf8 . TL.toStrict $ out
218247 , Stderr . T.decodeUtf8 . TL.toStrict $ err
219248 )
249+ where
250+ threadDelayMicro :: Micro -> IO ()
251+ threadDelayMicro (MkFixed i) = threadDelay (fromInteger i)
220252
221−-- | Helper to run command without pipes.
222−runCommandWithoutPipes :: FilePath -> [String] -> IO ExitCode
223−runCommandWithoutPipes cmd args =
224− withProcessWait (proc cmd args) waitExitCode
225−
226−fullConfigArgs :: FullDefaultConfig -> Deployment -> ControlScriptArgs
227−fullConfigArgs defCfg dep =
228− overridesArgs (applyOverrides (dep ^. #appOverrides) (appDefaultConfig defCfg))
229− <> overridesArgs (applyOverrides (dep ^. #deploymentOverrides) (depDefaultConfig defCfg))
253+fullConfigArgs :: FullConfig -> ControlScriptArgs
254+fullConfigArgs cfg = overridesArgs (appConfig cfg) <> overridesArgs (depConfig cfg)
230255
231256overridesArgs :: forall l. KnownOverrideLevel l => Config l -> ControlScriptArgs
232257overridesArgs (Config cc) =
233258 ControlScriptArgs
234259 . concatMap (\(T.unpack -> k, T.unpack -> v) -> [argumentName, k <> "=" <> v])
235− . MO.assocs
260+ . CT.toFlatList
236261 $ cc
237262 where
238263 argumentName = case knownOverrideLevel @l of
239− ApplicationLevel -> "--app-env-override"
240− DeploymentLevel -> "--deployment-override"
264+ ApplicationLevel -> "--application-config"
265+ DeploymentLevel -> "--deployment-config"
octopod-backend/src/Octopod/Server/Logger.hsdeleted−42
@@ -1,42 +0,0 @@
1−-- |
2−--Module : Octopod.Server.Logger
3−--Description : Octopod Server logger.
4−module Octopod.Server.Logger
5− ( newLogger,
6− logInfo,
7− logWarning,
8− logWithSeverity,
9− show',
10− )
11−where
12−
13−import Data.ByteString
14−import Data.Text
15−import qualified Data.Text as T
16−import System.Log.FastLogger
17−
18−-- | Creates new logger.
19−newLogger :: IO TimedFastLogger
20−newLogger = do
21− timeCache <- newTimeCache "%Y-%m-%d %T%z"
22− (logger, _) <- newTimedFastLogger timeCache (LogStdout defaultBufSize)
23− pure logger
24−
25−-- | Logs a message with the 'INFO' severity.
26−logInfo :: TimedFastLogger -> Text -> IO ()
27−logInfo l = logWithSeverity l "INFO"
28−
29−-- | Logs a message with the 'WARN' severity.
30−logWarning :: TimedFastLogger -> Text -> IO ()
31−logWarning l = logWithSeverity l "WARN"
32−
33−-- | Logs a message with the specified severity.
34−logWithSeverity :: ToLogStr msg => TimedFastLogger -> ByteString -> msg -> IO ()
35−logWithSeverity l severity msg = l $ \ft -> metadata ft <> message
36− where
37− metadata :: ByteString -> LogStr
38− metadata ft = foldMap toLogStr ["[", ft, " ", severity, "] "]
39− message = toLogStr msg <> toLogStr ("\n" :: ByteString)
40−
41−show' :: Show a => a -> Text
42−show' = T.pack . show
octopod-backend/src/Octopod/Server/Logging.hsnew file+121
@@ -0,0 +1,121 @@
1+-- |
2+--Module : Octopod.Server.Logging
3+--Description : Octopod Server logging setup
4+module Octopod.Server.Logging
5+ ( runLog,
6+ LogConfig (..),
7+ show',
8+ logLocM,
9+ logStr,
10+ katipAddContext,
11+ katipAddNamespace,
12+ Severity (..),
13+ KatipContext,
14+ KatipContextT,
15+ LogItem (..),
16+ ToObject (..),
17+ PayloadSelection (..),
18+ FilePayload (..),
19+ )
20+where
21+
22+import qualified Control.Exception.Lifted as L
23+import Control.Lens hiding (scribe, (.=))
24+import Control.Monad
25+import Control.Monad.Base
26+import Control.Monad.Trans.Control
27+import Data.Aeson
28+import Data.ByteString (ByteString)
29+import qualified Data.HashMap.Strict as HM
30+import Data.Text (Text)
31+import qualified Data.Text as T
32+import qualified Data.Text.Encoding as T
33+import Data.Text.Lazy.Builder
34+import GHC.Generics
35+import Katip
36+import Katip.Format.Time
37+import Katip.Scribes.Handle
38+import System.IO
39+import Types hiding (stdout)
40+
41+data LogConfig = LogConfig
42+ { project :: ProjectName
43+ , debug :: Bool
44+ , prodLogs :: Bool
45+ }
46+ deriving stock (Generic)
47+
48+newtype SingleLineify a = SingleLineify a
49+ deriving newtype (LogItem)
50+
51+instance ToObject a => ToObject (SingleLineify a) where
52+ toObject (SingleLineify x) = fmap go $ toObject x
53+ where
54+ go (Object obj) = Object $ fmap go obj
55+ go (Array v) = Array $ fmap go v
56+ go (String s) = String $ T.concat $ escape s
57+ go value = value
58+ escape s = case T.break (`elem` ("\a\b\f\n\r\t\v\\" :: String)) s of
59+ (xs, ys) ->
60+ xs : case T.uncons ys of
61+ Nothing -> []
62+ Just (c, zs) -> case c of
63+ '\a' -> "\\a" : escape zs
64+ '\b' -> "\\b" : escape zs
65+ '\f' -> "\\f" : escape zs
66+ '\n' -> "\\n" : escape zs
67+ '\r' -> "\\r" : escape zs
68+ '\t' -> "\\t" : escape zs
69+ '\v' -> "\\v" : escape zs
70+ '\\' -> "\\\\" : escape zs
71+ _ -> escape ys
72+
73+makeLogEnv :: LogConfig -> IO LogEnv
74+makeLogEnv conf = do
75+ scribe <- mkHandleScribeWithFormatter formatter ColorIfTerminal stdout (permitItem severity) verbosity
76+ env <- initLogEnv "octopod" $ Environment $ uProjectName $ conf ^. #project
77+ env' <- registerScribe "stdout" scribe defaultScribeSettings env
78+ pure env'
79+ where
80+ verbosity = if conf ^. #prodLogs then V3 else V0
81+ severity = if conf ^. #debug then DebugS else InfoS
82+ formatter :: LogItem a => ItemFormatter a
83+ formatter =
84+ if conf ^. #prodLogs
85+ then \color verb item -> bracketFormat color verb item {_itemPayload = SingleLineify $ _itemPayload item}
86+ else \color verb item ->
87+ brackets (fromText $ formatAsLogTime $ _itemTime item)
88+ <> brackets (fromText $ colorBySeverity color (_itemSeverity item) $ renderSeverity $ _itemSeverity item)
89+ <> mconcat (map brackets $ getKeys verb $ _itemPayload item)
90+ <> " "
91+ <> unLogStr (_itemMessage item)
92+ <> mconcat (("\n" <>) <$> renderFiles (toObject $ _itemPayload item))
93+ renderFiles :: Object -> [Builder]
94+ renderFiles obj =
95+ HM.toList obj >>= \(k, v) -> case (T.stripPrefix "file-" k, v) of
96+ (Just fn, String text) | not $ T.null text -> pure $ fromText fn <> ":\n" <> fromText (tabulate text)
97+ _ -> []
98+ tabulate = ("\t" <>) . T.replace "\n" "\n\t"
99+
100+destroyLogEnv :: LogEnv -> IO ()
101+destroyLogEnv = void . closeScribes
102+
103+runLog :: MonadBaseControl IO m => LogConfig -> KatipContextT m a -> m a
104+runLog conf act = L.bracket (liftBase $ makeLogEnv conf) (liftBase . destroyLogEnv) $
105+ \env -> runKatipContextT env () mempty act
106+
107+show' :: Show a => a -> LogStr
108+show' = logStr . show
109+
110+data FilePayload = FilePayload
111+ { filename :: Text
112+ , contents :: ByteString
113+ }
114+ deriving stock (Generic)
115+
116+instance ToObject FilePayload where
117+ toObject fp = ("file-" <> fp ^. #filename) .= T.decodeUtf8 (fp ^. #contents)
118+
119+instance LogItem FilePayload where
120+ payloadKeys V3 _ = AllKeys
121+ payloadKeys _ _ = SomeKeys []
octopod-backend/src/Octopod/Server/Posix.hs+22−17
@@ -1,25 +1,30 @@
11module Octopod.Server.Posix (installShutdownHandler) where
22
3+import Control.Monad.Trans.Control
4+import Data.Aeson
5+import Data.Traversable
6+import Octopod.Server.Logging
7+import System.Posix.Signals
38
4−import Control.Monad
5−import Control.Monad.Base (liftBase)
6−import Data.Text
7−import System.Log.FastLogger
8−import System.Posix.Signals
9+newtype SigContext = SigContext Signal
910
11+instance ToObject SigContext where
12+ toObject (SigContext signal) = "signal" .= show signal
1013
11−import Octopod.Server.Logger
12−
14+instance LogItem SigContext where
15+ payloadKeys _ _ = AllKeys
1316
1417-- | Installs the given shutdown handler for the specified signals.
15−installShutdownHandler
16− :: TimedFastLogger
17− -> [Signal]
18− -> IO ()
19− -> IO [Handler]
20−installShutdownHandler logger signals action =
21− forM signals $ \signal -> installHandler signal (handler signal) Nothing
18+installShutdownHandler ::
19+ (KatipContext m, MonadBaseControl IO m, StM m () ~ ()) =>
20+ [Signal] ->
21+ m () ->
22+ m [Handler]
23+installShutdownHandler signals action =
24+ for signals $ \signal -> liftBaseWith $ \run ->
25+ installHandler signal (Catch $ run (handler signal)) Nothing
2226 where
23− handler signal = Catch $ do
24− logInfo logger $ "Shutdown initiated by signal " <> (pack . show $ signal)
25− liftBase action
27+ handler signal = katipAddNamespace "signal handler" $
28+ katipAddContext (SigContext signal) $ do
29+ logLocM InfoS "Shutdown initiated by signal"
30+ action
octopod-backend/src/Orphans.hs−3
@@ -23,9 +23,6 @@ deriving newtype instance DBEq DeploymentId
2323deriving newtype instance DBType DeploymentName
2424deriving newtype instance DBEq DeploymentName
2525
26−deriving newtype instance DBType DeploymentTag
27−deriving newtype instance DBEq DeploymentTag
28−
2926instance DBType Action where
3027 typeInformation = parseTypeInformationFromMapping actionText
3128
octopod-backend/src/Types.hs+3−5
@@ -13,9 +13,9 @@ module Types
1313 Domain (..),
1414 Namespace (..),
1515 Command (..),
16− ArchiveRetention (..),
1716 Timeout (..),
1817 ControlScriptArgs (..),
18+ ControlScriptTimeout (..),
1919 )
2020where
2121
@@ -53,10 +53,6 @@ newtype Domain = Domain {unDomain :: Text}
5353newtype Namespace = Namespace {unNamespace :: Text}
5454 deriving stock (Show)
5555
56−-- | Archive retention.
57−newtype ArchiveRetention = ArchiveRetention {unArchiveRetention :: NominalDiffTime}
58− deriving stock (Show)
59−
6056-- | Timeout.
6157newtype Timeout = Timeout {unTimeout :: CalendarDiffTime}
6258 deriving stock (Show)
@@ -70,3 +66,5 @@ newtype ControlScriptArgs = ControlScriptArgs
7066 {unControlScriptArgs :: [String]}
7167 deriving stock (Show)
7268 deriving newtype (Semigroup, Monoid)
69+
70+newtype ControlScriptTimeout = ControlScriptTimeout DiffTime
octopod-common/Setup.hs+2−1
@@ -1,2 +1,3 @@
1−import Distribution.Simple
1+import Distribution.Simple
2+
23main = defaultMain
octopod-common/octopod-common.cabal+13−1
@@ -18,7 +18,9 @@ library
1818 exposed-modules: Common.Types
1919 , Common.Utils
2020 , Common.Validation
21− , Data.Map.Ordered.Strict.Extra
21+ , Data.ConfigTree
22+ , Control.Searchable
23+ , Common.Orphans
2224 -- other-modules:
2325 -- other-extensions:
2426 default-extensions: BlockArguments
@@ -47,6 +49,14 @@ library
4749 , EmptyDataDeriving
4850 , TupleSections
4951 , AllowAmbiguousTypes
52+ , TypeFamilies
53+ , RankNTypes
54+ , MultiParamTypeClasses
55+ , ApplicativeDo
56+ , UndecidableInstances
57+ , FunctionalDependencies
58+ , QuantifiedConstraints
59+ , PatternSynonyms
5060 build-depends: base
5161 , aeson
5262 , cassava
@@ -59,6 +69,8 @@ library
5969 , containers
6070 , time
6171 , ordered-containers
72+ , deepseq
73+ , these
6274 hs-source-dirs: src
6375 default-language: Haskell2010
6476 ghc-options:
octopod-common/src/Common/Orphans.hsnew file+20
@@ -0,0 +1,20 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Common.Orphans () where
4+
5+import Control.DeepSeq
6+import Data.Aeson
7+import Data.Map.Internal
8+import Data.Map.Ordered.Internal
9+import qualified Data.Map.Ordered.Strict as OM
10+import GHC.Generics (Generic)
11+
12+deriving stock instance Generic (Map k v)
13+
14+deriving stock instance Generic (OMap k v)
15+deriving anyclass instance (NFData k, NFData v) => NFData (OMap k v)
16+instance (ToJSON k, ToJSON v) => ToJSON (OMap k v) where
17+ toJSON = toJSON . OM.assocs
18+
19+instance (FromJSON k, FromJSON v, Ord k) => FromJSON (OMap k v) where
20+ parseJSON = fmap OM.fromList . parseJSON
octopod-common/src/Common/Types.hs+128−74
@@ -7,14 +7,17 @@
77-- This module contains common types between the backend and the frontend.
88module Common.Types where
99
10+import Common.Orphans ()
11+import Control.DeepSeq (NFData)
1012import Control.Lens
13+import Control.Searchable
1114import Data.Aeson hiding (Result)
15+import Data.ConfigTree (ConfigTree)
16+import qualified Data.ConfigTree as CT
1217import Data.Csv
1318import Data.Generics.Labels ()
1419import Data.Int
15−import Data.Map.Ordered.Strict (OMap, (<>|))
16−import qualified Data.Map.Ordered.Strict as OM
17−import Data.Map.Ordered.Strict.Extra ()
20+import Data.List.NonEmpty (NonEmpty)
1821import Data.Maybe
1922import Data.Text (Text)
2023import qualified Data.Text as T
@@ -35,17 +38,37 @@ instance KnownOverrideLevel 'ApplicationLevel where
3538instance KnownOverrideLevel 'DeploymentLevel where
3639 knownOverrideLevel = DeploymentLevel
3740
38−data OverrideValue = ValueAdded Text | ValueDeleted
39− deriving (ToJSON, FromJSON) via Snake OverrideValue
41+instance Searchable needle t => Searchable needle (OverrideValue' t) where
42+ type SearchableConstraint needle (OverrideValue' t) res = SearchableConstraint needle t res
43+ type Searched (OverrideValue' t) res = OverrideValue' (Searched t res)
44+ searchWith _ ValueDeleted = pure ValueDeleted
45+ searchWith f (ValueAdded t) = ValueAdded <$> searchWith f t
46+ {-# INLINE searchWith #-}
47+
48+data OverrideValue' t = ValueAdded t | ValueDeleted
49+ deriving (ToJSON, FromJSON) via Snake (OverrideValue' t)
4050 deriving stock (Eq, Ord, Show, Generic)
51+ deriving anyclass (NFData)
4152
42−newtype DefaultConfig (l :: OverrideLevel) = DefaultConfig (OMap Text Text)
43− deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
53+type OverrideValue = OverrideValue' Text
54+
55+type DefaultConfig = DefaultConfig' Text
56+
57+newtype DefaultConfig' te (l :: OverrideLevel) = DefaultConfig (ConfigTree te te)
58+ deriving newtype (Eq, Ord, Show)
59+
60+deriving newtype instance FromJSON (DefaultConfig' Text l)
61+deriving newtype instance ToJSON (DefaultConfig' Text l)
4462
45−lookupDefaultConfig :: DefaultConfig l -> Text -> Maybe Text
46−lookupDefaultConfig (DefaultConfig m) k = OM.lookup k m
63+instance Searchable t x => Searchable t (DefaultConfig' x l) where
64+ type SearchableConstraint t (DefaultConfig' x l) res = (Ord (Searched x res), SearchableConstraint t x res)
65+ type Searched (DefaultConfig' x l) res = DefaultConfig' (Searched x res) l
66+ searchWith f (DefaultConfig ct) = DefaultConfig <$> searchWith f ct
67+ {-# INLINE searchWith #-}
4768
48−newtype Config (l :: OverrideLevel) = Config {unConfig :: OMap Text Text}
69+type ConfigKey = [Text]
70+
71+newtype Config (l :: OverrideLevel) = Config {unConfig :: ConfigTree Text Text}
4972 deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
5073
5174data FullDefaultConfig = FullDefaultConfig
@@ -64,66 +87,75 @@ data FullConfig = FullConfig
6487
6588applyOverrides :: Overrides l -> DefaultConfig l -> Config l
6689applyOverrides (Overrides oo) (DefaultConfig dd) =
67− Config . extract $ oo <>| (ValueAdded <$> dd)
90+ Config . extract $ oo <> (ValueAdded <$> dd)
6891 where
69− extract :: OMap Text OverrideValue -> OMap Text Text
92+ extract :: ConfigTree Text OverrideValue -> ConfigTree Text Text
7093 extract =
7194 fmap
7295 ( \case
7396 ValueAdded v -> v
7497 ValueDeleted -> error "invariant"
7598 )
76− . OM.filter
77− ( \_ -> \case
99+ . CT.filter
100+ ( \case
78101 ValueAdded _ -> True
79102 ValueDeleted -> False
80103 )
81104
82−newtype Overrides (l :: OverrideLevel) = Overrides {unOverrides :: OMap Text OverrideValue}
83− deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
105+newtype Overrides' t (l :: OverrideLevel) = Overrides {unOverrides :: ConfigTree t (OverrideValue' t)}
106+ deriving newtype (Eq, Ord, Show, NFData, Semigroup, Monoid)
107+
108+deriving newtype instance FromJSON (Overrides' Text l)
109+deriving newtype instance ToJSON (Overrides' Text l)
110+
111+instance Searchable t x => Searchable t (Overrides' x l) where
112+ type SearchableConstraint t (Overrides' x l) res = (Ord (Searched x res), SearchableConstraint t x res)
113+ type Searched (Overrides' x l) res = Overrides' (Searched x res) l
114+ searchWith f (Overrides ct) = Overrides <$> searchWith f ct
115+ {-# INLINE searchWith #-}
116+
117+type Overrides = Overrides' Text
84118
85119extractOverrides :: DefaultConfig l -> Config l -> Overrides l
86120extractOverrides (DefaultConfig dCfg) (Config cfg) =
87− Overrides . OM.fromList $ removed <> present
121+ Overrides . CT.fromList $ removed <> present
88122 where
89− present :: [(Text, OverrideValue)]
90− present = mapMaybe processPresent . OM.assocs $ cfg
123+ present :: [(NonEmpty Text, OverrideValue)]
124+ present = mapMaybe processPresent . CT.toList $ cfg
91125
92− processPresent :: (Text, Text) -> Maybe (Text, OverrideValue)
93− processPresent (k, v) = case OM.lookup k dCfg of
126+ processPresent :: (NonEmpty Text, Text) -> Maybe (NonEmpty Text, OverrideValue)
127+ processPresent (k, v) = case CT.lookup k dCfg of
94128 Just v' | v == v' -> Nothing
95129 _ -> Just (k, ValueAdded v)
96130
97− processRemoved :: (Text, Text) -> Maybe (Text, OverrideValue)
131+ processRemoved :: (NonEmpty Text, Text) -> Maybe (NonEmpty Text, OverrideValue)
98132 processRemoved (k, _) =
99− if OM.member k cfg
133+ if CT.member k cfg
100134 then Nothing
101135 else Just (k, ValueDeleted)
102136
103− removed :: [(Text, OverrideValue)]
104− removed = mapMaybe processRemoved . OM.assocs $ dCfg
137+ removed :: [(NonEmpty Text, OverrideValue)]
138+ removed = mapMaybe processRemoved . CT.toList $ dCfg
105139
106−ov :: Text -> OverrideValue -> Overrides l
107−ov k v = Overrides $ OM.singleton (k, v)
108−
109−instance Semigroup (Overrides l) where
110− (Overrides lhs) <> (Overrides rhs) = Overrides $ rhs <>| lhs
111−
112−instance Monoid (Overrides l) where
113− mempty = Overrides OM.empty
140+ov :: NonEmpty Text -> OverrideValue -> Overrides l
141+ov k v = Overrides $ CT.singleton k v
114142
115143newtype DeploymentId = DeploymentId {unDeploymentId :: Int64}
116144 deriving stock (Show)
117145
118−newtype DeploymentName = DeploymentName {unDeploymentName :: Text}
119− deriving newtype
120− (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq, Ord)
146+instance Searchable needle t => Searchable needle (DeploymentName' t) where
147+ type SearchableConstraint needle (DeploymentName' t) res = SearchableConstraint needle t res
148+ type Searched (DeploymentName' t) res = DeploymentName' (Searched t res)
149+ searchWith f (DeploymentName t) = DeploymentName <$> searchWith f t
150+ {-# INLINE searchWith #-}
121151
122−newtype DeploymentTag = DeploymentTag {unDeploymentTag :: Text}
152+newtype DeploymentName' t = DeploymentName {unDeploymentName :: t}
123153 deriving newtype
124− (Show, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq)
154+ (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq, Ord, NFData)
125155
126−data Action = RestoreAction | ArchiveAction | UpdateAction | CreateAction
156+type DeploymentName = DeploymentName' Text
157+
158+data Action = RestoreAction | ArchiveAction | UpdateAction | CreateAction | CleanupAction
127159 deriving stock (Show, Read, Eq, Ord, Generic)
128160 deriving (FromJSON, ToJSON) via Snake Action
129161
@@ -160,6 +192,7 @@ deploymentStatusText =
160192 , (UpdatePending, "UpdatePending")
161193 , (ArchivePending, "ArchivePending")
162194 , (Archived, "Archived")
195+ , (CleanupFailed, "CleanupFailed")
163196 ]
164197
165198deploymentStatusToText :: DeploymentStatus -> Text
@@ -172,8 +205,10 @@ data DeploymentStatus
172205 | UpdatePending
173206 | ArchivePending
174207 | Archived
208+ | CleanupFailed
175209 deriving stock (Generic, Read, Show, Eq)
176210 deriving (FromJSON, ToJSON) via Snake DeploymentStatus
211+ deriving anyclass (NFData)
177212
178213data FailureType
179214 = GenericFailure
@@ -181,6 +216,7 @@ data FailureType
181216 | PartialAvailability
182217 deriving stock (Generic, Read, Show, Eq)
183218 deriving (FromJSON, ToJSON) via Snake FailureType
219+ deriving anyclass (NFData)
184220
185221data PreciseDeploymentStatus
186222 = -- | The deployment is currently being processed by the server
@@ -188,6 +224,7 @@ data PreciseDeploymentStatus
188224 | DeploymentNotPending {recordedStatus :: DeploymentStatus}
189225 deriving stock (Generic, Read, Show, Eq)
190226 deriving (FromJSON, ToJSON) via Snake PreciseDeploymentStatus
227+ deriving anyclass (NFData)
191228
192229archivedStatuses :: [DeploymentStatus]
193230archivedStatuses = [ArchivePending, Archived]
@@ -195,19 +232,39 @@ archivedStatuses = [ArchivePending, Archived]
195232isArchivedStatus :: DeploymentStatus -> Bool
196233isArchivedStatus = (`elem` archivedStatuses)
197234
198−data Deployment = Deployment
199− { name :: DeploymentName
200− , tag :: DeploymentTag
201− , appOverrides :: Overrides 'ApplicationLevel
202− , deploymentOverrides :: Overrides 'DeploymentLevel
235+data Deployment' t = Deployment
236+ { name :: DeploymentName' t
237+ , appOverrides :: Overrides' t 'ApplicationLevel
238+ , deploymentOverrides :: Overrides' t 'DeploymentLevel
203239 }
204240 deriving stock (Generic, Show, Eq)
205− deriving (FromJSON, ToJSON) via Snake Deployment
241+ deriving anyclass (NFData)
242+
243+deriving via Snake (Deployment' Text) instance FromJSON (Deployment' Text)
244+deriving via Snake (Deployment' Text) instance ToJSON (Deployment' Text)
245+
246+instance (Searchable needle t) => Searchable needle (Deployment' t) where
247+ type
248+ SearchableConstraint needle (Deployment' t) res =
249+ (SearchableConstraint needle t res, Ord (Searched t res))
250+ type Searched (Deployment' t) res = Deployment' (Searched t res)
251+ searchWith f d = do
252+ name' <- searchWith f $ d ^. #name
253+ appOverrides' <- searchWith f $ d ^. #appOverrides
254+ deploymentOverrides' <- searchWith f $ d ^. #deploymentOverrides
255+ pure
256+ Deployment
257+ { name = name'
258+ , appOverrides = appOverrides'
259+ , deploymentOverrides = deploymentOverrides'
260+ }
261+ {-# INLINE searchWith #-}
262+
263+type Deployment = Deployment' Text
206264
207265data DeploymentLog = DeploymentLog
208266 { actionId :: ActionId
209267 , action :: Action
210− , deploymentTag :: DeploymentTag
211268 , deploymentAppOverrides :: Overrides 'ApplicationLevel
212269 , deploymentDepOverrides :: Overrides 'DeploymentLevel
213270 , exitCode :: Int64
@@ -218,7 +275,7 @@ data DeploymentLog = DeploymentLog
218275 deriving (ToJSON, FromJSON) via Snake DeploymentLog
219276
220277newtype DeploymentMetadata = DeploymentMetadata {unDeploymentMetadata :: [DeploymentMetadatum]}
221− deriving newtype (Eq, Show, Ord, FromJSON, ToJSON)
278+ deriving newtype (Eq, Show, Ord, FromJSON, ToJSON, NFData)
222279
223280data DeploymentMetadatum = DeploymentMetadatum
224281 { -- | The name of the link
@@ -229,6 +286,7 @@ data DeploymentMetadatum = DeploymentMetadatum
229286 deriving stock (Generic, Show, Eq, Ord)
230287 deriving (FromJSON, ToJSON) via Snake DeploymentMetadatum
231288 deriving anyclass (FromRecord)
289+ deriving anyclass (NFData)
232290
233291data DeploymentInfo = DeploymentInfo
234292 { deployment :: Deployment
@@ -238,25 +296,32 @@ data DeploymentInfo = DeploymentInfo
238296 deriving stock (Generic, Show)
239297 deriving (FromJSON, ToJSON) via Snake DeploymentInfo
240298
241−data DeploymentFullInfo = DeploymentFullInfo
242− { deployment :: Deployment
299+instance (Searchable needle t) => Searchable needle (DeploymentFullInfo' t) where
300+ type
301+ SearchableConstraint needle (DeploymentFullInfo' t) res =
302+ (SearchableConstraint needle t res, Ord (Searched t res))
303+ type Searched (DeploymentFullInfo' t) res = DeploymentFullInfo' (Searched t res)
304+ searchWith f d = do
305+ deployment' <- searchWith f $ d ^. #deployment
306+ pure $ d & #deployment .~ deployment'
307+ {-# INLINE searchWith #-}
308+
309+data DeploymentFullInfo' t = DeploymentFullInfo
310+ { deployment :: Deployment' t
243311 , status :: PreciseDeploymentStatus
244312 , metadata :: DeploymentMetadata
245313 , createdAt :: UTCTime
246314 , updatedAt :: UTCTime
247− , deploymentDefaultConfig :: FullDefaultConfig
248315 }
249316 deriving stock (Generic, Show, Eq)
250− deriving (FromJSON, ToJSON) via Snake DeploymentFullInfo
317+ deriving anyclass (NFData)
318+
319+deriving via Snake (DeploymentFullInfo' Text) instance FromJSON (DeploymentFullInfo' Text)
320+deriving via Snake (DeploymentFullInfo' Text) instance ToJSON (DeploymentFullInfo' Text)
251321
252−getDeploymentConfig :: DeploymentFullInfo -> FullConfig
253−getDeploymentConfig d =
254− FullConfig
255− { appConfig = applyOverrides (d ^. #deployment . #appOverrides) (d ^. #deploymentDefaultConfig . #appDefaultConfig)
256− , depConfig = applyOverrides (d ^. #deployment . #deploymentOverrides) (d ^. #deploymentDefaultConfig . #depDefaultConfig)
257− }
322+type DeploymentFullInfo = DeploymentFullInfo' Text
258323
259−isDeploymentArchived :: DeploymentFullInfo -> Bool
324+isDeploymentArchived :: DeploymentFullInfo' t -> Bool
260325isDeploymentArchived DeploymentFullInfo {status = s} = case s of
261326 DeploymentNotPending s' -> isArchivedStatus s'
262327 -- if the deployment is currently undergoing some process,
@@ -264,8 +329,7 @@ isDeploymentArchived DeploymentFullInfo {status = s} = case s of
264329 DeploymentPending _ -> False
265330
266331data DeploymentUpdate = DeploymentUpdate
267− { newTag :: DeploymentTag
268− , appOverrides :: Overrides 'ApplicationLevel
332+ { appOverrides :: Overrides 'ApplicationLevel
269333 , deploymentOverrides :: Overrides 'DeploymentLevel
270334 }
271335 deriving stock (Generic, Show)
@@ -285,7 +349,6 @@ data CommandResponse
285349 = Success
286350 | ValidationError
287351 { nameField :: [Text]
288− , tagField :: [Text]
289352 }
290353 | AppError
291354 {errorMessage :: Text}
@@ -322,23 +385,14 @@ parseSetOverrides texts = do
322385 Just x -> Right x
323386 Nothing ->
324387 Left $ "Malformed override key-value pair " <> text <> ", should be similar to FOO=bar"
325− return . Overrides $ OM.fromList pairs'
388+ return . Overrides $ CT.fromList pairs'
326389 where
327− parseSingleOverride :: Text -> Maybe (Text, OverrideValue)
390+ parseSingleOverride :: Text -> Maybe (NonEmpty Text, OverrideValue)
328391 parseSingleOverride t
329392 | Just i <- T.findIndex (== '=') t =
330− let (key, value) = bimap T.strip (T.tail . T.strip) $ T.splitAt i t
393+ let (CT.deconstructConfigKey -> key, value) = bimap T.strip (T.tail . T.strip) $ T.splitAt i t
331394 in Just (key, ValueAdded value)
332395 parseSingleOverride _ = Nothing
333396
334397parseUnsetOverrides :: [Text] -> Overrides l
335−parseUnsetOverrides = Overrides . OM.fromList . fmap (,ValueDeleted)
336−
337−formatOverrides :: Overrides l -> Text
338−formatOverrides = T.unlines . formatOverrides'
339−
340−formatOverrides' :: Overrides l -> [Text]
341−formatOverrides' (Overrides m) = fmap (\(k, v) -> k <> "=" <> showValue v) . OM.assocs $ m
342− where
343− showValue (ValueAdded v) = v
344− showValue ValueDeleted = "<removed>"
398+parseUnsetOverrides = Overrides . CT.fromList . fmap ((,ValueDeleted) . CT.deconstructConfigKey)
octopod-common/src/Common/Utils.hs+1
@@ -47,3 +47,4 @@ isPending = \case
4747 CreatePending -> True
4848 UpdatePending -> True
4949 ArchivePending -> True
50+ CleanupFailed -> True
octopod-common/src/Control/Searchable.hsnew file+66
@@ -0,0 +1,66 @@
1+{-# OPTIONS_GHC -Wno-redundant-constraints #-}
2+
3+module Control.Searchable
4+ ( Searchable (..),
5+ traverseOMap,
6+ )
7+where
8+
9+import Control.Lens
10+import Data.Kind
11+import Data.Map.Ordered (OMap)
12+import qualified Data.Map.Ordered.Strict as OM
13+import Data.Text (Text)
14+
15+-- | This says that you can search 'needle's in a 'haystack'. What precisely
16+-- "search for" means is defined by a function (the first argument) which
17+-- looks at every 'needle' and returns a "search result".
18+--
19+-- Essentially this is just a glorified 'Traversal' that can have extra
20+-- constraints and can leverage the constraint solver.
21+--
22+-- For a concrete "search" implementation see 'Data.Text.Search' from the
23+-- 'octopod-frontend' package.
24+class Searchable needle haystack where
25+ type SearchableConstraint needle haystack res :: Constraint
26+ type SearchableConstraint _ _ _ = ()
27+ type Searched haystack res
28+ searchWith ::
29+ SearchableConstraint needle haystack res =>
30+ Traversal haystack (Searched haystack res) needle res
31+
32+instance Searchable Text Text where
33+ type Searched Text res = res
34+ searchWith f t = f t
35+ {-# INLINE searchWith #-}
36+
37+instance Searchable needle x => Searchable needle [x] where
38+ type SearchableConstraint needle [x] res = SearchableConstraint needle x res
39+ type Searched [x] res = [Searched x res]
40+ searchWith f = traverse (searchWith f)
41+ {-# INLINE searchWith #-}
42+
43+instance (Searchable needle a, Searchable needle b) => Searchable needle (a, b) where
44+ type
45+ SearchableConstraint needle (a, b) res =
46+ (SearchableConstraint needle a res, SearchableConstraint needle b res)
47+ type Searched (a, b) res = (Searched a res, Searched b res)
48+ searchWith f (a, b) = do
49+ a' <- searchWith f a
50+ b' <- searchWith f b
51+ pure (a', b')
52+ {-# INLINE searchWith #-}
53+
54+instance Searchable needle haystack => Searchable needle (Maybe haystack) where
55+ type SearchableConstraint needle (Maybe haystack) res = SearchableConstraint needle haystack res
56+ type Searched (Maybe haystack) res = Maybe (Searched haystack res)
57+ searchWith _ Nothing = pure Nothing
58+ searchWith f (Just h) = Just <$> searchWith f h
59+ {-# INLINE searchWith #-}
60+
61+traverseOMap ::
62+ forall h k x y.
63+ Ord k =>
64+ (forall f. Applicative f => (h -> f k) -> (x -> f y) -> OMap h x -> f (OMap k y))
65+traverseOMap kf vf (OM.assocs -> l) =
66+ fmap OM.fromList $ (\(h, x) -> (,) <$> kf h <*> vf x) `traverse` l
octopod-common/src/Data/ConfigTree.hsnew file+165
@@ -0,0 +1,165 @@
1+module Data.ConfigTree
2+ ( ConfigTree (..),
3+ null,
4+ filter,
5+ toList,
6+ singleton,
7+ fromList,
8+ lookup,
9+ member,
10+ deconstructConfigKey,
11+ reconstructConfigKey,
12+ toFlatList,
13+ fromFlatList,
14+ Data.ConfigTree.zip,
15+ catMaybes,
16+ insert,
17+ markValues,
18+ )
19+where
20+
21+import Common.Orphans ()
22+import Control.Applicative
23+import Control.DeepSeq (NFData)
24+import Control.Searchable
25+import Data.Aeson
26+import qualified Data.Bifunctor as Bi
27+import Data.Foldable (fold)
28+import qualified Data.List as L
29+import Data.List.NonEmpty (NonEmpty)
30+import qualified Data.List.NonEmpty as NE
31+import Data.Map.Ordered.Strict (OMap)
32+import qualified Data.Map.Ordered.Strict as OM
33+import Data.Maybe (fromJust, fromMaybe, isJust)
34+import Data.String
35+import Data.Text (Text)
36+import qualified Data.Text as T
37+import Data.These
38+import GHC.Generics (Generic)
39+import Prelude hiding (filter, lookup, null)
40+
41+zip :: Ord k => ConfigTree k v -> ConfigTree k h -> ConfigTree k (These v h)
42+zip (ConfigTree m) (ConfigTree w) =
43+ let intersection =
44+ OM.intersectionWith
45+ ( \_ (mv, vMap) (mh, hMap) ->
46+ ( case (mv, mh) of
47+ (Nothing, Nothing) -> Nothing
48+ (Just v, Nothing) -> Just $ This v
49+ (Nothing, Just h) -> Just $ That h
50+ (Just v, Just h) -> Just $ These v h
51+ , Data.ConfigTree.zip vMap hMap
52+ )
53+ )
54+ m
55+ w
56+ vCT = ConfigTree $ m OM.\\ intersection
57+ hCT = ConfigTree $ w OM.\\ intersection
58+ in fmap This vCT <> ConfigTree intersection <> fmap That hCT
59+
60+newtype ConfigTree k v = ConfigTree (OMap k (Maybe v, ConfigTree k v))
61+ deriving newtype (Eq, Ord, Show, NFData)
62+ deriving stock (Functor, Generic)
63+
64+instance ToJSON v => ToJSON (ConfigTree Text v) where
65+ toJSON = toJSON . toFlatList
66+
67+instance FromJSON v => FromJSON (ConfigTree Text v) where
68+ parseJSON = fmap fromFlatList . parseJSON
69+
70+markValues :: (IsString t, Monoid t, Ord t) => ConfigTree t c -> ConfigTree t (t, c)
71+markValues = fromList . fmap (\(k, v) -> (k, (reconstructConfigKey k, v))) . toList
72+{-# INLINE markValues #-}
73+
74+toFlatList :: (IsString t, Monoid t) => ConfigTree t c -> [(t, c)]
75+toFlatList = fmap (Bi.first reconstructConfigKey) . toList
76+{-# INLINE toFlatList #-}
77+
78+fromFlatList :: [(Text, v)] -> ConfigTree Text v
79+fromFlatList = fromList . fmap (Bi.first deconstructConfigKey)
80+{-# INLINE fromFlatList #-}
81+
82+toList :: ConfigTree k v -> [(NonEmpty k, v)]
83+toList (ConfigTree m) =
84+ OM.assocs m >>= \(k, (mv, s)) ->
85+ maybe id (\v -> (:) (pure k, v)) mv $ Bi.first (NE.cons k) <$> toList s
86+{-# INLINE toList #-}
87+
88+fromList :: Ord k => [(NonEmpty k, v)] -> ConfigTree k v
89+fromList = foldMap $ uncurry singleton
90+{-# INLINE fromList #-}
91+
92+singleton :: Ord k => NonEmpty k -> v -> ConfigTree k v
93+singleton (NE k) v = ConfigTree $ OM.singleton (k, (Just v, mempty))
94+singleton (NECons k ks) v = ConfigTree $ OM.singleton (k, (Nothing, singleton ks v))
95+{-# INLINE singleton #-}
96+
97+lookup :: Ord k => NonEmpty k -> ConfigTree k v -> Maybe v
98+lookup (NE k) (ConfigTree m) = OM.lookup k m >>= fst
99+lookup (NECons k ks) (ConfigTree m) = OM.lookup k m >>= lookup ks . snd
100+{-# INLINE lookup #-}
101+
102+member :: Ord k => NonEmpty k -> ConfigTree k v -> Bool
103+member (NE k) (ConfigTree m) = OM.member k m
104+member (NECons k ks) (ConfigTree m) = maybe False (member ks . snd) $ OM.lookup k m
105+{-# INLINE member #-}
106+
107+null :: ConfigTree l v -> Bool
108+null (ConfigTree m) = OM.null m
109+{-# INLINE null #-}
110+
111+filter :: Ord k => (v -> Bool) -> ConfigTree k v -> ConfigTree k v
112+filter f (ConfigTree m) =
113+ ConfigTree $
114+ OM.filter (\_ (mv, ConfigTree m') -> isJust mv || not (OM.null m'))
115+ . fmap (\(mv, ct) -> (mv >>= (\v -> if f v then Just v else Nothing), filter f ct))
116+ $ m
117+{-# INLINE filter #-}
118+
119+instance Ord k => Semigroup (ConfigTree k v) where
120+ (ConfigTree lMap) <> (ConfigTree rMap) =
121+ ConfigTree $ OM.unionWithR (\_ (lvm, lm) (rvm, rm) -> (lvm <|> rvm, lm <> rm)) lMap rMap
122+ {-# INLINE (<>) #-}
123+
124+instance Ord k => Monoid (ConfigTree k v) where
125+ mempty = ConfigTree OM.empty
126+
127+instance (Searchable needle k, Searchable needle v) => Searchable needle (ConfigTree k v) where
128+ type
129+ SearchableConstraint needle (ConfigTree k v) res =
130+ (SearchableConstraint needle k res, SearchableConstraint needle v res, Ord (Searched k res))
131+ type Searched (ConfigTree k v) res = ConfigTree (Searched k res) (Searched v res)
132+ searchWith f (ConfigTree oMap) = do
133+ oMap' <- traverseOMap (searchWith f) (searchWith f) oMap
134+ pure $ ConfigTree oMap'
135+ {-# INLINE searchWith #-}
136+
137+deconstructConfigKey :: Text -> NonEmpty Text
138+deconstructConfigKey =
139+ fromMaybe (pure "") . NE.nonEmpty . L.filter (not . T.null) . fmap T.strip . T.splitOn "."
140+{-# INLINE deconstructConfigKey #-}
141+
142+reconstructConfigKey :: (IsString t, Monoid t) => NonEmpty t -> t
143+reconstructConfigKey = fold . NE.intersperse "."
144+{-# INLINE reconstructConfigKey #-}
145+
146+pattern NE :: a -> NonEmpty a
147+pattern NE x = x NE.:| []
148+pattern NECons :: a -> NonEmpty a -> NonEmpty a
149+pattern NECons x y <- (NE.uncons -> (x, Just y))
150+{-# COMPLETE NE, NECons #-}
151+
152+catMaybes :: Ord k => ConfigTree k (Maybe v) -> ConfigTree k v
153+catMaybes = fmap fromJust . filter isJust
154+{-# INLINE catMaybes #-}
155+
156+insert :: Ord k => NonEmpty k -> v -> ConfigTree k v -> ConfigTree k v
157+insert (NE k) v (ConfigTree m) = ConfigTree $ (k, (Just v, subCfg)) OM.<| m
158+ where
159+ subCfg = case OM.lookup k m of
160+ Nothing -> mempty
161+ Just (_, m') -> m'
162+insert (NECons k ks) v (ConfigTree m) = ConfigTree $ case OM.lookup k m of
163+ Nothing -> (k, (Nothing, singleton ks v)) OM.<| m
164+ Just (v', m') -> (k, (v', insert ks v m')) OM.<| m
165+{-# INLINE insert #-}
octopod-common/src/Data/Map/Ordered/Strict/Extra.hsdeleted−15
@@ -1,15 +0,0 @@
1−{-# OPTIONS_GHC -Wno-orphans #-}
2−
3−module Data.Map.Ordered.Strict.Extra
4− (
5− )
6−where
7−
8−import Data.Aeson
9−import Data.Map.Ordered.Strict
10−
11−instance (ToJSON k, ToJSON v) => ToJSON (OMap k v) where
12− toJSON = toJSON . assocs
13−
14−instance (FromJSON k, FromJSON v, Ord k) => FromJSON (OMap k v) where
15− parseJSON = fmap fromList . parseJSON
octopod-css/default.nix+2
@@ -1,10 +1,12 @@
11{ pkgsSrc ? (import ./.. { }).pkgsSrc
22, sources ? import ../nix/sources.nix
33, nix-filter ? import sources.nix-filter
4+, system ? builtins.currentSystem
45}:
56let
67 pkgs = pkgsSrc {
78 overlays = [ (self: super: { nodejs = self.nodejs-10_x; }) ];
9+ inherit system;
810 };
911 production-css = pkgs.mkYarnPackage {
1012 name = "octopod-css";
octopod-css/development/layouts/index.html+321−174
@@ -1,20 +1,22 @@
11<!DOCTYPE html>
22<html lang="en">
3+
34 <head>
45 <meta charset="utf-8">
56 <meta http-equiv="x-ua-compatible" content="ie=edge">
67 <title>Index</title>
78 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
9+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
910 <link rel="stylesheet" href="../styles/style.css">
1011 </head>
12+
1113 <body>
1214
1315 <!--[if IE]>
14− <div class="browserupgrade">
15− You are using an <strong>outdated</strong> browser.
16− Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
17− </div>
16+ <div class="browserupgrade">
17+ You are using an <strong>outdated</strong> browser.
18+ Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
19+ </div>
1820 <![endif]-->
1921
2022 <header class="header">
@@ -63,13 +65,10 @@
6365 Links
6466 </th>
6567 <th>
66− Tag
67− </th>
68− <th>
69− App overrides
68+ App configuration
7069 </th>
7170 <th>
72− <button class="sort" type="button">Deployment overrides</button>
71+ <button class="sort" type="button">Deployment configuration</button>
7372 </th>
7473 <th>
7574 <button class="sort sort--active sort--asc" type="button">Created</button>
@@ -100,19 +99,23 @@
10099 </a>
101100 </div>
102101 </td>
103− <td>
104− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
105− </td>
106102 <td>
107103 <div class="listing listing--for-text">
108104 <div class="listing__item">
109− <b>ENVIRONENT:</b> production
105+ <span class="listing__key">ENVIRONMENT:</span>
106+ <span class="listing__value">production</span>
110107 </div>
111108 <div class="listing__item">
112− <b>app_api_url:</b> http://my-api.my-domain.com
109+ <span class="listing__key">app_api_url:</span>
110+ <span class="listing__value">http://my-api.my-domain.com</span>
113111 </div>
114112 <div class="listing__item">
115− <b>SECRET_CODE:</b> asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen
113+ <span class="listing__key default">SECRET_CODE:</span>
114+ <span class="listing__value">asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen</span>
115+ </div>
116+ <div class="listing__item">
117+ <span class="listing__key default">INFO:</span>
118+ <span class="listing__value default">New Project Staging</span>
116119 </div>
117120 <button class="listing__more expander" type="button">
118121 Show all (5)
@@ -174,9 +177,6 @@
174177 </a>
175178 </div>
176179 </td>
177− <td>
178− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
179− </td>
180180 <td>
181181 <div class="listing listing--for-text">
182182 <div class="listing__item">
@@ -242,9 +242,6 @@
242242 </a>
243243 </div>
244244 </td>
245− <td>
246− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
247− </td>
248245 <td>
249246 <div class="listing listing--for-text">
250247 <div class="listing__item">
@@ -313,9 +310,6 @@
313310 </a>
314311 </div>
315312 </td>
316− <td>
317− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
318− </td>
319313 <td>
320314 <div class="listing listing--for-text">
321315 <div class="listing__item">
@@ -368,7 +362,9 @@
368362 <td colspan="8">
369363 <div class="null null--search">
370364 <b class="null__heading">No results found</b>
371− <div class="null__message">It seems we can’t find any results <br>based on your search</div>
365+ <div class="null__message">It seems we can’t find any results
366+ <br>based on your search
367+ </div>
372368 </div>
373369 <!-- temp -->
374370 &nbsp;&nbsp;&nbsp;&nbsp;или&nbsp;&nbsp;&nbsp;&nbsp;
@@ -389,7 +385,7 @@
389385 </table>
390386 </div>
391387 </div>
392− <button class="data__show-archive expander expander--stand-alone" type="button" >
388+ <button class="data__show-archive expander expander--stand-alone" type="button">
393389 Show Archived deployments (2)
394390 </button>
395391 <div class="data__archive">
@@ -407,10 +403,10 @@
407403 Tag
408404 </th>
409405 <th>
410− App overrides
406+ App configuration
411407 </th>
412408 <th>
413− Deployment overrides
409+ Deployment configuration
414410 </th>
415411 <th>
416412 Created
@@ -434,9 +430,6 @@
434430 <td>
435431 ...
436432 </td>
437− <td>
438− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
439− </td>
440433 <td>
441434 <div class="listing listing--for-text">
442435 <div class="listing__item">
@@ -492,19 +485,31 @@
492485 <td>
493486 ...
494487 </td>
495− <td>
496− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
497− </td>
498488 <td>
499489 <div class="listing listing--for-text">
500490 <div class="listing__item">
501− <b>ENVIRONENT:</b> production
491+ <span class="listing__key">
492+ ENVIRONMENT:
493+ </span>
494+ <span class="listing__value">
495+ production
496+ </span>
502497 </div>
503498 <div class="listing__item">
504− <b>app_api_url:</b> http://my-api.my-domain.com
499+ <span class="listing__key">
500+ app_api_url:
501+ </span>
502+ <span class="listing__value">
503+ http://my-api.my-domain.com
504+ </span>
505505 </div>
506506 <div class="listing__item">
507− <b>SECRET_CODE:</b> asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen
507+ <span class="listing__key item-default">
508+ SECRET_CODE:
509+ </span>
510+ <span class="listing__value item-default">
511+ asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen
512+ </span>
508513 </div>
509514 </div>
510515 </td>
@@ -546,34 +551,34 @@
546551 </div>
547552 </div>
548553 <!-- или:
549− <div class="no-data">
550− <div class="null null--data">
551− <b class="null__heading">Cannot retrieve the data</b>
552− <div class="null__message">Try to reload page</div>
553− </div>
554− <div class="loading loading--enlarged loading--alternate">
555− Loading...
556− </div>
557− </div>
558− -->
554+ <div class="no-data">
555+ <div class="null null--data">
556+ <b class="null__heading">Cannot retrieve the data</b>
557+ <div class="null__message">Try to reload page</div>
558+ </div>
559+ <div class="loading loading--enlarged loading--alternate">
560+ Loading...
561+ </div>
562+ </div>
563+ -->
559564 </div>
560565 </div>
561566 </div>
562567
563568 <!-- или:
564− <div class="no-page">
565− <div class="no-page__inner">
566− <div class="null null--data">
567− <b class="null__heading">Cannot retrieve the data</b>
568− <div class="null__message">Try to reload page</div>
569− </div>
570− или:
571− <div class="loading loading--enlarged loading--alternate">
572− Loading...
573− </div>
574− </div>
575− </div>
576− -->
569+ <div class="no-page">
570+ <div class="no-page__inner">
571+ <div class="null null--data">
572+ <b class="null__heading">Cannot retrieve the data</b>
573+ <div class="null__message">Try to reload page</div>
574+ </div>
575+ или:
576+ <div class="loading loading--enlarged loading--alternate">
577+ Loading...
578+ </div>
579+ </div>
580+ </div>
581+ -->
577582
578583 <div class="popup" id="deployment">
579584 <div class="popup__overlay" aria-hidden="true">
@@ -589,6 +594,7 @@
589594 </h2>
590595 <div class="popup__operations">
591596 <button class="popup__action button button--save" type="button">Save</button>
597+ <button class="popup__action button button--save-loading" type="button" disabled>Save</button>
592598 <button class="popup__action button button--secondary" type="button">Cancel</button>
593599 </div>
594600 <div class="popup__menu drop drop--actions">
@@ -630,113 +636,224 @@
630636 </section>
631637 <section class="deployment__section">
632638 <h3 class="deployment__sub-heading">
633− App overrides
639+ App configuration
634640 </h3>
635641 <div class="deployment__widget">
636642 <div class="overrides">
637− <div class="overrides__item">
638− <div class="overrides__key input">
639− <input class="input__widget" type="text" placeholder="key" value="ENVIRONENT">
640− </div>
641− <div class="overrides__value input">
642− <input class="input__widget" type="text" placeholder="value" value="production">
643+ <button class="overrides__add dash dash--add" type="button">
644+ Add an override
645+ </button>
646+ <div class="overrides__list">
647+ <div class="editable-row">
648+ <div class="editable-row__key input">
649+ <input class="input__widget" type="text" placeholder="key">
650+ </div>
651+ <div class="editable-row__value input">
652+ <input class="input__widget" type="text" placeholder="value">
653+ </div>
654+ <button class="editable-row__delete spot spot--cancel">
655+ Delete
656+ </button>
643657 </div>
644− <button class="overrides__delete spot spot--cancel">
645− Delete
646− </button>
647− </div>
648− <div class="overrides__item">
649− <div class="overrides__key input">
650− <input class="input__widget" type="text" placeholder="key" value="app_api_url">
658+ <div class="editable-row">
659+ <div class="editable-row__key input">
660+ <input class="input__widget" type="text" placeholder="key" value="ENVIRONMENT">
661+ </div>
662+ <div class="editable-row__value input">
663+ <input class="input__widget" type="text" placeholder="value" value="production">
664+ </div>
665+ <button class="editable-row__delete spot spot--cancel">
666+ Delete
667+ </button>
651668 </div>
652− <div class="overrides__value input">
653− <input class="input__widget" type="text" placeholder="value" value="http://my-api.my-domain.com">
669+ <div class="editable-row">
670+ <div class="editable-row__key input">
671+ <input class="input__widget" type="text" placeholder="key" value="app_api_url">
672+ </div>
673+ <div class="editable-row__value input">
674+ <input class="input__widget" type="text" placeholder="value" value="http://my-api.my-domain.com">
675+ </div>
676+ <button class="editable-row__delete spot spot--cancel">
677+ Delete
678+ </button>
654679 </div>
655− <button class="overrides__delete spot spot--cancel">
656− Delete
657− </button>
658− </div>
659− <div class="overrides__item">
660− <div class="overrides__key input">
661− <input class="input__widget" type="text" placeholder="key" value="SECRET_CODE">
680+ <div class="editable-row">
681+ <div class="editable-row__key input input--default">
682+ <input class="input__widget" type="text" placeholder="key" value="SECRET_CODE">
683+ </div>
684+ <div class="editable-row__value input input--default">
685+ <input class="input__widget" type="text" placeholder="value" value="production">
686+ </div>
687+ <button class="editable-row__delete spot spot--cancel">
688+ Delete
689+ </button>
662690 </div>
663− <div class="overrides__value input">
664− <input class="input__widget" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
691+ <div class="editable-row">
692+ <div class="editable-row__key input input--deleted">
693+ <input class="input__widget" type="text" placeholder="key" value="APP_URL" disabled>
694+ </div>
695+ <div class="editable-row__value input input--deleted">
696+ <input class="input__widget" type="text" placeholder="value" value="http://my-api.my-domain.com" disabled>
697+ </div>
698+ <button class="editable-row__delete spot spot--undo">
699+ Delete
700+ </button>
665701 </div>
666− <button class="overrides__delete spot spot--cancel">
667− Delete
668− </button>
669− </div>
670− <div class="overrides__item">
671− <div class="overrides__key input">
672− <input class="input__widget" type="text" placeholder="key">
702+ <div class="editable-row">
703+ <div class="editable-row__key input input--disable">
704+ <input class="input__widget" type="text" placeholder="key" disabled>
705+ </div>
706+ <div class="editable-row__value input input--disable">
707+ <input class="input__widget" type="text" placeholder="value" value="production" disabled>
708+ </div>
673709 </div>
674− <div class="overrides__value input">
675− <input class="input__widget" type="text" placeholder="value">
710+ <div class="editable-row">
711+ <div class="editable-row__key input">
712+ <input class="input__widget" type="text" placeholder="key" value="LiveSearch">
713+ <ul class="input__dropdown">
714+ <li class="input__suggest"><b>S</b>ECRET_PASSWORD</li>
715+ <li class="input__suggest"><b>S</b>ECRET_INFO</li>
716+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
717+ </li>
718+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
719+ </li>
720+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
721+ </li>
722+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
723+ </li>
724+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
725+ </li>
726+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
727+ </li>
728+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
729+ </li>
730+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
731+ </li>
732+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
733+ </li>
734+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
735+ </li>
736+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
737+ </li>
738+ </ul>
739+ </div>
740+ <div class="editable-row__value input">
741+ <input class="input__widget" type="text" placeholder="value" value="Value">
742+ </div>
743+ <button class="editable-row__delete spot spot--cancel">
744+ Delete
745+ </button>
676746 </div>
677− <button class="overrides__delete spot spot--cancel">
678− Delete
679− </button>
680747 </div>
681− <button class="overrides__add dash dash--add" type="button">
682− Add an override
683− </button>
684748 </div>
685749 </div>
686750 </section>
687751 <section class="deployment__section">
688752 <h3 class="deployment__sub-heading">
689− Deployment overrides
753+ Deployment configuration
690754 </h3>
691755 <div class="deployment__widget">
692756 <div class="overrides">
693− <div class="overrides__item">
694− <div class="overrides__key input">
695− <input class="input__widget" type="text" placeholder="key" value="ENVIRONENT">
757+ <button class="overrides__add dash dash--add" type="button">
758+ Add an override
759+ </button>
760+ <div class="overrides__list">
761+ <div class="editable-row">
762+ <div class="editable-row__key input">
763+ <input class="input__widget" type="text" placeholder="key">
764+ </div>
765+ <div class="editable-row__value input">
766+ <input class="input__widget" type="text" placeholder="value">
767+ </div>
768+ <button class="editable-row__delete spot spot--cancel">
769+ Delete
770+ </button>
696771 </div>
697− <div class="overrides__value input">
698− <input class="input__widget" type="text" placeholder="value" value="production">
772+ <div class="editable-row">
773+ <div class="editable-row__key input">
774+ <input class="input__widget" type="text" placeholder="key" value="ENVIRONENT">
775+ </div>
776+ <div class="editable-row__value input">
777+ <input class="input__widget" type="text" placeholder="value" value="production">
778+ </div>
779+ <button class="editable-row__delete spot spot--cancel">
780+ Delete
781+ </button>
699782 </div>
700− <button class="overrides__delete spot spot--cancel">
701− Delete
702− </button>
703− </div>
704− <div class="overrides__item">
705− <div class="overrides__key input">
706− <input class="input__widget" type="text" placeholder="key" value="app_api_url">
783+ <div class="editable-row">
784+ <div class="editable-row__key input">
785+ <input class="input__widget" type="text" placeholder="key" value="app_api_url">
786+ </div>
787+ <div class="editable-row__value input">
788+ <input class="input__widget" type="text" placeholder="value" value="http://my-api.my-domain.com">
789+ </div>
790+ <button class="editable-row__delete spot spot--cancel">
791+ Delete
792+ </button>
707793 </div>
708− <div class="overrides__value input">
709− <input class="input__widget" type="text" placeholder="value" value="http://my-api.my-domain.com">
794+ <div class="editable-row">
795+ <div class="editable-row__key input">
796+ <input class="input__widget input--default" type="text" placeholder="key" value="SECRET_CODE">
797+ </div>
798+ <div class="editable-row__value input">
799+ <input class="input__widget" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
800+ </div>
801+ <button class="editable-row__delete spot spot--cancel">
802+ Delete
803+ </button>
710804 </div>
711− <button class="overrides__delete spot spot--cancel">
712− Delete
713− </button>
714− </div>
715− <div class="overrides__item">
716− <div class="overrides__key input">
717− <input class="input__widget" type="text" placeholder="key" value="SECRET_CODE">
805+ <div class="editable-row loader">
806+ <div class="editable-row__placeholder"></div>
807+ <div class="editable-row__placeholder"></div>
808+ <div class="editable-row__delete spot spot--loader"></div>
718809 </div>
719− <div class="overrides__value input">
720− <input class="input__widget" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
810+ <div class="editable-row loader">
811+ <div class="editable-row__placeholder"></div>
812+ <div class="editable-row__placeholder"></div>
813+ <div class="editable-row__delete spot spot--loader"></div>
721814 </div>
722− <button class="overrides__delete spot spot--cancel">
723− Delete
724− </button>
725− </div>
726− <div class="overrides__item">
727− <div class="overrides__key input">
728− <input class="input__widget" type="text" placeholder="key">
815+ <div class="editable-row loader">
816+ <div class="editable-row__placeholder"></div>
817+ <div class="editable-row__placeholder"></div>
818+ <div class="editable-row__delete spot spot--loader"></div>
819+ </div>
820+ <div class="editable-row">
821+ <div class="editable-row__key input input--error">
822+ <input class="input__widget input--default" type="text" placeholder="key" value="SECRET_CODE">
823+ <div class="input__output">Duplicate key values</div>
824+ </div>
825+ <div class="editable-row__value input">
826+ <input class="input__widget" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
827+ </div>
828+ <button class="editable-row__delete spot spot--cancel">
829+ Delete
830+ </button>
729831 </div>
730− <div class="overrides__value input">
731− <input class="input__widget" type="text" placeholder="value">
832+ <div class="editable-row">
833+ <div class="editable-row__key input input--error">
834+ <input class="input__widget" type="text" placeholder="key" value="ENVIRONMENT">
835+ <div class="input__output">Duplicate key values</div>
836+ </div>
837+ <div class="editable-row__value input">
838+ <input class="input__widget" type="text" placeholder="value" value="production">
839+ </div>
840+ <button class="editable-row__delete spot spot--cancel">
841+ Delete
842+ </button>
843+ </div>
844+ <div class="editable-row">
845+ <div class="editable-row__key input input--error">
846+ <input class="input__widget" type="text" placeholder="key" value="">
847+ <div class="input__output">Empty key value</div>
848+ </div>
849+ <div class="editable-row__value input">
850+ <input class="input__widget" type="text" placeholder="value" value="production">
851+ </div>
852+ <button class="editable-row__delete spot spot--cancel">
853+ Delete
854+ </button>
732855 </div>
733− <button class="overrides__delete spot spot--cancel">
734− Delete
735− </button>
736856 </div>
737− <button class="overrides__add dash dash--add" type="button">
738− Add an override
739− </button>
740857 </div>
741858 </div>
742859 </section>
@@ -784,42 +901,70 @@
784901 </div>
785902 <div class="popup__content">
786903 <div class="log">
787− 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
788− <br>"rm" -f inplace/bin/runghc
789− <br>echo 'exec "$executablename" ${1+"$@"}' >> inplace/lib/bin/ghc-iserv-dyn
790− <br>echo '#!/bin/sh' >> inplace/bin/runghc
791− <br>chmod +x inplace/lib/bin/ghc-iserv-dyn
792− <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/runghc"' >> inplace/bin/runghc
793− <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
904+ 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}"'
905+ >> inplace/lib/bin/ghc-iserv-dyn
906+ <br>"rm" -f inplace/bin/runghc
907+ <br>echo 'exec "$executablename" ${1+"$@"}' >> inplace/lib/bin/ghc-iserv-dyn
908+ <br>echo '#!/bin/sh' >> inplace/bin/runghc
909+ <br>chmod +x inplace/lib/bin/ghc-iserv-dyn
910+ <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/runghc"' >> inplace/bin/runghc
911+ <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
794912 <br>/usr/local/bin/ginstall -c -m 755 utils/hpc/dist-install/build/tmp/hpc inplace/lib/bin/hpc
795− <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/runghc
796− <br>"rm" -f inplace/bin/hpc
797− <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
798− <br>echo 'pgmgcc="cc"' >> inplace/bin/runghc
799− <br>echo '#!/bin/sh' >> inplace/bin/hpc
913+ <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/runghc
914+ <br>"rm" -f inplace/bin/hpc
915+ <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
916+ <br>echo 'pgmgcc="cc"' >> inplace/bin/runghc
917+ <br>echo '#!/bin/sh' >> inplace/bin/hpc
800918 <br>echo 'ghcprog="ghc-stage2"' >> "inplace/bin/runghc"
801− <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/hpc"' >> inplace/bin/hpc
802− <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/process/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/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/bin/runghc
803− <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
804− <br>cat utils/runghc/runghc.wrapper >> inplace/bin/runghc
805− <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/hpc
806− <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
807− <br>chmod +x inplace/bin/runghc
808− <br>echo 'pgmgcc="cc"' >> inplace/bin/hpc
809− <br>"rm" -f utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
810− <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/hpc/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/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/bin/hpc
811− <br>echo '#!/bin/sh' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
812− <br>cat utils/hpc/hpc.wrapper >> inplace/bin/hpc
813− <br>echo 'export DYLD_LIBRARY_PATH="libraries/Cabal/Cabal/dist-install/build:libraries/process/dist-install/build:libraries/parsec/dist-install/build:libraries/text/dist-install/build:libraries/template-haskell/dist-install/build:libraries/pretty/dist-install/build:libraries/ghc-boot-th/dist-install/build:libraries/mtl/dist-install/build:libraries/transformers/dist-install/build:libraries/directory/dist-install/build:libraries/unix/dist-install/build:libraries/time/dist-install/build:libraries/filepath/dist-install/build:libraries/binary/dist-install/build:libraries/containers/containers/dist-install/build:libraries/bytestring/dist-install/build:libraries/deepseq/dist-install/build:libraries/array/dist-install/build:libraries/base/dist-install/build:libraries/ghc-bignum/dist-install/build:libraries/ghc-prim/dist-install/build:rts/dist/build${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
814− <br>chmod +x inplace/bin/hpc
815− <br>echo 'exec "utils/ghc-cabal/dist-install/build/tmp/ghc-cabal" ${1+"$@"}' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
816− <br>chmod +x utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
817− <br><<ghc: 2787765536 bytes, 171 GCs, 20282449/67935792 avg/max bytes residency (8 samples), 144M in use, 0.000 INIT (0.003 elapsed), 1.738 MUT (2.286 elapsed), 0.567 GC (0.611 elapsed) :ghc>>
818− <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0 -package-id binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619 -package-id ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id time-1.9.3 -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/HsToCore.hs -o compiler/stage2/build/GHC/HsToCore.o -dyno compiler/stage2/build/GHC/HsToCore.dyn_o
819− <br><<ghc: 766891168 bytes, 91 GCs, 14722329/42245856 avg/max bytes residency (6 samples), 91M in use, 0.000 INIT (0.003 elapsed), 0.361 MUT (0.510 elapsed), 0.236 GC (0.258 elapsed) :ghc>>
820− <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0 -package-id binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619 -package-id ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id time-1.9.3 -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/Iface/Ext/Ast.hs -o compiler/stage2/build/GHC/Iface/Ext/Ast.o -dyno compiler/stage2/build/GHC/Iface/Ext/Ast.dyn_o
821− <br><<ghc: 3067019736 bytes, 347 GCs, 25353490/71392248 avg/max bytes residency (8 samples), 184M in use, 0.000 INIT (0.002 elapsed), 1.416 MUT (1.823 elapsed), 0.554 GC (0.595 elapsed) :ghc>>
822− <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompil...
919+ <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/hpc"' >> inplace/bin/hpc
920+ <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/process/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/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}"'
921+ >> inplace/bin/runghc
922+ <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
923+ <br>cat utils/runghc/runghc.wrapper >> inplace/bin/runghc
924+ <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/hpc
925+ <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
926+ <br>chmod +x inplace/bin/runghc
927+ <br>echo 'pgmgcc="cc"' >> inplace/bin/hpc
928+ <br>"rm" -f utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
929+ <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/hpc/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/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}"'
930+ >> inplace/bin/hpc
931+ <br>echo '#!/bin/sh' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
932+ <br>cat utils/hpc/hpc.wrapper >> inplace/bin/hpc
933+ <br>echo 'export DYLD_LIBRARY_PATH="libraries/Cabal/Cabal/dist-install/build:libraries/process/dist-install/build:libraries/parsec/dist-install/build:libraries/text/dist-install/build:libraries/template-haskell/dist-install/build:libraries/pretty/dist-install/build:libraries/ghc-boot-th/dist-install/build:libraries/mtl/dist-install/build:libraries/transformers/dist-install/build:libraries/directory/dist-install/build:libraries/unix/dist-install/build:libraries/time/dist-install/build:libraries/filepath/dist-install/build:libraries/binary/dist-install/build:libraries/containers/containers/dist-install/build:libraries/bytestring/dist-install/build:libraries/deepseq/dist-install/build:libraries/array/dist-install/build:libraries/base/dist-install/build:libraries/ghc-bignum/dist-install/build:libraries/ghc-prim/dist-install/build:rts/dist/build${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"'
934+ >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
935+ <br>chmod +x inplace/bin/hpc
936+ <br>echo 'exec "utils/ghc-cabal/dist-install/build/tmp/ghc-cabal" ${1+"$@"}' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
937+ <br>chmod +x utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
938+ <br>
939+ <
940+ <ghc: 2787765536 bytes, 171 GCs, 20282449
941+ /67935792 avg/max bytes residency (8 samples), 144M in use, 0.000 INIT (0.003 elapsed), 1.738 MUT (2.286 elapsed), 0.567 GC (0.611 elapsed) :ghc>>
942+ <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i
943+ -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build
944+ -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0 -package-id
945+ binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619 -package-id
946+ ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id time-1.9.3
947+ -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build
948+ -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/HsToCore.hs -o compiler/stage2/build/GHC/HsToCore.o
949+ -dyno compiler/stage2/build/GHC/HsToCore.dyn_o
950+ <br>
951+ <
952+ <ghc: 766891168 bytes, 91 GCs, 14722329
953+ /42245856 avg/max bytes residency (6 samples), 91M in use, 0.000 INIT (0.003 elapsed), 0.361 MUT (0.510 elapsed), 0.236 GC (0.258 elapsed) :ghc>>
954+ <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages
955+ -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build
956+ -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0
957+ -package-id binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619
958+ -package-id ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id
959+ time-1.9.3 -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build
960+ -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/Iface/Ext/Ast.hs -o compiler/stage2/build/GHC/Iface/Ext/Ast.o
961+ -dyno compiler/stage2/build/GHC/Iface/Ext/Ast.dyn_o
962+ <br>
963+ <
964+ <ghc: 3067019736 bytes, 347 GCs, 25353490
965+ /71392248 avg/max bytes residency (8 samples), 184M in use, 0.000 INIT (0.002 elapsed), 1.416 MUT (1.823 elapsed), 0.554 GC (0.595 elapsed) :ghc>>
966+ <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages
967+ -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompil...
823968 </div>
824969 <!-- temp -->
825970 или:
@@ -880,6 +1025,8 @@
8801025 <script src="../scripts/_data.js"></script>
8811026 <script src="../scripts/_popup.js"></script>
8821027 <script src="../scripts/_classic-popup.js"></script>
1028+ <script src="../scripts/_collapse.js"></script>
8831029
8841030 </body>
1031+
8851032</html>
octopod-css/development/layouts/staging.html+687−168
@@ -1,20 +1,22 @@
11<!DOCTYPE html>
22<html lang="en">
3+
34 <head>
45 <meta charset="utf-8">
56 <meta http-equiv="x-ua-compatible" content="ie=edge">
67 <title>Deployment</title>
78 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
9+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
910 <link rel="stylesheet" href="../styles/style.css">
1011 </head>
12+
1113 <body>
1214
1315 <!--[if IE]>
14− <div class="browserupgrade">
15− You are using an <strong>outdated</strong> browser.
16− Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
17− </div>
16+ <div class="browserupgrade">
17+ You are using an <strong>outdated</strong> browser.
18+ Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
19+ </div>
1820 <![endif]-->
1921
2022 <header class="header">
@@ -114,33 +116,195 @@
114116 App overrides
115117 </h3>
116118 <div class="deployment__widget">
117− <div class="listing listing--for-text listing--larger">
118− <div class="listing__item">
119− <b>ENVIRONENT:</b> production
119+ <div class="padded">
120+ <div class="row">
121+ <span class="key-custom-edited">ENVIRONENT: </span><span class="value-edited">production</span>
122+ </div>
123+ <div class="row">
124+ <span class="key-custom-edited">Secret_word: </span><span class="value-edited">Hello</span>
125+ </div>
126+ <div class="row">
127+ <span class="key-default-pristine">SECRET_CODE: </span><span class="value-edited">asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij</span>
120128 </div>
121− <div class="listing__item">
122− <b>app_api_url:</b> http: //my-api.my-domain.com
129+ <div class="row">
130+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL </span><span class="value-deleted">http://my-api.my-domain.com</span>
123131 </div>
124− <div class="listing__item">
125− <b>SECRET_CODE:</b> asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij
132+ <div class="row">
133+ <span class="key-deleted">air.vaultEnv.air-sso:</span> <span class="value-unknown">unknown</span>
134+ </div>
135+ <div class="collapse collapse--project">
136+ <button class="collapse__head" type="button">
137+ air
138+ </button>
139+ <div class="collapse__body">
140+ <div class="collapse__inner">
141+ <div class="row">
142+ <span class="key-default-pristine">vaultEngineName: </span><span class="value-pristine">staging</span>
143+ </div>
144+ <div class="collapse collapse--project">
145+ <button class="collapse__head" type="button">
146+ vaultEnv
147+ </button>
148+ <div class="collapse__body">
149+ <div class="collapse__inner">
150+ <div class="row">
151+ <span class="key-default-pristine">air-sso.engineOverride: </span><span class="value-pristine">staging</span>
152+ </div>
153+ <div class="collapse collapse--project">
154+ <button class="collapse__head" type="button">
155+ air-sso.mapping[0]
156+ </button>
157+ <div class="collapse__body">
158+ <div class="collapse__inner">
159+ <div class="row">
160+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
161+ </div>
162+ <div class="row">
163+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
164+ </div>
165+ </div>
166+ <div class="row">
167+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
168+ </div>
169+ </div>
170+ </div>
171+ <div class="collapse collapse--project">
172+ <button class="collapse__head" type="button">
173+ travolutionary.mapping[0]
174+ </button>
175+ <div class="collapse__body">
176+ <div class="collapse__inner">
177+ <div class="row">
178+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
179+ </div>
180+ <div class="row">
181+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
182+ </div>
183+ </div>
184+ </div>
185+ </div>
186+ <div class="collapse collapse--project">
187+ <button class="collapse__head" type="button">
188+ travolutionary.mapping[1]
189+ </button>
190+ <div class="collapse__body">
191+ <div class="collapse__inner">
192+ <div class="row">
193+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
194+ </div>
195+ <div class="row">
196+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
197+ </div>
198+ </div>
199+ </div>
200+ </div>
201+ <div class="row">
202+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
203+ </div>
204+ </div>
205+ </div>
206+ </div>
207+ </div>
208+ <div class="row">
209+ <span class="key-custom-edited">app_api_url: </span><span class="value-edited">http: //my-api.my-domain.com</span>
210+ </div>
211+ </div>
212+ </div>
213+ <div class="collapse collapse--project">
214+ <button class="collapse__head" type="button">
215+ sso
216+ </button>
217+ <div class="collapse__body">
218+ <div class="collapse__inner">
219+ <div class="row">
220+ <span class="key-default-pristine">vaultEngineName: </span><span class="value-pristine">staging</span>
221+ </div>
222+ <div class="collapse collapse--project">
223+ <button class="collapse__head" type="button">
224+ vaultEnv
225+ </button>
226+ <div class="collapse__body">
227+ <div class="collapse__inner">
228+ <div class="row">
229+ <span class="key-default-pristine">air-sso.engineOverride: </span><span class="value-pristine">staging</span>
230+ </div>
231+ <div class="collapse collapse--project">
232+ <button class="collapse__head" type="button">
233+ air-sso.mapping[0]
234+ </button>
235+ <div class="collapse__body">
236+ <div class="collapse__inner">
237+ <div class="row">
238+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
239+ </div>
240+ <div class="row">
241+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
242+ </div>
243+ </div>
244+ <div class="row">
245+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
246+ </div>
247+ </div>
248+ </div>
249+ <div class="collapse collapse--project">
250+ <button class="collapse__head" type="button">
251+ travolutionary.mapping[0]
252+ </button>
253+ <div class="collapse__body">
254+ <div class="collapse__inner">
255+ <div class="row">
256+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
257+ </div>
258+ <div class="row">
259+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
260+ </div>
261+ </div>
262+ </div>
263+ </div>
264+ <div class="collapse collapse--project">
265+ <button class="collapse__head" type="button">
266+ travolutionary.mapping[1]
267+ </button>
268+ <div class="collapse__body">
269+ <div class="collapse__inner">
270+ <div class="row">
271+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
272+ </div>
273+ <div class="row">
274+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
275+ </div>
276+ </div>
277+ </div>
278+ </div>
279+ <div class="row">
280+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
281+ </div>
282+ </div>
283+ </div>
284+ </div>
285+ </div>
286+ <div class="row">
287+ <span class="key-custom-edited">app_api_url: </span><span class="value-edited">http: //my-api.my-domain.com</span>
288+ </div>
289+ </div>
126290 </div>
127291 </div>
128292 </div>
129293 </section>
130294 <section class="deployment__section">
131295 <h3 class="deployment__sub-heading">
132− Deployment overrides
296+ Staging overrides
133297 </h3>
134298 <div class="deployment__widget">
135− <div class="listing listing--for-text listing--larger">
136− <div class="listing__item">
137− <b>ENVIRONENT:</b> production
299+ <div class="padded">
300+ <div class="row">
301+ <span class="key-custom-edited">ENVIRONENT: </span><span class="value-edited">http://my-api.my-domain.com</span>
138302 </div>
139− <div class="listing__item">
140− <b>app_api_url:</b> http: //my-api.my-domain.com
303+ <div class="row">
304+ <span class="key-default-pristine">SECRET_CODE: </span><span class="value-edited">http://my-api.my-domain.com</span>
141305 </div>
142− <div class="listing__item">
143− <b>SECRET_CODE:</b> asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij
306+ <div class="row">
307+ <span class="key-default-pristine">ENVIRONENT: </span><span class="value-pristine">production</span>
144308 </div>
145309 </div>
146310 </div>
@@ -158,13 +322,10 @@
158322 Action type
159323 </th>
160324 <th>
161− Image tag
325+ App configuration
162326 </th>
163327 <th>
164− App overrides
165− </th>
166− <th>
167− Deployment overrides
328+ Deployment configuration
168329 </th>
169330 <th>
170331 Exit code
@@ -182,13 +343,12 @@
182343 <td>
183344 Create
184345 </td>
185− <td>
186− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
187− </td>
188346 <td>
189347 <div class="listing listing--for-text">
190348 <div class="listing__item">
191− <b>ENVIRONENT:</b> production
349+ <b>ENVIRONENT:</b>
350+ <div class="listing__placeholder listing__placeholder__value"></div>
351+ <div class="listing__spinner"></div>
192352 </div>
193353 <div class="listing__item">
194354 <b>app_api_url:</b> http://my-api.my-domain.com
@@ -231,9 +391,6 @@
231391 <td>
232392 Edit
233393 </td>
234− <td>
235− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
236− </td>
237394 <td>
238395 <div class="listing listing--for-text">
239396 <div class="listing__item">
@@ -274,9 +431,6 @@
274431 <td>
275432 Update
276433 </td>
277− <td>
278− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
279− </td>
280434 <td>
281435 <div class="listing listing--for-text">
282436 <div class="listing__item">
@@ -317,9 +471,6 @@
317471 <td>
318472 Delete
319473 </td>
320− <td>
321− fa3a5bd4cc1134ae259c34d4cf530be0f0996a0b
322− </td>
323474 <td>
324475 <div class="listing listing--for-text">
325476 <div class="listing__item">
@@ -396,19 +547,19 @@
396547 </div>
397548
398549 <!-- или:
399− <div class="no-page">
400− <div class="no-page__inner">
401− <div class="null null--data">
402− <b class="null__heading">Cannot retrieve the data</b>
403− <div class="null__message">Try to reload page</div>
404− </div>
405− или:
406− <div class="loading loading--enlarged loading--alternate">
407− Loading...
408− </div>
409− </div>
410− </div>
411− -->
550+ <div class="no-page">
551+ <div class="no-page__inner">
552+ <div class="null null--data">
553+ <b class="null__heading">Cannot retrieve the data</b>
554+ <div class="null__message">Try to reload page</div>
555+ </div>
556+ или:
557+ <div class="loading loading--enlarged loading--alternate">
558+ Loading...
559+ </div>
560+ </div>
561+ </div>
562+ -->
412563
413564 <div class="popup" id="deployment">
414565 <div class="popup__overlay" aria-hidden="true">
@@ -455,113 +606,451 @@
455606 </section>
456607 <section class="deployment__section">
457608 <h3 class="deployment__sub-heading">
458− App overrides
609+ App configuration
459610 </h3>
460611 <div class="deployment__widget">
461612 <div class="overrides">
462− <div class="overrides__item">
463− <div class="overrides__key input">
464− <input class="input__widget" type="text" placeholder="key" value="ENVIRONENT">
465− </div>
466− <div class="overrides__value input">
467− <input class="input__widget" type="text" placeholder="value" value="production">
468− </div>
469− <button class="overrides__delete spot spot--cancel">
470− Delete
471− </button>
472− </div>
473− <div class="overrides__item">
474− <div class="overrides__key input">
475− <input class="input__widget" type="text" placeholder="key" value="app_api_url">
613+ <button class="overrides__add dash dash--add" type="button">
614+ Add an override
615+ </button>
616+ <div class="overrides__list">
617+ <div class="editable-row">
618+ <div class="editable-row__key input">
619+ <input class="input__widget" type="text" placeholder="key">
620+ </div>
621+ <div class="editable-row__value input">
622+ <input class="input__widget" type="text" placeholder="value">
623+ </div>
624+ <button class="editable-row__delete spot spot--cancel">
625+ Delete
626+ </button>
476627 </div>
477− <div class="overrides__value input">
478− <input class="input__widget" type="text" placeholder="value" value="http://my-api.my-domain.com">
628+ <div class="editable-row">
629+ <div class="editable-row__key input">
630+ <input class="input__widget key-custom-edited" type="text" placeholder="key" value="ENVIRONENT">
631+ </div>
632+ <div class="editable-row__value input">
633+ <input class="input__widget value-edited" type="text" placeholder="value" value="production">
634+ </div>
635+ <button class="editable-row__delete spot spot--cancel">
636+ Delete
637+ </button>
479638 </div>
480− <button class="overrides__delete spot spot--cancel">
481− Delete
482− </button>
483− </div>
484− <div class="overrides__item">
485− <div class="overrides__key input">
486− <input class="input__widget" type="text" placeholder="key" value="SECRET_CODE">
639+ <div class="editable-row">
640+ <div class="editable-row__key input">
641+ <input class="input__widget key-custom-edited" type="text" placeholder="key" value="app_api_url">
642+ </div>
643+ <div class="editable-row__value input">
644+ <input class="input__widget value-edited" type="text" placeholder="value" value="http://my-api.my-domain.com">
645+ </div>
646+ <button class="editable-row__delete spot spot--cancel">
647+ Delete
648+ </button>
487649 </div>
488− <div class="overrides__value input">
489− <input class="input__widget" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
650+ <div class="editable-row">
651+ <div class="editable-row__key input">
652+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
653+ </div>
654+ <div class="editable-row__value input">
655+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
656+ </div>
657+ <button class="editable-row__delete spot spot--cancel">
658+ Delete
659+ </button>
490660 </div>
491− <button class="overrides__delete spot spot--cancel">
492− Delete
493− </button>
494661 </div>
495− <div class="overrides__item">
496− <div class="overrides__key input">
497− <input class="input__widget" type="text" placeholder="key">
662+ <div class="overrides__tree">
663+ <div class="collapse collapse--project">
664+ <button class="collapse__head" type="button">
665+ air
666+ </button>
667+ <div class="collapse__body">
668+ <div class="collapse__inner">
669+ <div class="editable-row">
670+ <div class="editable-row__key input">
671+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
672+ </div>
673+ <div class="editable-row__value input">
674+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
675+ </div>
676+ <button class="editable-row__delete spot spot--cancel">
677+ Delete
678+ </button>
679+ </div>
680+ <div class="collapse collapse--project">
681+ <button class="collapse__head" type="button">
682+ vaultEnv
683+ </button>
684+ <div class="collapse__body">
685+ <div class="collapse__inner">
686+ <div class="editable-row">
687+ <div class="editable-row__key input">
688+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
689+ </div>
690+ <div class="editable-row__value input">
691+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
692+ </div>
693+ <button class="editable-row__delete spot spot--cancel">
694+ Delete
695+ </button>
696+ </div>
697+ <div class="collapse collapse--project">
698+ <button class="collapse__head" type="button">
699+ air-sso.mapping[0]
700+ </button>
701+ <div class="collapse__body">
702+ <div class="collapse__inner">
703+ <div class="editable-row">
704+ <div class="editable-row__key input">
705+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
706+ </div>
707+ <div class="editable-row__value input">
708+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
709+ </div>
710+ <button class="editable-row__delete spot spot--cancel">
711+ Delete
712+ </button>
713+ </div>
714+ <div class="editable-row">
715+ <div class="editable-row__key input">
716+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
717+ </div>
718+ <div class="editable-row__value input">
719+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
720+ </div>
721+ <button class="editable-row__delete spot spot--cancel">
722+ Delete
723+ </button>
724+ </div>
725+ </div>
726+ <div class="editable-row">
727+ <div class="editable-row__key input">
728+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
729+ </div>
730+ <div class="editable-row__value input">
731+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
732+ </div>
733+ <button class="editable-row__delete spot spot--cancel">
734+ Delete
735+ </button>
736+ </div>
737+ </div>
738+ </div>
739+ <div class="collapse collapse--project">
740+ <button class="collapse__head" type="button">
741+ travolutionary.mapping[0]
742+ </button>
743+ <div class="collapse__body">
744+ <div class="collapse__inner">
745+ <div class="editable-row">
746+ <div class="editable-row__key input">
747+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
748+ </div>
749+ <div class="editable-row__value input">
750+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
751+ </div>
752+ <button class="editable-row__delete spot spot--cancel">
753+ Delete
754+ </button>
755+ </div>
756+ <div class="editable-row">
757+ <div class="editable-row__key input">
758+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
759+ </div>
760+ <div class="editable-row__value input">
761+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
762+ </div>
763+ <button class="editable-row__delete spot spot--cancel">
764+ Delete
765+ </button>
766+ </div>
767+ </div>
768+ </div>
769+ </div>
770+ <div class="collapse collapse--project">
771+ <button class="collapse__head" type="button">
772+ travolutionary.mapping[1]
773+ </button>
774+ <div class="collapse__body">
775+ <div class="collapse__inner">
776+ <div class="editable-row">
777+ <div class="editable-row__key input">
778+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
779+ </div>
780+ <div class="editable-row__value input">
781+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
782+ </div>
783+ <button class="editable-row__delete spot spot--cancel">
784+ Delete
785+ </button>
786+ </div>
787+ <div class="editable-row">
788+ <div class="editable-row__key input">
789+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
790+ </div>
791+ <div class="editable-row__value input">
792+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
793+ </div>
794+ <button class="editable-row__delete spot spot--cancel">
795+ Delete
796+ </button>
797+ </div>
798+ </div>
799+ </div>
800+ </div>
801+ <div class="editable-row">
802+ <div class="editable-row__key input">
803+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
804+ </div>
805+ <div class="editable-row__value input">
806+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
807+ </div>
808+ <button class="editable-row__delete spot spot--cancel">
809+ Delete
810+ </button>
811+ </div>
812+ </div>
813+ </div>
814+ </div>
815+ </div>
816+ <div class="editable-row">
817+ <div class="editable-row__key input">
818+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
819+ </div>
820+ <div class="editable-row__value input">
821+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
822+ </div>
823+ <button class="editable-row__delete spot spot--cancel">
824+ Delete
825+ </button>
826+ </div>
827+ </div>
498828 </div>
499− <div class="overrides__value input">
500− <input class="input__widget" type="text" placeholder="value">
829+ <div class="collapse collapse--project">
830+ <button class="collapse__head" type="button">
831+ sso
832+ </button>
833+ <div class="collapse__body">
834+ <div class="collapse__inner">
835+ <div class="editable-row">
836+ <div class="editable-row__key input">
837+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
838+ </div>
839+ <div class="editable-row__value input">
840+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
841+ </div>
842+ <button class="editable-row__delete spot spot--cancel">
843+ Delete
844+ </button>
845+ </div>
846+ <div class="collapse collapse--project">
847+ <button class="collapse__head" type="button">
848+ vaultEnv
849+ </button>
850+ <div class="collapse__body">
851+ <div class="collapse__inner">
852+ <div class="editable-row">
853+ <div class="editable-row__key input">
854+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
855+ </div>
856+ <div class="editable-row__value input">
857+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
858+ </div>
859+ <button class="editable-row__delete spot spot--cancel">
860+ Delete
861+ </button>
862+ </div>
863+ <div class="collapse collapse--project">
864+ <button class="collapse__head" type="button">
865+ air-sso.mapping[0]
866+ </button>
867+ <div class="collapse__body">
868+ <div class="collapse__inner">
869+ <div class="editable-row">
870+ <div class="editable-row__key input">
871+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
872+ </div>
873+ <div class="editable-row__value input">
874+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
875+ </div>
876+ <button class="editable-row__delete spot spot--cancel">
877+ Delete
878+ </button>
879+ </div>
880+ <div class="editable-row">
881+ <div class="editable-row__key input">
882+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
883+ </div>
884+ <div class="editable-row__value input">
885+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
886+ </div>
887+ <button class="editable-row__delete spot spot--cancel">
888+ Delete
889+ </button>
890+ </div>
891+ </div>
892+ <div class="editable-row">
893+ <div class="editable-row__key input">
894+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
895+ </div>
896+ <div class="editable-row__value input">
897+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
898+ </div>
899+ <button class="editable-row__delete spot spot--cancel">
900+ Delete
901+ </button>
902+ </div>
903+ </div>
904+ </div>
905+ <div class="collapse collapse--project">
906+ <button class="collapse__head" type="button">
907+ travolutionary.mapping[0]
908+ </button>
909+ <div class="collapse__body">
910+ <div class="collapse__inner">
911+ <div class="editable-row">
912+ <div class="editable-row__key input">
913+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
914+ </div>
915+ <div class="editable-row__value input">
916+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
917+ </div>
918+ <button class="editable-row__delete spot spot--cancel">
919+ Delete
920+ </button>
921+ </div>
922+ <div class="editable-row">
923+ <div class="editable-row__key input">
924+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
925+ </div>
926+ <div class="editable-row__value input">
927+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
928+ </div>
929+ <button class="editable-row__delete spot spot--cancel">
930+ Delete
931+ </button>
932+ </div>
933+ </div>
934+ </div>
935+ </div>
936+ <div class="collapse collapse--project">
937+ <button class="collapse__head" type="button">
938+ travolutionary.mapping[1]
939+ </button>
940+ <div class="collapse__body">
941+ <div class="collapse__inner">
942+ <div class="editable-row">
943+ <div class="editable-row__key input">
944+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
945+ </div>
946+ <div class="editable-row__value input">
947+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
948+ </div>
949+ <button class="editable-row__delete spot spot--cancel">
950+ Delete
951+ </button>
952+ </div>
953+ <div class="editable-row">
954+ <div class="editable-row__key input">
955+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
956+ </div>
957+ <div class="editable-row__value input">
958+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
959+ </div>
960+ <button class="editable-row__delete spot spot--cancel">
961+ Delete
962+ </button>
963+ </div>
964+ </div>
965+ </div>
966+ </div>
967+ <div class="editable-row">
968+ <div class="editable-row__key input">
969+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
970+ </div>
971+ <div class="editable-row__value input">
972+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
973+ </div>
974+ <button class="editable-row__delete spot spot--cancel">
975+ Delete
976+ </button>
977+ </div>
978+ </div>
979+ </div>
980+ </div>
981+ </div>
982+ <div class="editable-row">
983+ <div class="editable-row__key input">
984+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
985+ </div>
986+ <div class="editable-row__value input">
987+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
988+ </div>
989+ <button class="editable-row__delete spot spot--cancel">
990+ Delete
991+ </button>
992+ </div>
993+ </div>
501994 </div>
502− <button class="overrides__delete spot spot--cancel">
503− Delete
504− </button>
505995 </div>
506− <button class="overrides__add dash dash--add" type="button">
507− Add an override
508− </button>
509996 </div>
510997 </div>
511998 </section>
512999 <section class="deployment__section">
5131000 <h3 class="deployment__sub-heading">
514− Deployment overrides
1001+ Deployment configuration
5151002 </h3>
5161003 <div class="deployment__widget">
5171004 <div class="overrides">
518− <div class="overrides__item">
519− <div class="overrides__key input">
520− <input class="input__widget" type="text" placeholder="key" value="ENVIRONENT">
521− </div>
522− <div class="overrides__value input">
523− <input class="input__widget" type="text" placeholder="value" value="production">
524− </div>
525− <button class="overrides__delete spot spot--cancel">
526− Delete
527− </button>
528− </div>
529− <div class="overrides__item">
530− <div class="overrides__key input">
531− <input class="input__widget" type="text" placeholder="key" value="app_api_url">
532− </div>
533− <div class="overrides__value input">
534− <input class="input__widget" type="text" placeholder="value" value="http://my-api.my-domain.com">
535− </div>
536− <button class="overrides__delete spot spot--cancel">
537− Delete
538− </button>
539− </div>
540− <div class="overrides__item">
541− <div class="overrides__key input">
542− <input class="input__widget" type="text" placeholder="key" value="SECRET_CODE">
1005+ <button class="overrides__add dash dash--add" type="button">
1006+ Add an override
1007+ </button>
1008+ <div class="overrides__list">
1009+ <div class="editable-row">
1010+ <div class="editable-row__key input">
1011+ <input class="input__widget" type="text" placeholder="key">
1012+ </div>
1013+ <div class="editable-row__value input">
1014+ <input class="input__widget" type="text" placeholder="value">
1015+ </div>
1016+ <button class="editable-row__delete spot spot--cancel">
1017+ Delete
1018+ </button>
5431019 </div>
544− <div class="overrides__value input">
545− <input class="input__widget" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
1020+ <div class="editable-row">
1021+ <div class="editable-row__key input">
1022+ <input class="input__widget key-custom-edited" type="text" placeholder="key" value="ENVIRONENT">
1023+ </div>
1024+ <div class="editable-row__value input">
1025+ <input class="input__widget value-edited" type="text" placeholder="value" value="production">
1026+ </div>
1027+ <button class="editable-row__delete spot spot--cancel">
1028+ Delete
1029+ </button>
5461030 </div>
547− <button class="overrides__delete spot spot--cancel">
548− Delete
549− </button>
550− </div>
551− <div class="overrides__item">
552− <div class="overrides__key input">
553− <input class="input__widget" type="text" placeholder="key">
1031+ <div class="editable-row">
1032+ <div class="editable-row__key input">
1033+ <input class="input__widget key-custom-edited" type="text" placeholder="key" value="app_api_url">
1034+ </div>
1035+ <div class="editable-row__value input">
1036+ <input class="input__widget value-edited" type="text" placeholder="value" value="http://my-api.my-domain.com">
1037+ </div>
1038+ <button class="editable-row__delete spot spot--cancel">
1039+ Delete
1040+ </button>
5541041 </div>
555− <div class="overrides__value input">
556− <input class="input__widget" type="text" placeholder="value">
1042+ <div class="editable-row">
1043+ <div class="editable-row__key input">
1044+ <input class="input__widget key-default-pristine" type="text" placeholder="key" value="SECRET_CODE">
1045+ </div>
1046+ <div class="editable-row__value input">
1047+ <input class="input__widget value-edited" type="text" placeholder="value" value="asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij">
1048+ </div>
1049+ <button class="editable-row__delete spot spot--cancel">
1050+ Delete
1051+ </button>
5571052 </div>
558− <button class="overrides__delete spot spot--cancel">
559− Delete
560− </button>
5611053 </div>
562− <button class="overrides__add dash dash--add" type="button">
563− Add an override
564− </button>
5651054 </div>
5661055 </div>
5671056 </section>
@@ -609,42 +1098,70 @@
6091098 </div>
6101099 <div class="popup__content">
6111100 <div class="log">
612− 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
613− <br>"rm" -f inplace/bin/runghc
614− <br>echo 'exec "$executablename" ${1+"$@"}' >> inplace/lib/bin/ghc-iserv-dyn
615− <br>echo '#!/bin/sh' >> inplace/bin/runghc
616− <br>chmod +x inplace/lib/bin/ghc-iserv-dyn
617− <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/runghc"' >> inplace/bin/runghc
618− <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
1101+ 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}"'
1102+ >> inplace/lib/bin/ghc-iserv-dyn
1103+ <br>"rm" -f inplace/bin/runghc
1104+ <br>echo 'exec "$executablename" ${1+"$@"}' >> inplace/lib/bin/ghc-iserv-dyn
1105+ <br>echo '#!/bin/sh' >> inplace/bin/runghc
1106+ <br>chmod +x inplace/lib/bin/ghc-iserv-dyn
1107+ <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/runghc"' >> inplace/bin/runghc
1108+ <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
6191109 <br>/usr/local/bin/ginstall -c -m 755 utils/hpc/dist-install/build/tmp/hpc inplace/lib/bin/hpc
620− <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/runghc
621− <br>"rm" -f inplace/bin/hpc
622− <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
623− <br>echo 'pgmgcc="cc"' >> inplace/bin/runghc
624− <br>echo '#!/bin/sh' >> inplace/bin/hpc
1110+ <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/runghc
1111+ <br>"rm" -f inplace/bin/hpc
1112+ <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/runghc
1113+ <br>echo 'pgmgcc="cc"' >> inplace/bin/runghc
1114+ <br>echo '#!/bin/sh' >> inplace/bin/hpc
6251115 <br>echo 'ghcprog="ghc-stage2"' >> "inplace/bin/runghc"
626− <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/hpc"' >> inplace/bin/hpc
627− <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/process/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/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/bin/runghc
628− <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
629− <br>cat utils/runghc/runghc.wrapper >> inplace/bin/runghc
630− <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/hpc
631− <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
632− <br>chmod +x inplace/bin/runghc
633− <br>echo 'pgmgcc="cc"' >> inplace/bin/hpc
634− <br>"rm" -f utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
635− <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/hpc/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/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/bin/hpc
636− <br>echo '#!/bin/sh' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
637− <br>cat utils/hpc/hpc.wrapper >> inplace/bin/hpc
638− <br>echo 'export DYLD_LIBRARY_PATH="libraries/Cabal/Cabal/dist-install/build:libraries/process/dist-install/build:libraries/parsec/dist-install/build:libraries/text/dist-install/build:libraries/template-haskell/dist-install/build:libraries/pretty/dist-install/build:libraries/ghc-boot-th/dist-install/build:libraries/mtl/dist-install/build:libraries/transformers/dist-install/build:libraries/directory/dist-install/build:libraries/unix/dist-install/build:libraries/time/dist-install/build:libraries/filepath/dist-install/build:libraries/binary/dist-install/build:libraries/containers/containers/dist-install/build:libraries/bytestring/dist-install/build:libraries/deepseq/dist-install/build:libraries/array/dist-install/build:libraries/base/dist-install/build:libraries/ghc-bignum/dist-install/build:libraries/ghc-prim/dist-install/build:rts/dist/build${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
639− <br>chmod +x inplace/bin/hpc
640− <br>echo 'exec "utils/ghc-cabal/dist-install/build/tmp/ghc-cabal" ${1+"$@"}' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
641− <br>chmod +x utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
642− <br><<ghc: 2787765536 bytes, 171 GCs, 20282449/67935792 avg/max bytes residency (8 samples), 144M in use, 0.000 INIT (0.003 elapsed), 1.738 MUT (2.286 elapsed), 0.567 GC (0.611 elapsed) :ghc>>
643− <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0 -package-id binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619 -package-id ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id time-1.9.3 -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/HsToCore.hs -o compiler/stage2/build/GHC/HsToCore.o -dyno compiler/stage2/build/GHC/HsToCore.dyn_o
644− <br><<ghc: 766891168 bytes, 91 GCs, 14722329/42245856 avg/max bytes residency (6 samples), 91M in use, 0.000 INIT (0.003 elapsed), 0.361 MUT (0.510 elapsed), 0.236 GC (0.258 elapsed) :ghc>>
645− <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0 -package-id binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619 -package-id ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id time-1.9.3 -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/Iface/Ext/Ast.hs -o compiler/stage2/build/GHC/Iface/Ext/Ast.o -dyno compiler/stage2/build/GHC/Iface/Ext/Ast.dyn_o
646− <br><<ghc: 3067019736 bytes, 347 GCs, 25353490/71392248 avg/max bytes residency (8 samples), 184M in use, 0.000 INIT (0.002 elapsed), 1.416 MUT (1.823 elapsed), 0.554 GC (0.595 elapsed) :ghc>>
647− <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompil...
1116+ <br>echo 'executablename="/Users/dr/workspace/ghc/inplace/lib/bin/hpc"' >> inplace/bin/hpc
1117+ <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/process/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/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}"'
1118+ >> inplace/bin/runghc
1119+ <br>echo 'datadir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
1120+ <br>cat utils/runghc/runghc.wrapper >> inplace/bin/runghc
1121+ <br>echo 'bindir="/Users/dr/workspace/ghc/inplace/bin"' >> inplace/bin/hpc
1122+ <br>echo 'topdir="/Users/dr/workspace/ghc/inplace/lib"' >> inplace/bin/hpc
1123+ <br>chmod +x inplace/bin/runghc
1124+ <br>echo 'pgmgcc="cc"' >> inplace/bin/hpc
1125+ <br>"rm" -f utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
1126+ <br>echo 'export DYLD_LIBRARY_PATH="/Users/dr/workspace/ghc/libraries/hpc/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/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}"'
1127+ >> inplace/bin/hpc
1128+ <br>echo '#!/bin/sh' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
1129+ <br>cat utils/hpc/hpc.wrapper >> inplace/bin/hpc
1130+ <br>echo 'export DYLD_LIBRARY_PATH="libraries/Cabal/Cabal/dist-install/build:libraries/process/dist-install/build:libraries/parsec/dist-install/build:libraries/text/dist-install/build:libraries/template-haskell/dist-install/build:libraries/pretty/dist-install/build:libraries/ghc-boot-th/dist-install/build:libraries/mtl/dist-install/build:libraries/transformers/dist-install/build:libraries/directory/dist-install/build:libraries/unix/dist-install/build:libraries/time/dist-install/build:libraries/filepath/dist-install/build:libraries/binary/dist-install/build:libraries/containers/containers/dist-install/build:libraries/bytestring/dist-install/build:libraries/deepseq/dist-install/build:libraries/array/dist-install/build:libraries/base/dist-install/build:libraries/ghc-bignum/dist-install/build:libraries/ghc-prim/dist-install/build:rts/dist/build${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"'
1131+ >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
1132+ <br>chmod +x inplace/bin/hpc
1133+ <br>echo 'exec "utils/ghc-cabal/dist-install/build/tmp/ghc-cabal" ${1+"$@"}' >> utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
1134+ <br>chmod +x utils/ghc-cabal/dist-install/build/tmp/ghc-cabal-bindist
1135+ <br>
1136+ <
1137+ <ghc: 2787765536 bytes, 171 GCs, 20282449
1138+ /67935792 avg/max bytes residency (8 samples), 144M in use, 0.000 INIT (0.003 elapsed), 1.738 MUT (2.286 elapsed), 0.567 GC (0.611 elapsed) :ghc>>
1139+ <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages -i
1140+ -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build
1141+ -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0 -package-id
1142+ binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619 -package-id
1143+ ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id time-1.9.3
1144+ -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build
1145+ -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/HsToCore.hs -o compiler/stage2/build/GHC/HsToCore.o
1146+ -dyno compiler/stage2/build/GHC/HsToCore.dyn_o
1147+ <br>
1148+ <
1149+ <ghc: 766891168 bytes, 91 GCs, 14722329
1150+ /42245856 avg/max bytes residency (6 samples), 91M in use, 0.000 INIT (0.003 elapsed), 0.361 MUT (0.510 elapsed), 0.236 GC (0.258 elapsed) :ghc>>
1151+ <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages
1152+ -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompiler/stage2/build/../rts/dist/build
1153+ -Icompiler/stage2/build/stage2 -optP-DHAVE_INTERNAL_INTERPRETER -optP-DCAN_LOAD_DLL -optP-Iincludes/dist-install/build -optP-include -optPcompiler/stage2/build/./autogen/cabal_macros.h -package-id array-0.5.4.0 -package-id base-4.15.0.0
1154+ -package-id binary-0.8.7.0 -package-id bytestring-0.10.10.0 -package-id containers-0.6.2.1 -package-id deepseq-1.4.4.0 -package-id directory-1.3.6.1 -package-id exceptions-0.10.4 -package-id filepath-1.4.2.1 -package-id ghc-boot-8.11.0.20200619
1155+ -package-id ghc-boot-th-8.11.0.20200619 -package-id ghc-heap-8.11.0.20200619 -package-id ghci-8.11.0.20200619 -package-id hpc-0.6.1.0 -package-id process-1.6.8.2 -package-id template-haskell-2.17.0.0 -package-id terminfo-0.4.1.4 -package-id
1156+ time-1.9.3 -package-id transformers-0.5.6.2 -package-id unix-2.7.2.2 -Wall -Wno-name-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monoid-instances -this-unit-id ghc -XHaskell2010 -XNoImplicitPrelude -optc-DTHREADED_RTS -Iincludes/dist-install/build
1157+ -Rghc-timing -O0 -Wcpp-undef -Wincomplete-uni-patterns -Wincomplete-record-updates -no-user-package-db -rtsopts -Wnoncanonical-monad-instances -outputdir compiler/stage2/build -dynamic-too -c compiler/./GHC/Iface/Ext/Ast.hs -o compiler/stage2/build/GHC/Iface/Ext/Ast.o
1158+ -dyno compiler/stage2/build/GHC/Iface/Ext/Ast.dyn_o
1159+ <br>
1160+ <
1161+ <ghc: 3067019736 bytes, 347 GCs, 25353490
1162+ /71392248 avg/max bytes residency (8 samples), 184M in use, 0.000 INIT (0.002 elapsed), 1.416 MUT (1.823 elapsed), 0.554 GC (0.595 elapsed) :ghc>>
1163+ <br>"inplace/bin/ghc-stage1" -hisuf hi -osuf o -hcsuf hc -static -O0 -H64m -Wall -Iincludes -Iincludes/dist -Iincludes/dist-derivedconstants/header -Iincludes/dist-ghcconstants/header -this-unit-id ghc-8.11.0.20200619 -hide-all-packages
1164+ -i -icompiler/. -icompiler/stage2/build -Icompiler/stage2/build -icompiler/stage2/build/./autogen -Icompiler/stage2/build/./autogen -Icompiler/. -Icompiler/../rts/dist/build -Icompiler/stage2 -Icompiler/stage2/build/. -Icompil...
6481165 </div>
6491166 <!-- temp -->
6501167 или:
@@ -705,6 +1222,8 @@
7051222 <script src="../scripts/_data.js"></script>
7061223 <script src="../scripts/_popup.js"></script>
7071224 <script src="../scripts/_classic-popup.js"></script>
1225+ <script src="../scripts/_collapse.js"></script>
7081226
7091227 </body>
1228+
7101229</html>
octopod-css/development/markups/_action.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.action</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -104,6 +104,7 @@
104104 <script src="../scripts/_data.js"></script>
105105 <script src="../scripts/_popup.js"></script>
106106 <script src="../scripts/_classic-popup.js"></script>
107+ <script src="../scripts/_collapse.js"></script>
107108
108109 </body>
109110</html>
octopod-css/development/markups/_bar.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.bar</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -53,6 +53,7 @@
5353 <script src="../scripts/_data.js"></script>
5454 <script src="../scripts/_popup.js"></script>
5555 <script src="../scripts/_classic-popup.js"></script>
56+ <script src="../scripts/_collapse.js"></script>
5657
5758 </body>
5859</html>
octopod-css/development/markups/_break.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.break</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111
@@ -76,6 +76,7 @@
7676 <script src="../scripts/_data.js"></script>
7777 <script src="../scripts/_popup.js"></script>
7878 <script src="../scripts/_classic-popup.js"></script>
79+ <script src="../scripts/_collapse.js"></script>
7980
8081
8182 </body>
octopod-css/development/markups/_button.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.button</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111
@@ -128,6 +128,7 @@
128128 <script src="../scripts/_data.js"></script>
129129 <script src="../scripts/_popup.js"></script>
130130 <script src="../scripts/_classic-popup.js"></script>
131+ <script src="../scripts/_collapse.js"></script>
131132
132133
133134 </body>
octopod-css/development/markups/_collapse.htmlnew file+277
@@ -0,0 +1,277 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+ <head>
4+ <meta charset="utf-8">
5+ <meta http-equiv="x-ua-compatible" content="ie=edge">
6+ <title>.collapse</title>
7+ <meta name="viewport" content="width=device-width, initial-scale=1">
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
9+ <link rel="stylesheet" href="../styles/style.css">
10+ </head>
11+ <body>
12+
13+
14+
15+ <div style="width: 600px; margin: 0 auto;">
16+ <br>
17+ <br>
18+ <br>
19+ <br>
20+ <h1>.collapse</h1>
21+
22+ <p>
23+ Это функциональный блок. Кликаем в <b>.collapse__head</b>
24+ -- раскрывается <b>.collapse__body</b>.
25+ </p>
26+ <p>
27+ Дефолтная версия выглядит никак:
28+ </p>
29+ <br>
30+ <br>
31+
32+
33+ <div class="collapse">
34+ <button class="collapse__head" type="button">
35+ Шапка
36+ </button>
37+ <div class="collapse__body">
38+ Тело
39+ </div>
40+ </div>
41+
42+ <br>
43+ <br>
44+ <br>
45+ <p>
46+ Оформление добавляем модификаторами-скинами (см. ниже).
47+ </p>
48+
49+ <p>
50+ Раскрываем <b>.collapse__body</b> вручную и так же добавляем <b>.collapse--expanded</b>.
51+ </p>
52+
53+
54+
55+
56+
57+ <br>
58+ <br>
59+ <br>
60+ <br>
61+ <h2>.collapse--project</h2>
62+ <p>
63+ Внутри обычно используется <b>.row</b>
64+ </p>
65+
66+ <div class="collapse collapse--project">
67+ <button class="collapse__head" type="button">
68+ travolutionary.mapping[1]
69+ </button>
70+ <div class="collapse__body">
71+ <div class="collapse__inner">
72+ <div class="row">
73+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
74+ </div>
75+ <div class="row">
76+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
77+ </div>
78+ </div>
79+ </div>
80+ </div>
81+
82+
83+ <p>
84+ Можно вкладывать любое количество <b>.collapse</b> и <b>.row</b> друг в друга,
85+ в любом порядке, в любых комбинациях. Ещё можно добавлять<br> класс <b>.collapse__head--has-changes</b>
86+ </p>
87+
88+ <div class="collapse collapse--project">
89+ <button class="collapse__head collapse__head--has-changes" type="button">
90+ air
91+ </button>
92+ <div class="collapse__body">
93+ <div class="collapse__inner">
94+ <div class="row">
95+ <span class="key-default-pristine">vaultEngineName: </span><span class="value-pristine">staging</span>
96+ </div>
97+ <div class="collapse collapse--project">
98+ <button class="collapse__head collapse__head--has-changes" type="button">
99+ vaultEnv
100+ </button>
101+ <div class="collapse__body">
102+ <div class="collapse__inner">
103+ <div class="row">
104+ <span class="key-default-pristine">air-sso.engineOverride: </span><span class="value-pristine">staging</span>
105+ </div>
106+ <div class="collapse collapse--project">
107+ <button class="collapse__head" type="button">
108+ air-sso.mapping[0]
109+ </button>
110+ <div class="collapse__body">
111+ <div class="collapse__inner">
112+ <div class="row">
113+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
114+ </div>
115+ <div class="row">
116+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
117+ </div>
118+ </div>
119+ <div class="row">
120+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
121+ </div>
122+ </div>
123+ </div>
124+ <div class="collapse collapse--project">
125+ <button class="collapse__head collapse__head--has-changes" type="button">
126+ travolutionary.mapping[0]
127+ </button>
128+ <div class="collapse__body">
129+ <div class="collapse__inner">
130+ <div class="row">
131+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
132+ </div>
133+ <div class="row">
134+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
135+ </div>
136+ </div>
137+ </div>
138+ </div>
139+ <div class="collapse collapse--project">
140+ <button class="collapse__head collapse__head--has-changes" type="button">
141+ travolutionary.mapping[1]
142+ </button>
143+ <div class="collapse__body">
144+ <div class="collapse__inner">
145+ <div class="row">
146+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
147+ </div>
148+ <div class="row">
149+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
150+ </div>
151+ </div>
152+ </div>
153+ </div>
154+ <div class="row">
155+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
156+ </div>
157+ </div>
158+ </div>
159+ </div>
160+ </div>
161+ <div class="row">
162+ <span class="key-custom-edited">app_api_url: </span><span class="value-edited">http: //my-api.my-domain.com</span>
163+ </div>
164+ </div>
165+ </div>
166+ <div class="collapse collapse--project">
167+ <button class="collapse__head" type="button">
168+ sso
169+ </button>
170+ <div class="collapse__body">
171+ <div class="collapse__inner">
172+ <div class="row">
173+ <span class="key-default-pristine">vaultEngineName: </span><span class="value-pristine">staging</span>
174+ </div>
175+ <div class="collapse collapse--project">
176+ <button class="collapse__head" type="button">
177+ vaultEnv
178+ </button>
179+ <div class="collapse__body">
180+ <div class="collapse__inner">
181+ <div class="row">
182+ <span class="key-default-pristine">air-sso.engineOverride: </span><span class="value-pristine">staging</span>
183+ </div>
184+ <div class="collapse collapse--project">
185+ <button class="collapse__head" type="button">
186+ air-sso.mapping[0]
187+ </button>
188+ <div class="collapse__body">
189+ <div class="collapse__inner">
190+ <div class="row">
191+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
192+ </div>
193+ <div class="row">
194+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
195+ </div>
196+ </div>
197+ <div class="row">
198+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
199+ </div>
200+ </div>
201+ </div>
202+ <div class="collapse collapse--project">
203+ <button class="collapse__head" type="button">
204+ travolutionary.mapping[0]
205+ </button>
206+ <div class="collapse__body">
207+ <div class="collapse__inner">
208+ <div class="row">
209+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
210+ </div>
211+ <div class="row">
212+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
213+ </div>
214+ </div>
215+ </div>
216+ </div>
217+ <div class="collapse collapse--project">
218+ <button class="collapse__head" type="button">
219+ travolutionary.mapping[1]
220+ </button>
221+ <div class="collapse__body">
222+ <div class="collapse__inner">
223+ <div class="row">
224+ <span class="key-default-edited">name: </span><span class="value-edited">SSO_CLIENT_ID</span>
225+ </div>
226+ <div class="row">
227+ <span class="key-default-pristine">key: </span><span class="value-pristine">clientId</span>
228+ </div>
229+ </div>
230+ </div>
231+ </div>
232+ <div class="row">
233+ <span class="key-default-edited">air-sso.mapping[1].name: </span><span class="value-edited">SSO_CLIENT_SECRET</span>
234+ </div>
235+ </div>
236+ </div>
237+ </div>
238+ </div>
239+ <div class="row">
240+ <span class="key-custom-edited">app_api_url: </span><span class="value-edited">http: //my-api.my-domain.com</span>
241+ </div>
242+ </div>
243+ </div>
244+
245+
246+ <br>
247+ <br>
248+ <br>
249+ <br>
250+ <br>
251+ <br>
252+ <br>
253+ <br>
254+ <br>
255+ <br>
256+ <br>
257+ </div>
258+
259+
260+
261+ <!-- jQuery -->
262+ <script src="../vendors/jquery/jquery-3.2.1.min.js"></script>
263+
264+ <!-- Outline.js -->
265+ <script src="../vendors/outline/outline.js"></script>
266+
267+ <!-- Temp -->
268+ <script src="../scripts/_expander.js"></script>
269+ <script src="../scripts/_drop.js"></script>
270+ <script src="../scripts/_sort.js"></script>
271+ <script src="../scripts/_data.js"></script>
272+ <script src="../scripts/_popup.js"></script>
273+ <script src="../scripts/_classic-popup.js"></script>
274+ <script src="../scripts/_collapse.js"></script>
275+
276+ </body>
277+</html>
octopod-css/development/markups/_dash.html+3−2
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.dash</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111
@@ -32,7 +32,7 @@
3232 <br>
3333 <br>
3434 <br>
35− <h2>.dash--add</h2>
35+ <h2>.dash--back</h2>
3636
3737 <button class="dash dash--back" type="button">
3838 All deployments
@@ -121,6 +121,7 @@
121121 <script src="../scripts/_data.js"></script>
122122 <script src="../scripts/_popup.js"></script>
123123 <script src="../scripts/_classic-popup.js"></script>
124+ <script src="../scripts/_collapse.js"></script>
124125
125126
126127 </body>
octopod-css/development/markups/_drop.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.drop</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -104,6 +104,7 @@
104104 <script src="../scripts/_data.js"></script>
105105 <script src="../scripts/_popup.js"></script>
106106 <script src="../scripts/_classic-popup.js"></script>
107+ <script src="../scripts/_collapse.js"></script>
107108
108109 </body>
109110</html>
octopod-css/development/markups/_expander.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.expander</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -88,6 +88,7 @@
8888 <script src="../scripts/_data.js"></script>
8989 <script src="../scripts/_popup.js"></script>
9090 <script src="../scripts/_classic-popup.js"></script>
91+ <script src="../scripts/_collapse.js"></script>
9192
9293 </body>
9394</html>
octopod-css/development/markups/_external.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.external</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -53,6 +53,7 @@
5353 <script src="../scripts/_data.js"></script>
5454 <script src="../scripts/_popup.js"></script>
5555 <script src="../scripts/_classic-popup.js"></script>
56+ <script src="../scripts/_collapse.js"></script>
5657
5758 </body>
5859</html>
octopod-css/development/markups/_input.html+36−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.input</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111
@@ -72,6 +72,40 @@
7272 </div>
7373
7474
75+ <br>
76+ <br>
77+ <br>
78+ <h2>Выпадайка</h2>
79+
80+ <div class="input">
81+ <input class="input__widget" type="text" placeholder="key" value="LiveSearch">
82+ <ul class="input__dropdown">
83+ <li class="input__suggest"><b>S</b>ECRET_PASSWORD</li>
84+ <li class="input__suggest"><b>S</b>ECRET_INFO</li>
85+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
86+ </li>
87+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
88+ </li>
89+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
90+ </li>
91+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
92+ </li>
93+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
94+ </li>
95+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
96+ </li>
97+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
98+ </li>
99+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
100+ </li>
101+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
102+ </li>
103+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
104+ </li>
105+ <li class="input__suggest"><b>S</b>oviet Socialist Republics
106+ </li>
107+ </ul>
108+ </div>
75109
76110 <br>
77111 <br>
@@ -102,6 +136,7 @@
102136 <script src="../scripts/_data.js"></script>
103137 <script src="../scripts/_popup.js"></script>
104138 <script src="../scripts/_classic-popup.js"></script>
139+ <script src="../scripts/_collapse.js"></script>
105140
106141
107142 </body>
octopod-css/development/markups/_keys-and-values.htmlnew file+373
@@ -0,0 +1,373 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+ <head>
4+ <meta charset="utf-8">
5+ <meta http-equiv="x-ua-compatible" content="ie=edge">
6+ <title>Keys and values</title>
7+ <meta name="viewport" content="width=device-width, initial-scale=1">
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
9+ <link rel="stylesheet" href="../styles/style.css">
10+ </head>
11+ <body>
12+
13+
14+
15+ <div style="width: 800px; margin: 0 auto;">
16+ <br>
17+ <br>
18+ <br>
19+ <br>
20+ <h1>Keys and values</h1>
21+
22+
23+ <span class="key-default-pristine">
24+ air.vaultEngineName:
25+ </span>
26+
27+
28+ <br>
29+ <br>
30+ <br>
31+ <h2>Классы</h2>
32+
33+ <div class="table">
34+ <table>
35+ <tbody>
36+ <tr>
37+ <td>
38+ Ключ по умолчанию, значение не изменено:
39+ </td>
40+ <td>
41+ <span class="key-default-pristine">key-default-pristine</span>
42+ </td>
43+ <td>
44+ <span class="value-pristine">value-pristine</span>
45+ </td>
46+ </tr>
47+ <tr>
48+ <td>
49+ Ключ по умолчанию, значение изменено:
50+ </td>
51+ <td>
52+ <span class="key-default-edited">key-default-edited</span>
53+ </td>
54+ <td>
55+ <span class="value-edited">value-edited</span>
56+ </td>
57+ </tr>
58+ <tr>
59+ <td>
60+ Ключ кастомный, значение изменено:
61+ </td>
62+ <td>
63+ <span class="key-custom-edited">key-custom-edited</span>
64+ </td>
65+ <td>
66+ <span class="value-edited">value-edited</span>
67+ </td>
68+ </tr>
69+ <tr>
70+ <td>
71+ Ключ и значение удалены:
72+ </td>
73+ <td>
74+ <span class="key-deleted">key-deleted</span>
75+ </td>
76+ <td>
77+ <span class="value-deleted">value-deleted</span>
78+ </td>
79+ </tr>
80+ <tr>
81+ <td>
82+ Ключ удалён, значение неизвестно:
83+ </td>
84+ <td>
85+ <span class="key-deleted">key-deleted</span>
86+ </td>
87+ <td>
88+ <span class="value-unknown">unknown</span> (value-unknown)
89+ </td>
90+ </tr>
91+ </tbody>
92+ </table>
93+ </div>
94+
95+
96+ <br>
97+ <br>
98+ <h2>Примеры</h2>
99+
100+ <div class="table">
101+ <table>
102+ <tbody>
103+ <tr>
104+ <td>
105+ Ключ по умолчанию, значение не изменено:
106+ </td>
107+ <td>
108+ <span class="key-default-pristine">air.vaultEngineName:</span>
109+ </td>
110+ <td>
111+ <span class="value-pristine">staging</span>
112+ </td>
113+ </tr>
114+ <tr>
115+ <td>
116+ Ключ по умолчанию, значение изменено:
117+ </td>
118+ <td>
119+ <span class="key-default-edited">air.vaultEngineName:</span>
120+ </td>
121+ <td>
122+ <span class="value-edited">staging</span>
123+ </td>
124+ </tr>
125+ <tr>
126+ <td>
127+ Ключ кастомный, значение изменено:
128+ </td>
129+ <td>
130+ <span class="key-custom-edited">app_api_url:</span>
131+ </td>
132+ <td>
133+ <span class="value-edited">http://my-api.my-domain.com</span>
134+ </td>
135+ </tr>
136+ <tr>
137+ <td>
138+ Ключ и значение удалены:
139+ </td>
140+ <td>
141+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL</span>
142+ </td>
143+ <td>
144+ <span class="value-deleted">http://my-api.my-domain.com</span>
145+ </td>
146+ </tr>
147+ <tr>
148+ <td>
149+ Ключ удалён, значение неизвестно:
150+ </td>
151+ <td>
152+ <span class="key-deleted">air.vaultEnv.air-sso:</span>
153+ </td>
154+ <td>
155+ <span class="value-unknown">unknown</span>
156+ </td>
157+ </tr>
158+ </tbody>
159+ </table>
160+ </div>
161+
162+ <br>
163+ <br>
164+ <p>
165+ Для экономии узлов в DOM-дереве можно не создавать отдельную обёртку &lt;span&gt;...&lt;/span&gt;
166+ а вешать эти классы прям на ячейки таблицы, или на любого другого родителя.
167+ Работает со всеми классами кроме <b>.value-unknown</b> -- это отдельный компонент.
168+ </p>
169+
170+ <div class="table">
171+ <table>
172+ <tbody>
173+ <tr>
174+ <td>
175+ Ключ по умолчанию, значение не изменено:
176+ </td>
177+ <td class="key-default-pristine">
178+ air.vaultEngineName:
179+ </td>
180+ <td class="value-pristine">
181+ staging
182+ </td>
183+ </tr>
184+ <tr>
185+ <td>
186+ Ключ по умолчанию, значение изменено:
187+ </td>
188+ <td class="key-default-edited">
189+ air.vaultEngineName:
190+ </td>
191+ <td class="value-edited">
192+ staging
193+ </td>
194+ </tr>
195+ <tr>
196+ <td>
197+ Ключ кастомный, значение изменено:
198+ </td>
199+ <td class="key-custom-edited">
200+ app_api_url:
201+ </td>
202+ <td class="value-edited">
203+ http://my-api.my-domain.com
204+ </td>
205+ </tr>
206+ <tr>
207+ <td>
208+ Ключ и значение удалены:
209+ </td>
210+ <td class="key-deleted">
211+ sso.vaultEnv.hotels-sso.mapping[0].APP_URL
212+ </td>
213+ <td class="value-deleted">
214+ http://my-api.my-domain.com
215+ </td>
216+ </tr>
217+ <tr>
218+ <td>
219+ Ключ удалён, значение неизвестно:
220+ </td>
221+ <td class="key-deleted">
222+ air.vaultEnv.air-sso:
223+ </td>
224+ <td>
225+ <span class="value-unknown">unknown</span>
226+ </td>
227+ </tr>
228+ </tbody>
229+ </table>
230+ </div>
231+
232+
233+ <br>
234+ <br>
235+ <h2>
236+ Запись в строку
237+ </h2>
238+ <p>
239+ Прост пишем одно за другим, разделяем пробелом:
240+ </p>
241+
242+ <div class="table">
243+ <table>
244+ <tbody>
245+ <tr>
246+ <td>
247+ Ключ по умолчанию, значение не изменено:
248+ </td>
249+ <td>
250+ <span class="key-default-pristine">air.vaultEngineName:</span> <span class="value-pristine">staging</span>
251+ </td>
252+ </tr>
253+ <tr>
254+ <td>
255+ Ключ по умолчанию, значение изменено:
256+ </td>
257+ <td>
258+ <span class="key-default-edited">air.vaultEngineName:</span> <span class="value-edited">staging</span>
259+ </td>
260+ </tr>
261+ <tr>
262+ <td>
263+ Ключ кастомный, значение изменено:
264+ </td>
265+ <td>
266+ <span class="key-custom-edited">app_api_url:</span> <span class="value-edited">http://my-api.my-domain.com</span>
267+ </td>
268+ </tr>
269+ <tr>
270+ <td>
271+ Ключ и значение удалены:
272+ </td>
273+ <td>
274+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL</span> <span class="value-deleted">http://my-api.my-domain.com</span>
275+ </td>
276+ </tr>
277+ <tr>
278+ <td>
279+ Ключ удалён, значение неизвестно:
280+ </td>
281+ <td>
282+ <span class="key-deleted">air.vaultEnv.air-sso:</span> <span class="value-unknown">unknown</span>
283+ </td>
284+ </tr>
285+ </tbody>
286+ </table>
287+ </div>
288+
289+ <br>
290+ <br>
291+ <p>
292+ Пробел можно положить не между тегами, а внутрь ключа, и тогда подчёркивания сомкнуться
293+ </p>
294+
295+ <div class="table">
296+ <table>
297+ <tbody>
298+ <tr>
299+ <td>
300+ Ключ по умолчанию, значение не изменено:
301+ </td>
302+ <td>
303+ <span class="key-default-pristine">air.vaultEngineName: </span><span class="value-pristine">staging</span>
304+ </td>
305+ </tr>
306+ <tr>
307+ <td>
308+ Ключ по умолчанию, значение изменено:
309+ </td>
310+ <td>
311+ <span class="key-default-edited">air.vaultEngineName: </span><span class="value-edited">staging</span>
312+ </td>
313+ </tr>
314+ <tr>
315+ <td>
316+ Ключ кастомный, значение изменено:
317+ </td>
318+ <td>
319+ <span class="key-custom-edited">app_api_url: </span><span class="value-edited">http://my-api.my-domain.com</span>
320+ </td>
321+ </tr>
322+ <tr>
323+ <td>
324+ Ключ и значение удалены:
325+ </td>
326+ <td>
327+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL </span><span class="value-deleted">http://my-api.my-domain.com</span>
328+ </td>
329+ </tr>
330+ <tr>
331+ <td>
332+ Ключ удалён, значение неизвестно:
333+ </td>
334+ <td>
335+ <span class="key-deleted">air.vaultEnv.air-sso: </span><span class="value-unknown">unknown</span>
336+ </td>
337+ </tr>
338+ </tbody>
339+ </table>
340+ </div>
341+
342+ <br>
343+ <br>
344+ <br>
345+ <br>
346+ <br>
347+ <br>
348+ <br>
349+ <br>
350+ <br>
351+ <br>
352+ <br>
353+ </div>
354+
355+
356+
357+ <!-- jQuery -->
358+ <script src="../vendors/jquery/jquery-3.2.1.min.js"></script>
359+
360+ <!-- Outline.js -->
361+ <script src="../vendors/outline/outline.js"></script>
362+
363+ <!-- Temp -->
364+ <script src="../scripts/_expander.js"></script>
365+ <script src="../scripts/_drop.js"></script>
366+ <script src="../scripts/_sort.js"></script>
367+ <script src="../scripts/_data.js"></script>
368+ <script src="../scripts/_popup.js"></script>
369+ <script src="../scripts/_classic-popup.js"></script>
370+ <script src="../scripts/_collapse.js"></script>
371+
372+ </body>
373+</html>
octopod-css/development/markups/_listing.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.listing</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -92,6 +92,7 @@
9292 <script src="../scripts/_data.js"></script>
9393 <script src="../scripts/_popup.js"></script>
9494 <script src="../scripts/_classic-popup.js"></script>
95+ <script src="../scripts/_collapse.js"></script>
9596
9697 </body>
9798</html>
octopod-css/development/markups/_loading.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.loading</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -128,6 +128,7 @@
128128 <script src="../scripts/_data.js"></script>
129129 <script src="../scripts/_popup.js"></script>
130130 <script src="../scripts/_classic-popup.js"></script>
131+ <script src="../scripts/_collapse.js"></script>
131132
132133 </body>
133134</html>
octopod-css/development/markups/_notification.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.notification</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -80,6 +80,7 @@
8080 <script src="../scripts/_data.js"></script>
8181 <script src="../scripts/_popup.js"></script>
8282 <script src="../scripts/_classic-popup.js"></script>
83+ <script src="../scripts/_collapse.js"></script>
8384
8485 </body>
8586</html>
octopod-css/development/markups/_null.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.null</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -73,6 +73,7 @@
7373 <script src="../scripts/_data.js"></script>
7474 <script src="../scripts/_popup.js"></script>
7575 <script src="../scripts/_classic-popup.js"></script>
76+ <script src="../scripts/_collapse.js"></script>
7677
7778 </body>
7879</html>
octopod-css/development/markups/_spot.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.spot</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -53,6 +53,7 @@
5353 <script src="../scripts/_data.js"></script>
5454 <script src="../scripts/_popup.js"></script>
5555 <script src="../scripts/_classic-popup.js"></script>
56+ <script src="../scripts/_collapse.js"></script>
5657
5758 </body>
5859</html>
octopod-css/development/markups/_status.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.status</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -75,6 +75,7 @@
7575 <script src="../scripts/_data.js"></script>
7676 <script src="../scripts/_popup.js"></script>
7777 <script src="../scripts/_classic-popup.js"></script>
78+ <script src="../scripts/_collapse.js"></script>
7879
7980 </body>
8081</html>
octopod-css/development/markups/_title.html+2−1
@@ -5,7 +5,7 @@
55 <meta http-equiv="x-ua-compatible" content="ie=edge">
66 <title>.title</title>
77 <meta name="viewport" content="width=device-width, initial-scale=1">
8− <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">-->
8+ <!--<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">-->
99 <link rel="stylesheet" href="../styles/style.css">
1010 </head>
1111 <body>
@@ -52,6 +52,7 @@
5252 <script src="../scripts/_data.js"></script>
5353 <script src="../scripts/_popup.js"></script>
5454 <script src="../scripts/_classic-popup.js"></script>
55+ <script src="../scripts/_collapse.js"></script>
5556
5657 </body>
5758</html>
octopod-css/development/scripts/_collapse.jsnew file+18
@@ -0,0 +1,18 @@
1+(function($) {
2+
3+ var animationTime = 300;
4+
5+ $('.collapse__head').on('click', function () {
6+ var $collapse = $(this).parent('.collapse');
7+
8+ if( ! $collapse.hasClass('collapse--expanded') ) {
9+ $collapse.addClass('collapse--expanded');
10+ $collapse.children('.collapse__body').slideDown(animationTime);
11+ } else {
12+ $collapse.children('.collapse__body').slideUp(animationTime);
13+ $collapse.removeClass('collapse--expanded');
14+ }
15+
16+ });
17+
18+})(jQuery);
octopod-css/development/styles/_button.css+27
@@ -100,6 +100,33 @@
100100 background-repeat: no-repeat;
101101}
102102
103+.button--save-loading {
104+ position: relative;
105+ padding-left: 40px;
106+ color: rgba(255, 255, 255, 0.6);
107+ cursor: default;
108+}
109+
110+.button--save-loading:hover {
111+ background-color: var(--primary);
112+}
113+
114+.button--save-loading::before {
115+ content: "";
116+ position: absolute;
117+ display: block;
118+ width: 24px;
119+ height: 24px;
120+ left: 11px;
121+ top: 0;
122+ bottom: 0;
123+ margin: auto;
124+ background-image: url("../vectors/button-loader.svg");
125+ background-repeat: no-repeat;
126+ animation: loading 2.2s linear infinite;
127+}
128+
129+
103130.button--logs {
104131 padding-left: 40px;
105132 background-image: url("../vectors/common-use-logs.svg");
octopod-css/development/styles/_collapse.cssnew file+95
@@ -0,0 +1,95 @@
1+.collapse {
2+ position: relative; /* для .collapse__clear */
3+}
4+ .collapse__head {
5+ position: relative;
6+ padding: 0;
7+ background-color: transparent;
8+ border: none;
9+ font-family: inherit;
10+ text-align: left;
11+ cursor: pointer;
12+ -webkit-touch-callout: none;
13+ -webkit-user-select: none;
14+ -khtml-user-select: none;
15+ -moz-user-select: none;
16+ -ms-user-select: none;
17+ user-select: none;
18+ }
19+
20+ .collapse__body {
21+ display: none;
22+ }
23+
24+
25+
26+
27+/* Модификаторы: */
28+
29+
30+.collapse--project {
31+
32+}
33+
34+.collapse--project {
35+ margin-top: 8px;
36+}
37+ .collapse--project > .collapse__head {
38+ position: relative;
39+ display: inline-block;
40+ vertical-align: top;
41+ margin-left: -24px;
42+ padding: 0 9px 0 24px;
43+ background-color: #F3F3F3;
44+ font-weight: 300;
45+ font-size: 16px;
46+ line-height: 24px;
47+ color: var(--dark-grey);
48+ }
49+
50+ .collapse--project > .collapse__head--has-changes {
51+ text-decoration: underline;
52+ }
53+
54+ .collapse--project > .collapse__head:hover {
55+ background-color: #E2E2E2;
56+ color: var(--primary);
57+ }
58+ .collapse--project > .collapse__head::after {
59+ content: "";
60+ flex-shrink: 0;
61+ position: absolute;
62+ left: 5px;
63+ top: 4px;
64+ display: block;
65+ width: 16px;
66+ height: 16px;
67+ background-image: url("../vectors/collapse-project.svg");
68+ background-size: 16px 16px;
69+ background-repeat: no-repeat;
70+ background-position: center center;
71+ transition: transform 0.3s ease;
72+ }
73+
74+ .collapse--project > .collapse__head:hover {
75+
76+ }
77+ .collapse--project > .collapse__head:hover::after {
78+
79+ }
80+
81+ .collapse--project.collapse--expanded > .collapse__head {
82+
83+ }
84+ .collapse--project.collapse--expanded > .collapse__head::after {
85+ transform: rotate(90deg);
86+ }
87+
88+ .collapse--project > .collapse__body {
89+
90+ }
91+ .collapse--project .collapse__inner {
92+ margin-top: 8px;
93+ padding-left: 32px;
94+ border-left: 1px solid #C4C4C4;
95+ }
octopod-css/development/styles/_deployment.css+4
@@ -1,5 +1,6 @@
11.deployment {
22 padding: 24px;
3+ margin-bottom: 274px;
34}
45
56.page__body .deployment {
@@ -57,3 +58,6 @@
5758 font-size: 16px;
5859 line-height: 24px;
5960 }
61+ .deployment__widget .row:first-child {
62+ margin-top: 0;
63+ }
octopod-css/development/styles/_editable-row.cssnew file+49
@@ -0,0 +1,49 @@
1+.editable-row {
2+ position: relative;
3+ margin-top: 8px;
4+ display: flex;
5+ align-self: flex-start;
6+}
7+
8+.editable-row:first-child {
9+ margin-top: 0;
10+}
11+ .editable-row__key {
12+ width: 400px;
13+ }
14+
15+ .editable-row__value {
16+ margin-left: 16px;
17+ flex-grow: 1;
18+ }
19+
20+ .editable-row__delete {
21+ margin-left: 2px;
22+ }
23+
24+
25+ .editable-row__placeholder {
26+ position: relative;
27+ width: 400px;
28+ height: 40px;
29+ padding: 14px 0;
30+ box-shadow: inset 0 -1px 0 rgba(176, 176, 176, 0.2);
31+ }
32+
33+ .editable-row__placeholder::before {
34+ position: relative;
35+ content: "";
36+ display: block;
37+ height: 12px;
38+ width: 350px;
39+ border-radius: 2px;
40+ background: rgba(238, 238, 238, 0.5);
41+ }
42+
43+ .editable-row__placeholder:first-child {
44+ margin-right: 16px;
45+ }
46+
47+ .editable-row__placeholder:first-child::before {
48+ width: 200px;
49+ }
octopod-css/development/styles/_has-changes.cssnew file+3
@@ -0,0 +1,3 @@
1+.has-changes {
2+ text-decoration: underline;
3+}
octopod-css/development/styles/_input.css+80−2
@@ -41,6 +41,36 @@
4141 /* stylelint-enable */
4242
4343
44+ .input__dropdown {
45+ z-index: 2;
46+ position: absolute;
47+ top: 30px;
48+ background: #FFFFFF;
49+ padding: 8px 0;
50+ list-style: none;
51+ border-radius: 2px;
52+ box-shadow: 0 8px 8px 0 #0000003D, 0 0 8px 0 #0000001F;
53+ max-height: 274px;
54+ overflow-y: auto;
55+ }
56+
57+ .input__suggest {
58+ font-size: 16px;
59+ line-height: 24px;
60+ padding: 12px 16px;
61+ cursor: pointer;
62+ color: rgba(0, 0, 0, 0.87);
63+ font-weight: 400;
64+ }
65+
66+ .input__suggest b {
67+ font-weight: 700;
68+ }
69+
70+ .input__suggest:hover {
71+ background: #F8F8F8;
72+ }
73+
4474
4575.input--has-clear-type {
4676
@@ -71,12 +101,16 @@
71101 background-position: left center;
72102 }
73103
74−
75104.input--error {
76105
77106}
78107 .input--error .input__widget {
79− box-shadow: inset 0 -2px var(--red);
108+ color: var(--red);
109+ box-shadow: inset 0 -1px var(--red);
110+ }
111+
112+ .input--error .input__widget::placeholder {
113+ color: rgba(212, 42, 42, 0.5);
80114 }
81115
82116 .input__output {
@@ -89,3 +123,47 @@
89123 .input__output + .input__output {
90124 margin-top: 3px;
91125 }
126+
127+
128+
129+.input--default {
130+
131+}
132+
133+ .input--default .input__widget {
134+ font-weight: 300;
135+ font-style: italic;
136+ }
137+
138+
139+.input--deleted {
140+
141+}
142+
143+ .input--deleted .input__widget {
144+ font-weight: 300;
145+ font-style: italic;
146+ text-decoration-line: line-through;
147+ opacity: 0.2;
148+ }
149+
150+ .input--deleted .input__widget:hover {
151+ box-shadow: inset 0 -1px #B0B0B0;
152+ }
153+
154+
155+.input--disable {
156+
157+}
158+
159+ .input--disable .input__widget {
160+ color: #B0B0B0;
161+ }
162+
163+ .input--disable .input__widget::placeholder {
164+ color: #B0B0B0;
165+ }
166+
167+ .input--disable .input__widget:hover {
168+ box-shadow: inset 0 -1px #B0B0B0;
169+ }
octopod-css/development/styles/_key-custom-edited.cssnew file+4
@@ -0,0 +1,4 @@
1+.key-custom-edited {
2+ font-weight: 500 !important;
3+ text-decoration: underline !important;
4+}
octopod-css/development/styles/_key-default-edited.cssnew file+5
@@ -0,0 +1,5 @@
1+.key-default-edited {
2+ font-weight: 300 !important;
3+ font-style: italic !important;
4+ text-decoration: underline !important;
5+}
octopod-css/development/styles/_key-default-pristine.cssnew file+4
@@ -0,0 +1,4 @@
1+.key-default-pristine {
2+ font-weight: 300 !important;
3+ font-style: italic !important;
4+}
octopod-css/development/styles/_key-deleted.cssnew file+6
@@ -0,0 +1,6 @@
1+.key-deleted {
2+ font-weight: 300 !important;
3+ font-style: italic !important;
4+ text-decoration: line-through;
5+ color: #D3D3D3;
6+}
octopod-css/development/styles/_listing.css+73−1
@@ -38,13 +38,52 @@
3838 text-overflow: ellipsis;
3939 }
4040
41+ .listing--for-text .deleted > span {
42+ text-decoration-line: line-through;
43+ opacity: 0.2;
44+ }
4145 .listing--for-text .listing__item:first-child {
4246 margin-top: 0;
4347 }
44− .listing--for-text b {
48+ .listing--for-text .listing__key {
4549 font-weight: 500;
4650 }
4751
52+ .listing--for-text .listing__value {
53+ font-weight: 400;
54+ }
55+
56+ .listing--for-text .listing__key.default {
57+ font-weight: 300;
58+ font-style: italic;
59+ }
60+
61+ .listing--for-text .listing__value.default {
62+ font-weight: 300;
63+ font-style: italic;
64+ }
65+
66+ .listing__placeholder {
67+ content: "";
68+ display: inline-block;
69+ height: 12px;
70+ width: 140px;
71+ border-radius: 2px;
72+ background: rgba(143, 143, 143, 0.2);
73+ margin-right: 4px;
74+ margin-top: -5px;
75+ margin-bottom: -1px;
76+ }
77+
78+ .listing__placeholder__value {
79+ width: 70px;
80+ }
81+
82+ .listing--info-text {
83+ font-style: italic;
84+ color: rgba(143, 143, 143);
85+ }
86+
4887 .listing--for-text .listing__more {
4988 margin-top: 8px;
5089 }
@@ -54,3 +93,36 @@
5493 font-size: 16px;
5594 line-height: 24px;
5695}
96+
97+.listing__spinner {
98+ display: inline-block;
99+ vertical-align: middle;
100+ border: none;
101+ background-color: transparent;
102+ background-repeat: no-repeat;
103+ background-position: center center;
104+ background-size: 20px 20px;
105+ padding: 0;
106+ width: 20px;
107+ height: 20px;
108+ margin-top: -10px;
109+ margin-bottom: -7px;
110+ cursor: default;
111+ text-indent: 200%;
112+ white-space: nowrap;
113+ overflow: hidden;
114+ opacity: 1;
115+ background-image: url("../vectors/spot-loader.svg");
116+ animation: loading 2.2s linear infinite;
117+}
118+
119+.listing.listing--larger .listing__placeholder {
120+ height: 14px;
121+}
122+
123+.listing.listing--larger .listing__spinner {
124+ background-size: 24px 24px;
125+ width: 24px;
126+ height: 24px;
127+ margin-bottom: -5px;
128+}
octopod-css/development/styles/_overrides.css+7−20
@@ -1,28 +1,15 @@
11.overrides {
22
33}
4− .overrides__item {
5− margin-top: 8px;
6− display: flex;
7− align-self: flex-start;
8− }
9−
10− .overrides__item:first-child {
11− margin-top: 0;
4+ .overrides__add {
5+ padding: 8px 0 8px 24px !important;
126 }
13− .overrides__key {
14− width: 400px;
15− }
167
17− .overrides__value {
18− margin-left: 16px;
19− width: 400px;
20− }
8+ .overrides__list {
219
22− .overrides__delete {
23− margin-left: 2px;
24− }
10+ }
2511
26− .overrides__add {
27− margin-top: 4px;
12+ .overrides__tree {
13+ margin-top: 16px;
14+ margin-left: 16px;
2815 }
octopod-css/development/styles/_padded.cssnew file+3
@@ -0,0 +1,3 @@
1+.padded {
2+ padding-left: 24px;
3+}
octopod-css/development/styles/_root.css+2
@@ -17,6 +17,8 @@
1717 --red: #D42A2A;
1818 --green: #0F7F12;
1919
20+ --black: #212121;
21+
2022 --shadow_1: 0 2px 2px rgba(0, 0, 0, 0.24),
2123 0 0 2px rgba(0, 0, 0, 0.12);
2224
octopod-css/development/styles/_row.cssnew file+3
@@ -0,0 +1,3 @@
1+.row {
2+ margin-top: 8px;
3+}
octopod-css/development/styles/_spot.css+11
@@ -23,3 +23,14 @@
2323.spot--cancel:hover {
2424 opacity: 1;
2525}
26+
27+.spot--undo {
28+ opacity: 1;
29+ background-image: url("../vectors/spot-undo.svg");
30+}
31+
32+.spot--loader {
33+ opacity: 1;
34+ background-image: url("../vectors/spot-loader.svg");
35+ animation: loading 2.2s linear infinite;
36+}
octopod-css/development/styles/_table.css+21−26
@@ -23,6 +23,10 @@
2323 .table tbody tr {
2424 border-top: 1px solid #E0E0E0;
2525 }
26+
27+ .table tbody tr:first-child {
28+ border-top: none;
29+ }
2630 .table th {
2731 vertical-align: middle;
2832 height: 48px;
@@ -57,7 +61,7 @@
5761
5862}
5963 .table--clickable tbody tr {
60− cursor: default;
64+ cursor: pointer;
6165 }
6266
6367 .table--clickable tbody tr:hover {
@@ -84,12 +88,11 @@
8488 /* В случае table-layout: fixed; ширины колонкам задаются по ширинам в первой строке */
8589 .table--deployments th:nth-child(1) { width: 16.1818%; } /* 178px */
8690 .table--deployments th:nth-child(2) { width: 16.1818%; } /* 178px */
87− .table--deployments th:nth-child(3) { width: 8%; } /* 88px */
88− .table--deployments th:nth-child(4) { width: 20.3636%; } /* 224px */
89− .table--deployments th:nth-child(5) { width: 20.3636%; } /* 224px */
91+ .table--deployments th:nth-child(3) { width: 24.3636%; } /* 224px */
92+ .table--deployments th:nth-child(4) { width: 24.3636%; } /* 224px */
93+ .table--deployments th:nth-child(5) { width: 9.4546%; } /* 104px */
9094 .table--deployments th:nth-child(6) { width: 9.4546%; } /* 104px */
91− .table--deployments th:nth-child(7) { width: 9.4546%; } /* 104px */
92− .table--deployments th:nth-child(8) { width: 52px; }
95+ .table--deployments th:nth-child(7) { width: 52px; }
9396
9497 .table--deployments th:first-child,
9598 .table--deployments td:first-child {
@@ -119,38 +122,32 @@
119122
120123 }
121124
122− /* Tag */
123− .table--deployments td:nth-child(3) {
124− white-space: nowrap;
125− overflow: hidden;
126− text-overflow: ellipsis;
127− }
128−
129125 /* App overrides */
130− .table--deployments td:nth-child(4) {
126+ .table--deployments td:nth-child(3) {
131127
132128 }
133129
134130 /* Deployment overrides */
135− .table--deployments td:nth-child(5) {
131+ .table--deployments td:nth-child(4) {
136132
137133 }
138134
139135 /* Created */
140− .table--deployments td:nth-child(6) {
136+ .table--deployments td:nth-child(5) {
141137 white-space: nowrap;
142138 }
143139
144140 /* Changed */
145− .table--deployments td:nth-child(7) {
141+ .table--deployments td:nth-child(6) {
146142 white-space: nowrap;
147143 }
148144
149145 /* Dropdown */
150− .table--deployments td:nth-child(8) {
146+ .table--deployments td:nth-child(7) {
151147
152148 }
153− .table--deployments td:nth-child(8) .drop {
149+
150+ .table--deployments td:nth-child(7) .drop {
154151 margin: -8px;
155152 }
156153
@@ -166,12 +163,11 @@
166163
167164 /* В случае table-layout: fixed; ширины колонкам задаются по ширинам в первой строке */
168165 .table--actions th:nth-child(1) { width: 8%; }
169− .table--actions th:nth-child(2) { width: 20%; }
170− .table--actions th:nth-child(3) { width: 21%; }
171− .table--actions th:nth-child(4) { width: 20%; }
172− .table--actions th:nth-child(5) { width: 7%; }
173− .table--actions th:nth-child(6) { width: 15%; }
174− .table--actions th:nth-child(7) { width: 9%; }
166+ .table--actions th:nth-child(2) { width: 30%; }
167+ .table--actions th:nth-child(3) { width: 31%; }
168+ .table--actions th:nth-child(4) { width: 7%; }
169+ .table--actions th:nth-child(5) { width: 15%; }
170+ .table--actions th:nth-child(6) { width: 9%; }
175171
176172 .table--actions th:first-child,
177173 .table--actions td:first-child {
@@ -197,7 +193,6 @@
197193
198194 /* Image tag */
199195 .table--actions td:nth-child(2) {
200− word-break: break-all;
201196 }
202197
203198 /* Overrides */
octopod-css/development/styles/_value-deleted.cssnew file+5
@@ -0,0 +1,5 @@
1+.value-deleted {
2+ font-weight: 300 !important;
3+ text-decoration: line-through;
4+ color: #D3D3D3;
5+}
octopod-css/development/styles/_value-edited.cssnew file+4
@@ -0,0 +1,4 @@
1+.value-edited {
2+ font-weight: 500 !important;
3+ text-decoration: underline !important;
4+}
octopod-css/development/styles/_value-pristine.cssnew file+3
@@ -0,0 +1,3 @@
1+.value-pristine {
2+ font-weight: 300 !important;
3+}
octopod-css/development/styles/_value-unknown.cssnew file+12
@@ -0,0 +1,12 @@
1+.value-unknown {
2+ display: inline-block;
3+ vertical-align: baseline;
4+ width: 92px;
5+ height: 0.5715em;
6+ background-color: #F3F3F3;
7+ border-radius: 2px;
8+ margin-right: 4px;
9+ text-indent: 200%;
10+ white-space: nowrap;
11+ overflow: hidden;
12+}
octopod-css/development/styles/style.css+15
@@ -40,3 +40,18 @@
4040@import url("_no-log.css");
4141@import url("_nav.css");
4242@import url("_break.css");
43+@import url("_key-default-pristine.css");
44+@import url("_key-default-edited.css");
45+@import url("_key-custom-edited.css");
46+@import url("_key-deleted.css");
47+@import url("_value-pristine.css");
48+@import url("_value-edited.css");
49+@import url("_value-deleted.css");
50+@import url("_value-pristine.css");
51+@import url("_value-edited.css");
52+@import url("_collapse.css");
53+@import url("_row.css");
54+@import url("_padded.css");
55+@import url("_has-changes.css");
56+@import url("_editable-row.css");
57+@import url("_value-unknown.css");
octopod-css/development/vectors/button-loader.svgnew file+5
@@ -0,0 +1,5 @@
1+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<g opacity="0.6">
3+<path d="M19.5 12C19.5 16.1421 16.1421 19.5 12 19.5C7.85786 19.5 4.5 16.1421 4.5 12C4.5 7.85786 7.85786 4.5 12 4.5" stroke="white" stroke-width="2"/>
4+</g>
5+</svg>
octopod-css/development/vectors/collapse-project.svgnew file+3
@@ -0,0 +1,3 @@
1+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M6.66678 11.3332L10.0001 7.99989L6.66678 4.66655L6.66678 11.3332Z" fill="#212121"/>
3+</svg>
octopod-css/development/vectors/spot-loader.svgnew file+3
@@ -0,0 +1,3 @@
1+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M19.5 12C19.5 16.1421 16.1421 19.5 12 19.5C7.85786 19.5 4.5 16.1421 4.5 12C4.5 7.85786 7.85786 4.5 12 4.5" stroke="#B0B0B0" stroke-width="2"/>
3+</svg>
octopod-css/development/vectors/spot-undo.svgnew file+3
@@ -0,0 +1,3 @@
1+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<path d="M4.92896 4.92896C1.02596 8.83196 1.02596 15.168 4.92896 19.072C8.83196 22.974 15.168 22.975 19.072 19.072C22.974 15.169 22.974 8.83296 19.072 4.92896C15.168 1.02596 8.83196 1.02596 4.92896 4.92896ZM16.597 16.597C14.256 18.937 10.452 18.938 8.11096 16.597L9.52496 15.182C11.088 16.745 13.619 16.745 15.182 15.182C16.745 13.62 16.745 11.088 15.182 9.52496C13.598 7.94096 10.982 7.94096 9.41896 9.64596H11.64L11.647 11.647H5.98996V5.98996H7.98396V8.25196C10.359 5.72096 14.25 5.76396 16.597 8.11096C18.938 10.452 18.938 14.255 16.597 16.597Z" fill="#3671E3"/>
3+</svg>
octopod-css/package.json+2−2
@@ -6,7 +6,7 @@
66 "gulp": "3.9.1",
77 "gulp-base64": "0.1.3",
88 "gulp-change": "1.0.0",
9− "gulp-clean-css": "2.0.6",
9+ "gulp-clean-css": "4.2.0",
1010 "gulp-plumber": "1.1.0",
1111 "gulp-postcss": "6.1.0",
1212 "gulp-size": "2.1.0",
@@ -17,4 +17,4 @@
1717 "postcss-sprites": "3.1.2",
1818 "run-sequence": "1.2.2"
1919 }
20−}
20+}
octopod-css/yarn.lock+67−67
@@ -30,6 +30,13 @@ amdefine@>=0.0.4:
3030 resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
3131 integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
3232
33+ansi-colors@^1.0.1:
34+ version "1.1.0"
35+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9"
36+ integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==
37+ dependencies:
38+ ansi-wrap "^0.1.0"
39+
3340ansi-gray@^0.1.1:
3441 version "0.1.1"
3542 resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251"
@@ -66,7 +73,7 @@ ansi-styles@^4.1.0:
6673 dependencies:
6774 color-convert "^2.0.1"
6875
69−ansi-wrap@0.1.0:
76+ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
7077 version "0.1.0"
7178 resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
7279 integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768=
@@ -344,13 +351,6 @@ buffers@~0.1.1:
344351 resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
345352 integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s=
346353
347−bufferstreams@1.0.1:
348− version "1.0.1"
349− resolved "https://registry.yarnpkg.com/bufferstreams/-/bufferstreams-1.0.1.tgz#cfb1ad9568d3ba3cfe935ba9abdd952de88aab2a"
350− integrity sha1-z7GtlWjTujz+k1upq92VLeiKqyo=
351− dependencies:
352− readable-stream "^1.0.33"
353−
354354cache-base@^1.0.1:
355355 version "1.0.1"
356356 resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -471,13 +471,12 @@ class-utils@^0.3.5:
471471 isobject "^3.0.0"
472472 static-extend "^0.1.1"
473473
474−clean-css@^3.4.12:
475− version "3.4.28"
476− resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
477− integrity sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=
474+clean-css@4.2.1:
475+ version "4.2.1"
476+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
477+ integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==
478478 dependencies:
479− commander "2.8.x"
480− source-map "0.4.x"
479+ source-map "~0.6.0"
481480
482481cliui@^3.0.3:
483482 version "3.2.0"
@@ -629,13 +628,6 @@ combined-stream@~0.0.4:
629628 dependencies:
630629 delayed-stream "0.0.5"
631630
632−commander@2.8.x:
633− version "2.8.1"
634− resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
635− integrity sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=
636− dependencies:
637− graceful-readlink ">= 1.0.0"
638−
639631component-emitter@^1.2.1:
640632 version "1.3.0"
641633 resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@@ -1531,11 +1523,6 @@ graceful-fs@~1.2.0:
15311523 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364"
15321524 integrity sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=
15331525
1534−"graceful-readlink@>= 1.0.0":
1535− version "1.0.1"
1536− resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
1537− integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
1538−
15391526gulp-base64@0.1.3:
15401527 version "0.1.3"
15411528 resolved "https://registry.yarnpkg.com/gulp-base64/-/gulp-base64-0.1.3.tgz#164bd9d4f336dc16d669b331cebc139343579145"
@@ -1555,17 +1542,15 @@ gulp-change@1.0.0:
15551542 dependencies:
15561543 event-stream "^3.1.7"
15571544
1558−gulp-clean-css@2.0.6:
1559− version "2.0.6"
1560− resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-2.0.6.tgz#c664d5e2f19faad7750d62e543b21d07f6aa3c8d"
1561− integrity sha1-xmTV4vGfqtd1DWLlQ7IdB/aqPI0=
1545+gulp-clean-css@4.2.0:
1546+ version "4.2.0"
1547+ resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-4.2.0.tgz#915ec258dc6d3e6a50043f610066d5c2eac4f54e"
1548+ integrity sha512-r4zQsSOAK2UYUL/ipkAVCTRg/2CLZ2A+oPVORopBximRksJ6qy3EX1KGrIWT4ZrHxz3Hlobb1yyJtqiut7DNjA==
15621549 dependencies:
1563− clean-css "^3.4.12"
1564− gulp-util "^3.0.7"
1565− object-assign "^4.0.1"
1566− readable-stream "^2.0.6"
1567− vinyl-bufferstream "^1.0.1"
1568− vinyl-sourcemaps-apply "^0.2.1"
1550+ clean-css "4.2.1"
1551+ plugin-error "1.0.1"
1552+ through2 "3.0.1"
1553+ vinyl-sourcemaps-apply "0.2.1"
15691554
15701555gulp-plumber@1.1.0:
15711556 version "1.1.0"
@@ -3108,6 +3093,16 @@ pleeease-filters@^3.0.0:
31083093 onecolor "~2.4.0"
31093094 postcss "^5.0.4"
31103095
3096+plugin-error@1.0.1:
3097+ version "1.0.1"
3098+ resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c"
3099+ integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==
3100+ dependencies:
3101+ ansi-colors "^1.0.1"
3102+ arr-diff "^4.0.0"
3103+ arr-union "^3.1.0"
3104+ extend-shallow "^3.0.2"
3105+
31113106plur@^2.0.0, plur@^2.1.2:
31123107 version "2.1.2"
31133108 resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a"
@@ -3520,6 +3515,15 @@ read-pkg@^1.0.0:
35203515 normalize-package-data "^2.3.2"
35213516 path-type "^1.0.0"
35223517
3518+"readable-stream@2 || 3", readable-stream@^3.1.1:
3519+ version "3.6.0"
3520+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
3521+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
3522+ dependencies:
3523+ inherits "^2.0.3"
3524+ string_decoder "^1.1.1"
3525+ util-deprecate "^1.0.1"
3526+
35233527"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17, readable-stream@~1.0.33-1:
35243528 version "1.0.34"
35253529 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
@@ -3540,28 +3544,6 @@ readable-stream@^1.0.33, readable-stream@~1.1.9:
35403544 isarray "0.0.1"
35413545 string_decoder "~0.10.x"
35423546
3543−readable-stream@^2.0.6, readable-stream@~2.3.6:
3544− version "2.3.7"
3545− resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
3546− integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
3547− dependencies:
3548− core-util-is "~1.0.0"
3549− inherits "~2.0.3"
3550− isarray "~1.0.0"
3551− process-nextick-args "~2.0.0"
3552− safe-buffer "~5.1.1"
3553− string_decoder "~1.1.1"
3554− util-deprecate "~1.0.1"
3555−
3556−readable-stream@^3.1.1:
3557− version "3.6.0"
3558− resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
3559− integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
3560− dependencies:
3561− inherits "^2.0.3"
3562− string_decoder "^1.1.1"
3563− util-deprecate "^1.0.1"
3564−
35653547readable-stream@~2.0.0:
35663548 version "2.0.6"
35673549 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
@@ -3574,6 +3556,19 @@ readable-stream@~2.0.0:
35743556 string_decoder "~0.10.x"
35753557 util-deprecate "~1.0.1"
35763558
3559+readable-stream@~2.3.6:
3560+ version "2.3.7"
3561+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
3562+ integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
3563+ dependencies:
3564+ core-util-is "~1.0.0"
3565+ inherits "~2.0.3"
3566+ isarray "~1.0.0"
3567+ process-nextick-args "~2.0.0"
3568+ safe-buffer "~5.1.1"
3569+ string_decoder "~1.1.1"
3570+ util-deprecate "~1.0.1"
3571+
35773572rechoir@^0.6.2:
35783573 version "0.6.2"
35793574 resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@@ -3900,7 +3895,7 @@ source-map-url@^0.4.0:
39003895 resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
39013896 integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
39023897
3903−source-map@0.4.x, source-map@^0.4.2:
3898+source-map@^0.4.2:
39043899 version "0.4.4"
39053900 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
39063901 integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
@@ -3912,6 +3907,11 @@ source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6:
39123907 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
39133908 integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
39143909
3910+source-map@~0.6.0:
3911+ version "0.6.1"
3912+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
3913+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
3914+
39153915sparkles@^1.0.0:
39163916 version "1.0.1"
39173917 resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c"
@@ -4248,6 +4248,13 @@ text-table@^0.2.0:
42484248 resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
42494249 integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
42504250
4251+through2@3.0.1:
4252+ version "3.0.1"
4253+ resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
4254+ integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
4255+ dependencies:
4256+ readable-stream "2 || 3"
4257+
42514258through2@^0.6.1, through2@^0.6.3, through2@~0.6.1:
42524259 version "0.6.5"
42534260 resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
@@ -4452,13 +4459,6 @@ verror@1.10.0:
44524459 core-util-is "1.0.2"
44534460 extsprintf "^1.2.0"
44544461
4455−vinyl-bufferstream@^1.0.1:
4456− version "1.0.1"
4457− resolved "https://registry.yarnpkg.com/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz#0537869f580effa4ca45acb47579e4b9fe63081a"
4458− integrity sha1-BTeGn1gO/6TKRay0dXnkuf5jCBo=
4459− dependencies:
4460− bufferstreams "1.0.1"
4461−
44624462vinyl-file@~1.3.0:
44634463 version "1.3.0"
44644464 resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-1.3.0.tgz#aa05634d3a867ba91447bedbb34afcb26f44f6e7"
@@ -4483,7 +4483,7 @@ vinyl-fs@^0.3.0:
44834483 through2 "^0.6.1"
44844484 vinyl "^0.4.0"
44854485
4486−vinyl-sourcemaps-apply@^0.2.0, vinyl-sourcemaps-apply@^0.2.1:
4486+vinyl-sourcemaps-apply@0.2.1, vinyl-sourcemaps-apply@^0.2.0:
44874487 version "0.2.1"
44884488 resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705"
44894489 integrity sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=
octopod-frontend/Setup.hs+2−1
@@ -1,2 +1,3 @@
1−import Distribution.Simple
1+import Distribution.Simple
2+
23main = defaultMain
octopod-frontend/index.html+1
@@ -3,6 +3,7 @@
33
44<head>
55 <meta charset="UTF-8">
6+ <meta name="theme-color" content="#3671E3">
67</head>
78
89<body>
octopod-frontend/octopod-frontend.cabal+28
@@ -23,6 +23,8 @@ flag development
2323executable frontend
2424 if impl(ghcjs >= 0.2.1)
2525 ghcjs-options: -dedupe
26+ ghcjs-options: -DGHCJS_BUSY_YIELD=20
27+ -DGHCJS_GC_INTERVAL=60000
2628 if flag(development)
2729 cpp-options: -DDEVELOPMENT
2830 main-is: Main.hs
@@ -41,6 +43,18 @@ executable frontend
4143 , Data.Text.Search
4244 , Data.UniqMap
4345 , Data.WorkingOverrides
46+ , Frontend.UIKit
47+ , Frontend.UIKit.Button.Large
48+ , Frontend.UIKit.Button.Dash
49+ , Frontend.Classes
50+ , Frontend.UIKit.Button.Common
51+ , Frontend.UIKit.Button.Expander
52+ , Frontend.UIKit.Button.Static
53+ , Frontend.UIKit.Button.Action
54+ , Frontend.UIKit.Button.Sort
55+ , Frontend.UIKit.Button.Tree
56+ , Reflex.Dom.Renderable
57+ , Reflex.Dom.AsyncEvent
4458 ghc-options:
4559 -Weverything
4660 -Wno-implicit-prelude
@@ -56,6 +70,7 @@ executable frontend
5670 -Wno-missed-specialisations
5771 -- Because reflex-dom is a false positive.
5872 -Wno-unused-packages
73+ -Wno-partial-type-signatures
5974 -threaded
6075 default-extensions: BlockArguments
6176 , ConstraintKinds
@@ -87,6 +102,9 @@ executable frontend
87102 , OverloadedLabels
88103 , ViewPatterns
89104 , ApplicativeDo
105+ , PartialTypeSignatures
106+ , DeriveAnyClass
107+ , MultiWayIf
90108 build-depends: aeson
91109 , base
92110 , bytestring
@@ -111,5 +129,15 @@ executable frontend
111129 , ordered-containers
112130 , witherable
113131 , reflex
132+ , data-default
133+ , free
134+ , generic-data
135+ , monoidal-containers
136+ , fuzzyfind
137+ , deepseq
138+ , patch
139+ , reflex-dom-core
140+ , these
141+ , ref-tf
114142 hs-source-dirs: src
115143 default-language: Haskell2010
octopod-frontend/src/Data/Text/Search.hs+136−37
@@ -1,48 +1,147 @@
1+{-# OPTIONS_GHC -Wno-redundant-constraints -Wno-orphans #-}
2+
13module Data.Text.Search
24 ( fuzzySearch,
5+ fuzzySearchMany,
6+ searchMany,
7+ wrapResult,
8+ search,
9+ deSearch,
310 FuzzySearchStringChunk (..),
11+ SearchResult (..),
412 )
513where
614
7−import Control.Applicative
8−import Data.Bifunctor
9−import Data.Char
10−import Data.Function
15+import Control.Applicative.Free.Fast
16+import Control.DeepSeq (NFData)
17+import Control.Lens
18+import Control.Searchable
1119import qualified Data.List as L
20+import Data.Maybe (listToMaybe)
21+import Data.Ord
22+import Data.Semigroup
23+import Data.Sequence (Seq (..))
24+import Data.String
1225import Data.Text (Text)
1326import qualified Data.Text as T
27+import GHC.Generics (Generic)
28+import Reflex.Dom
29+import Reflex.Dom.Renderable
30+import Text.FuzzyFind
1431
15−type Needle = Text
32+type Needle = [String]
1633type Haystack = Text
17−type Running = Int
18−type Penalty = Int
19−
20−fuzzySearch' :: Needle -> Haystack -> Running -> Penalty -> [([FuzzySearchStringChunk String], Int)]
21−fuzzySearch' needle haystack i p = case (T.uncons needle, T.uncons haystack) of
22− (Just _, Nothing) -> []
23− (Just (n, eedle), Just (h, aystack)) ->
24− bimap (prependNotMatched h) (+ (i - p)) <$> fuzzySearch' needle aystack 0 p
25− <|> if toLower n == toLower h
26− then first (prependMatched h) <$> fuzzySearch' eedle aystack (i * 2 + 1) 1
27− else empty
28− (Nothing, Nothing) -> [([], i)]
29− (Nothing, Just _) -> [([NotMatched $ T.unpack haystack], i)]
30−
31−prependMatched :: Char -> [FuzzySearchStringChunk String] -> [FuzzySearchStringChunk String]
32−prependMatched c xs@(NotMatched _ : _) = Matched [c] : xs
33−prependMatched c (Matched cs : xs) = Matched (c : cs) : xs
34−prependMatched c [] = [Matched [c]]
35−
36−prependNotMatched :: Char -> [FuzzySearchStringChunk String] -> [FuzzySearchStringChunk String]
37−prependNotMatched c xs@(Matched _ : _) = NotMatched [c] : xs
38−prependNotMatched c (NotMatched cs : xs) = NotMatched (c : cs) : xs
39−prependNotMatched c [] = [NotMatched [c]]
40−
41−fuzzySearch :: Needle -> Haystack -> Maybe ([FuzzySearchStringChunk Text], Int)
42−fuzzySearch "" h = Just ([NotMatched h], 0)
43−fuzzySearch n h = case fuzzySearch' n h 0 0 of
44− [] -> Nothing
45− xs@(_ : _) -> Just . (first . fmap . fmap) T.pack $ L.maximumBy (compare `on` snd) xs
46−
47−data FuzzySearchStringChunk a = NotMatched !a | Matched !a
48− deriving stock (Show, Eq, Functor)
34+
35+fuzzySearch :: Needle -> Haystack -> Maybe Alignment
36+fuzzySearch [] h = Just (Alignment 0 $ Result $ pure $ Gap $ T.unpack h)
37+fuzzySearch n h = listToMaybe $ fuzzyFind n [T.unpack h]
38+
39+fuzzySearchMany :: Needle -> [Haystack] -> [(Seq FuzzySearchStringChunk, Haystack)]
40+fuzzySearchMany [] ts = ts <&> \t -> (pure $ NotMatched t, t)
41+fuzzySearchMany n h =
42+ (\(unAlignment -> (res, _), initial) -> (res, T.pack initial))
43+ <$> L.sortOn (Down . score . fst) (fuzzyFindOn id n (T.unpack <$> h))
44+
45+searchMany ::
46+ (Searchable Text x, SearchableConstraint Text x SearchResult) =>
47+ [String] ->
48+ [x] ->
49+ [Searched x SearchResult]
50+searchMany [] = fmap wrapResult
51+searchMany t = fmap snd . L.sortOn (Down . fst) . mapMaybe (search t)
52+{-# INLINE searchMany #-}
53+
54+-- | Extract initial structure from search result.
55+deSearch ::
56+ forall x.
57+ (Searchable SearchResult x, SearchableConstraint SearchResult x Text) =>
58+ x ->
59+ Searched x Text
60+deSearch = searchWith @SearchResult @x %~ initialSearchText
61+{-# INLINE deSearch #-}
62+
63+-- | Like 'search', but doesn't search for anything. All 'Text' are marked as not matched.
64+wrapResult :: (Searchable Text x, SearchableConstraint Text x SearchResult) => x -> Searched x SearchResult
65+wrapResult =
66+ runIdentity . searchWith (\t -> Identity $ SearchResult t Nothing)
67+{-# INLINE wrapResult #-}
68+
69+search ::
70+ (Searchable Text x, SearchableConstraint Text x SearchResult) =>
71+ Needle ->
72+ x ->
73+ Maybe (Int, Searched x SearchResult)
74+search needle x = runSearchApplicative $ searchWith searchSingle x
75+ where
76+ searchSingle :: Text -> SearchApplicative SearchResult
77+ searchSingle t = case fuzzySearch needle t of
78+ Nothing -> pure $ SearchResult t Nothing
79+ Just (unAlignment -> (res, score)) -> liftAp $ TextSearch t res score
80+{-# INLINE search #-}
81+
82+unAlignment :: Alignment -> (Seq FuzzySearchStringChunk, Score)
83+unAlignment (Alignment score (Result res)) = (unResultSegment <$> res, score)
84+
85+unResultSegment :: ResultSegment -> FuzzySearchStringChunk
86+unResultSegment (Gap x) = NotMatched $ T.pack x
87+unResultSegment (Match x) = Matched $ T.pack x
88+
89+data SearchResult = SearchResult
90+ { initialSearchText :: !Text
91+ , searchResult :: !(Maybe (Seq FuzzySearchStringChunk))
92+ }
93+ deriving stock (Eq, Ord, Generic)
94+ deriving anyclass (NFData)
95+
96+instance IsString SearchResult where
97+ fromString s = SearchResult (T.pack s) Nothing
98+
99+instance Semigroup SearchResult where
100+ SearchResult a aRes <> SearchResult b bRes = SearchResult (a <> b) $ case (aRes, bRes) of
101+ (Nothing, Nothing) -> Nothing
102+ (Just a', Nothing) -> Just $ a' :|> NotMatched b
103+ (Nothing, Just b') -> Just $ NotMatched a :<| b'
104+ (Just a', Just b') -> Just $ a' <> b'
105+
106+instance Monoid SearchResult where
107+ mempty = SearchResult mempty Nothing
108+
109+instance Searchable SearchResult SearchResult where
110+ type Searched SearchResult res = res
111+ searchWith f t = f t
112+ {-# INLINE searchWith #-}
113+
114+instance Renderable SearchResult where
115+ rndr (SearchResult _ (Just cs)) = rndr cs
116+ rndr (SearchResult t Nothing) = rndr t
117+ {-# INLINE rndr #-}
118+
119+data SearchF x where
120+ TextSearch :: Text -> Seq FuzzySearchStringChunk -> !Int -> SearchF SearchResult
121+
122+type SearchApplicative = Ap SearchF
123+
124+runSearchApplicative :: SearchApplicative x -> Maybe (Int, x)
125+runSearchApplicative x = case findMaxResult x of
126+ Nothing -> Nothing
127+ Just m ->
128+ Just . (m,) . runIdentity $
129+ runAp
130+ ( \case
131+ TextSearch t res i | i == m -> Identity $ SearchResult t (Just res)
132+ TextSearch t _ _ -> Identity $ SearchResult t Nothing
133+ )
134+ x
135+ where
136+ findMaxResult :: SearchApplicative a -> Maybe Int
137+ findMaxResult = fmap getMax . runAp_ (\(TextSearch _ _ i) -> Just $ Max i)
138+{-# INLINE runSearchApplicative #-}
139+
140+data FuzzySearchStringChunk = NotMatched !Text | Matched !Text
141+ deriving stock (Show, Eq, Ord, Generic)
142+ deriving anyclass (NFData)
143+
144+instance Renderable FuzzySearchStringChunk where
145+ rndr (NotMatched a) = rndr a
146+ rndr (Matched a) = elAttr "span" ("style" =: "text-decoration: underline;") (rndr a)
147+ {-# INLINE rndr #-}
octopod-frontend/src/Data/WorkingOverrides.hs+118−70
@@ -1,90 +1,138 @@
11module Data.WorkingOverrides
22 ( WorkingOverrides,
3− WorkingOverride,
4− WorkingOverrideKey (..),
3+ WorkingOverrideKey' (..),
4+ getConfigValueText,
5+ WorkingOverrideKey,
56 WorkingOverrideKeyType (..),
6− WorkingOverrideValue (..),
77 destructWorkingOverrides,
88 constructWorkingOverrides,
9− newWorkingOverride,
9+ ConfigValue (..),
10+ CustomKey (..),
11+ CustomConfigValue (..),
12+ WorkingConfigTree,
13+ WorkingOverride,
14+ WorkingOverride',
15+ configTreeHasLeaf,
16+ destructWorkingOverridesDyn,
1017 )
1118where
1219
20+import Common.Orphans ()
1321import Common.Types
14−import qualified Data.List as L
15−import qualified Data.Map.Ordered.Strict as OM
16−import Data.Maybe
22+import Control.Monad.Reader
23+import Control.Monad.Ref
24+import Data.ConfigTree (ConfigTree)
25+import qualified Data.ConfigTree as CT
26+import Data.Foldable
27+import Data.Functor
28+import Data.Map (Map)
29+import qualified Data.Map as M
30+import Data.Maybe (catMaybes)
31+import qualified Data.Maybe as M
1732import Data.Text (Text)
18−import Data.UniqMap
33+import Data.These
34+import GHC.Generics (Generic)
35+import Reflex
36+
37+type WorkingConfigTree k v = ConfigTree k (ConfigValue v)
38+
39+data ConfigValue te
40+ = DefaultConfigValue !te
41+ | CustomConfigValue !(Either (CustomKey te) (CustomConfigValue te))
42+ deriving stock (Show, Generic, Eq, Ord)
43+
44+newtype CustomKey v = CustomKey v
45+ deriving stock (Show, Generic, Eq, Ord)
46+
47+data CustomConfigValue te
48+ = CustomValue !te
49+ | DeletedValue !(Maybe te)
50+ deriving stock (Show, Generic, Eq, Ord)
51+
52+getConfigValueText :: ConfigValue te -> Maybe te
53+getConfigValueText (DefaultConfigValue t) = Just t
54+getConfigValueText (CustomConfigValue (Left (CustomKey t))) = Just t
55+getConfigValueText (CustomConfigValue (Right (CustomValue t))) = Just t
56+getConfigValueText (CustomConfigValue (Right (DeletedValue t))) = t
57+
58+-- Super inefficient but i dont care
59+configTreeHasLeaf ::
60+ forall m kv te.
61+ ( MonadReader (Ref m (Map (Text, ConfigTree kv te) Bool)) m
62+ , MonadRef m
63+ , Ord kv
64+ , Ord te
65+ ) =>
66+ -- | The id of the predicate used for caching
67+ Text ->
68+ (te -> Bool) ->
69+ ConfigTree kv te ->
70+ m Bool
71+configTreeHasLeaf name f = memo (\(_, CT.ConfigTree x) -> go $ toList x) . (name,)
72+ where
73+ go :: [(Maybe te, ConfigTree kv te)] -> m Bool
74+ go [] = pure False
75+ go ((Just y, _) : _) | f y = pure True
76+ go ((_, ct) : rest) =
77+ configTreeHasLeaf name f ct >>= \case
78+ True -> pure True
79+ False -> go rest
80+
81+memo ::
82+ (MonadReader (Ref m (Map x y)) m, Ord x, MonadRef m) =>
83+ (x -> m y) ->
84+ x ->
85+ m y
86+memo f x = do
87+ memoRef <- ask
88+ m <- readRef memoRef
89+ case M.lookup x m of
90+ Just y -> pure y
91+ Nothing -> do
92+ y <- f x
93+ modifyRef memoRef $ M.insert x y
94+ pure y
95+type WorkingOverrides = WorkingOverrides' Text
96+
97+type WorkingOverrides' te = ConfigTree te (ConfigValue te)
98+
99+type WorkingOverride = WorkingOverride' Text
19100
20−type WorkingOverrides = UniqKeyMap WorkingOverride
101+type WorkingOverride' te = (te, ConfigValue te)
21102
22−type WorkingOverride = (WorkingOverrideKey, WorkingOverrideValue)
103+type WorkingOverrideKey = WorkingOverrideKey' Text
23104
24−data WorkingOverrideKey = WorkingOverrideKey !WorkingOverrideKeyType !Text
105+data WorkingOverrideKey' te = WorkingOverrideKey !WorkingOverrideKeyType !te
25106 deriving stock (Show)
26107
27108data WorkingOverrideKeyType = CustomWorkingOverrideKey | DefaultWorkingOverrideKey
28109 deriving stock (Show, Eq)
29110
30−data WorkingOverrideValue
31− = WorkingCustomValue !Text
32− | WorkingDefaultValue !Text
33− | WorkingDeletedValue !(Maybe Text)
34− deriving stock (Show)
111+destructWorkingOverridesDyn :: Reflex t => [Dynamic t WorkingOverride] -> Dynamic t (Overrides l)
112+destructWorkingOverridesDyn =
113+ fmap (Overrides . CT.fromFlatList . catMaybes) . sequenceA . (fmap . fmap) (\(k, v) -> (k,) <$> unConfigValue v)
35114
36−destructWorkingOverrides :: WorkingOverrides -> Overrides l
115+destructWorkingOverrides :: [WorkingOverride] -> Overrides l
37116destructWorkingOverrides =
38− Overrides
39− . OM.fromList
40− . mapMaybe
41− ( \case
42− (WorkingOverrideKey CustomWorkingOverrideKey k, getWorkingOverrideValue -> v) -> Just (k, v)
43− (WorkingOverrideKey _ k, WorkingCustomValue v) -> Just (k, ValueAdded v)
44− (WorkingOverrideKey _ k, WorkingDeletedValue _) -> Just (k, ValueDeleted)
45− (WorkingOverrideKey DefaultWorkingOverrideKey _, WorkingDefaultValue _) -> Nothing
46− )
47− . elemsUniq
48− where
49− getWorkingOverrideValue :: WorkingOverrideValue -> OverrideValue
50− getWorkingOverrideValue (WorkingCustomValue x) = ValueAdded x
51− getWorkingOverrideValue (WorkingDefaultValue x) = ValueAdded x
52− getWorkingOverrideValue (WorkingDeletedValue _) = ValueDeleted
53−
54−constructWorkingOverrides :: Maybe (DefaultConfig l) -> Overrides l -> WorkingOverrides
55−constructWorkingOverrides (Just (DefaultConfig dCfg)) (Overrides ovsM) =
56− let custom =
57− uniqMapFromList
58− . mapMaybe
59− ( \(k, v) ->
60− let k' = WorkingOverrideKey (if OM.member k dCfg then DefaultWorkingOverrideKey else CustomWorkingOverrideKey) k
61− in case v of
62− ValueAdded x -> Just (k', WorkingCustomValue x)
63− ValueDeleted -> (k',) . WorkingDeletedValue . Just <$> OM.lookup k dCfg
64− )
65− . OM.assocs
66− $ ovsM
67− in L.foldl'
68− ( \m (k, v) ->
69− if OM.member k ovsM
70− then m
71− else
72− fst $
73− insertUniqEnd (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDefaultValue v) m
74− )
75− custom
76− (OM.assocs dCfg)
77−constructWorkingOverrides Nothing (Overrides ovsM) =
78− uniqMapFromList
79− . fmap
80− ( \(k, v) ->
81− let k' = WorkingOverrideKey CustomWorkingOverrideKey k
82− in case v of
83− ValueAdded x -> (k', WorkingCustomValue x)
84− ValueDeleted -> (k', WorkingDeletedValue Nothing)
85− )
86− . OM.assocs
87− $ ovsM
88−
89−newWorkingOverride :: WorkingOverride
90−newWorkingOverride = (WorkingOverrideKey CustomWorkingOverrideKey "", WorkingCustomValue "")
117+ Overrides . CT.fromFlatList . M.catMaybes . fmap (\(k, v) -> (k,) <$> unConfigValue v)
118+
119+unConfigValue :: ConfigValue te -> Maybe (OverrideValue' te)
120+unConfigValue (DefaultConfigValue _) = Nothing
121+unConfigValue (CustomConfigValue (Left (CustomKey te))) = Just $ ValueAdded te
122+unConfigValue (CustomConfigValue (Right (CustomValue te))) = Just $ ValueAdded te
123+unConfigValue (CustomConfigValue (Right (DeletedValue _))) = Just ValueDeleted
124+
125+constructWorkingOverrides ::
126+ Ord te =>
127+ DefaultConfig' te l ->
128+ Overrides' te l ->
129+ WorkingOverrides' te
130+constructWorkingOverrides (DefaultConfig defCT) (Overrides newCT) =
131+ CT.catMaybes $
132+ CT.zip newCT defCT <&> \case
133+ That x -> Just $ DefaultConfigValue x
134+ This x -> case x of
135+ ValueDeleted -> Nothing
136+ ValueAdded v -> Just $ CustomConfigValue $ Left $ CustomKey v
137+ These ValueDeleted v -> Just $ CustomConfigValue $ Right $ DeletedValue $ Just v
138+ These (ValueAdded v) _ -> Just $ CustomConfigValue $ Right $ CustomValue v
octopod-frontend/src/Frontend/API.hs+1−6
@@ -21,7 +21,6 @@ module Frontend.API
2121 fullInfoEndpoint,
2222 statusEndpoint,
2323 restoreEndpoint,
24− pingEndpoint,
2524 projectName,
2625 commandResponse,
2726 wsPath,
@@ -127,10 +126,6 @@ restoreEndpoint ::
127126 Dynamic t (Either Text DeploymentName) ->
128127 Event t () ->
129128 m (Event t (ReqResult () CommandResponse))
130−pingEndpoint ::
131− MonadWidget t m =>
132− Event t () ->
133− m (Event t (ReqResult () NoContent))
134129projectName ::
135130 MonadWidget t m =>
136131 Event t () ->
@@ -148,7 +143,7 @@ deploymentOverrideKeys
148143 :<|> statusEndpoint
149144 :<|> restoreEndpoint
150145 )
151− :<|> pingEndpoint
146+ :<|> _pingEndpoint
152147 :<|> projectName = apiClients
153148
154149-- | Parser of kubectl command responses.
octopod-frontend/src/Frontend/Classes.hsnew file+18
@@ -0,0 +1,18 @@
1+module Frontend.Classes
2+ ( Classes,
3+ destructClasses,
4+ )
5+where
6+
7+import Data.String
8+import Data.Text (Text)
9+import qualified Data.Text as T
10+
11+newtype Classes = Classes [Text]
12+ deriving newtype (Semigroup, Monoid)
13+
14+instance IsString Classes where
15+ fromString s = Classes [fromString s]
16+
17+destructClasses :: Classes -> Text
18+destructClasses (Classes cs) = T.unwords cs
octopod-frontend/src/Frontend/Route.hs+1−1
@@ -33,7 +33,7 @@ data Routes :: Type -> Type where
3333deriving stock instance Show (Routes a)
3434
3535fmap mconcat . sequence $
36− [ makeWrapped ''DeploymentName
36+ [ makeWrapped ''DeploymentName'
3737 , deriveRouteComponent ''Routes
3838 ]
3939
octopod-frontend/src/Frontend/UIKit.hsnew file+555
@@ -0,0 +1,555 @@
1+-- | Most of HTML details should be implemented here with a domain API.
2+module Frontend.UIKit
3+ ( deploymentSection,
4+ loadingCommonWidget,
5+ errorCommonWidget,
6+ octopodTextInput',
7+ octopodTextInputDyn,
8+ loadingOverride,
9+ loadingOverrides,
10+ configField,
11+ popupOverlay,
12+ Default (..),
13+ module X,
14+ (.~~),
15+ (?~~),
16+ deletedOverride,
17+ untilReadyEv',
18+ untilReady',
19+ runWithReplace',
20+ joinEvM,
21+ showNonEditableWorkingOverrideTree,
22+ showFlatConfig,
23+ ConfigFieldButtonAction (..),
24+ showOverrideTree,
25+ nonEditableLoading,
26+ )
27+where
28+
29+import Common.Types
30+import Control.Lens
31+import Control.Lens.Extras (is)
32+import Control.Monad
33+import Control.Monad.Reader
34+import Control.Monad.Ref
35+import Data.Align
36+import Data.ConfigTree (ConfigTree (ConfigTree))
37+import qualified Data.ConfigTree as CT
38+import Data.Default
39+import Data.Either
40+import Data.Foldable
41+import Data.Functor
42+import Data.Generics.Labels ()
43+import Data.Generics.Sum
44+import Data.List.NonEmpty
45+import qualified Data.List.NonEmpty as NE
46+import Data.Map (Map)
47+import qualified Data.Map as M
48+import qualified Data.Map.Ordered.Strict as OM
49+import Data.Maybe (fromMaybe, isNothing, maybeToList)
50+import Data.Monoid (Endo (..))
51+import Data.Text (Text)
52+import qualified Data.Text as T
53+import Data.Text.Search
54+import Data.These
55+import Data.Traversable
56+import Data.Witherable
57+import Data.WorkingOverrides
58+import Frontend.Classes as X
59+import Frontend.UIKit.Button.Action as X
60+import Frontend.UIKit.Button.Common
61+import Frontend.UIKit.Button.Dash as X
62+import Frontend.UIKit.Button.Expander as X
63+import Frontend.UIKit.Button.Large as X
64+import Frontend.UIKit.Button.Sort as X
65+import Frontend.UIKit.Button.Static as X
66+import Frontend.UIKit.Button.Tree
67+import GHC.Generics (Generic)
68+import Reflex.Dom
69+import Reflex.Dom.AsyncEvent
70+import Reflex.Dom.Renderable
71+import Reflex.Network
72+
73+(.~~) :: ASetter' s a -> a -> s -> s
74+(.~~) = (.~)
75+
76+(?~~) :: ASetter' s (Maybe a) -> a -> s -> s
77+(?~~) = (?~)
78+
79+deploymentSection :: DomBuilder t m => Text -> m a -> m a
80+deploymentSection n m = elClass "section" "deployment__section" $ do
81+ elClass "h3" "deployment__sub-heading" $ text n
82+ elDiv "deployment__widget" m
83+
84+-- | Widget with a loading spinner.
85+loadingCommonWidget :: MonadWidget t m => m ()
86+loadingCommonWidget =
87+ divClass "loading loading--enlarged loading--alternate" $
88+ text "Loading..."
89+
90+-- | Widget with an error message.
91+errorCommonWidget :: MonadWidget t m => m ()
92+errorCommonWidget =
93+ divClass "null null--data" $
94+ divClass "null__content" $ do
95+ elClass "b" "null__heading" $ text "Cannot retrieve the data"
96+ divClass "null__message" $ text "Try to reload the page"
97+
98+data ConfigField t = ConfigField
99+ { fieldValue :: Dynamic t Text
100+ , fieldError :: Dynamic t (Maybe (NonEmpty Text))
101+ , fieldType :: Dynamic t ConfigFieldType
102+ }
103+ deriving stock (Generic)
104+
105+data ConfigFieldType
106+ = DefaultConfigFieldType
107+ | CustomConfigFieldType
108+ deriving stock (Show)
109+
110+configField ::
111+ MonadWidget t m =>
112+ Dynamic t [Text] ->
113+ Dynamic t WorkingOverride ->
114+ m (Dynamic t Text, Dynamic t Text, Event t ConfigFieldButtonAction)
115+configField overrideKeyValues (splitDynPure -> (k, v')) = do
116+ let (splitDynPure -> (keyClass, valueClass)) =
117+ v' <&> \case
118+ DefaultConfigValue _ -> ("key-default-pristine", "value-pristine")
119+ CustomConfigValue (Right (CustomValue _)) -> ("key-default-edited", "value-edited")
120+ CustomConfigValue (Right (DeletedValue Just {})) -> ("key-deleted", "value-deleted")
121+ CustomConfigValue (Right (DeletedValue Nothing)) -> ("key-deleted", "value-unknown")
122+ CustomConfigValue (Left _) -> ("key-custom-edited", "value-edited")
123+ valDyn =
124+ v' <&> \case
125+ DefaultConfigValue v -> v
126+ CustomConfigValue (Right (CustomValue v)) -> v
127+ CustomConfigValue (Right (DeletedValue v)) -> fromMaybe "UNKNOWN" v
128+ CustomConfigValue (Left (CustomKey v)) -> v
129+ deletedDyn' = (is (_Ctor' @"CustomConfigValue" . _Right . _Ctor' @"DeletedValue")) <$> v'
130+ deletedDyn <- holdUniqDyn deletedDyn'
131+ let extraDivClasses =
132+ deletedDyn <&> \case
133+ True -> "input--deleted"
134+ False -> mempty
135+ elDiv "editable-row" $ mdo
136+ (value -> keyTextDyn) <-
137+ octopodTextInputDyn
138+ overrideKeyValues
139+ deletedDyn
140+ (mappend "editable-row__key" <$> extraDivClasses)
141+ keyClass
142+ "key"
143+ k
144+ (pure Nothing)
145+ (value -> valTextDyn) <-
146+ octopodTextInputDyn
147+ (pure [])
148+ deletedDyn
149+ (mappend "editable-row__value" <$> extraDivClasses)
150+ valueClass
151+ "value"
152+ valDyn
153+ (valTextDyn <&> \t -> if (T.null . T.strip) t then Just (pure "Value can not be empty") else Nothing)
154+ closeEv <-
155+ networkView >=> switchHold never $
156+ deletedDyn <&> \case
157+ True -> fmap (const ConfigFieldButtonRecover) <$> undoOverrideButton
158+ False -> fmap (const ConfigFieldButtonClear) <$> deleteOverrideButton
159+ pure (keyTextDyn, valTextDyn, closeEv)
160+
161+data ConfigFieldButtonAction = ConfigFieldButtonClear | ConfigFieldButtonRecover
162+ deriving stock (Show)
163+
164+deletedOverride :: MonadWidget t m => Text -> Maybe Text -> m (Event t ())
165+deletedOverride k vM = do
166+ elDiv "editable-row" $ do
167+ elDiv "editable-row__key input input--deleted" $ field k
168+ case vM of
169+ Nothing -> do
170+ elDiv "editable-row__placeholder editable-row__value" blank
171+ loadingOverrideSpinner
172+ pure never
173+ Just v -> do
174+ elDiv "editable-row__value input input--deleted" $ field $ v
175+ (($> ()) <$> undoOverrideButton)
176+ where
177+ field t =
178+ void $
179+ inputElement $
180+ def
181+ & inputElementConfig_initialValue .~ t
182+ & initialAttributes .~ ("type" =: "text" <> "class" =: "input__widget")
183+
184+loadingOverrideField :: MonadWidget t m => m ()
185+loadingOverrideField = elDiv "editable-row__placeholder" blank
186+
187+loadingOverrideSpinner :: MonadWidget t m => m ()
188+loadingOverrideSpinner = elDiv "overrides__delete spot spot--loader" blank
189+
190+loadingOverride :: MonadWidget t m => m ()
191+loadingOverride = do
192+ elDiv "editable-row loader" $ do
193+ loadingOverrideField
194+ loadingOverrideField
195+ loadingOverrideSpinner
196+
197+loadingOverrides :: MonadWidget t m => m ()
198+loadingOverrides = divClass "padded" $ do
199+ loadingOverride
200+ loadingOverride
201+ loadingOverride
202+
203+elDiv :: DomBuilder t m => Text -> m a -> m a
204+elDiv = elClass "div"
205+
206+-- | The only text input field that is used in project forms. This input
207+-- provides automatic error message hiding after user starts typing.
208+octopodTextInput' ::
209+ MonadWidget t m =>
210+ -- | Value to suggest
211+ (Dynamic t [Text]) ->
212+ -- | Disabled?
213+ Dynamic t Bool ->
214+ -- | Div classes.
215+ Dynamic t Classes ->
216+ -- | Input field classes.
217+ Dynamic t Classes ->
218+ -- | Placeholder for input field.
219+ Text ->
220+ -- | Init value.
221+ (Dynamic t Text) ->
222+ -- | Event carrying the error message.
223+ Event t Text ->
224+ m (InputElement EventResult GhcjsDomSpace t, Dynamic t Bool)
225+octopodTextInput' valuesDyn disabledDyn inpClassesDyn clssDyn placeholder inValDyn' errEv = mdo
226+ errDyn <-
227+ holdDyn Nothing $
228+ leftmost
229+ [ Just . pure <$> errEv
230+ , Nothing <$ updated (value inp)
231+ ]
232+ inp <- octopodTextInputDyn valuesDyn disabledDyn inpClassesDyn clssDyn placeholder inValDyn' errDyn
233+ pure (inp, isNothing <$> errDyn)
234+
235+-- | The only text input field that is used in project forms. This input
236+-- provides automatic error message hiding after user starts typing.
237+octopodTextInputDyn ::
238+ MonadWidget t m =>
239+ -- | Value to suggest
240+ Dynamic t [Text] ->
241+ -- | Disabled?
242+ Dynamic t Bool ->
243+ -- | Div classes.
244+ Dynamic t Classes ->
245+ -- | Input field classes.
246+ Dynamic t Classes ->
247+ -- | Placeholder for input field.
248+ Text ->
249+ -- | Init value.
250+ Dynamic t Text ->
251+ -- | Event carrying the error message.
252+ Dynamic t (Maybe (NonEmpty Text)) ->
253+ m (InputElement EventResult GhcjsDomSpace t)
254+octopodTextInputDyn valuesDyn disabledDyn divClasses inpClassesDyn placeholder inValDyn' errDyn = mdo
255+ inValDyn <- holdUniqDyn inValDyn'
256+ let valDyn = value inp
257+ inValEv =
258+ align (leftmost [selectedValue, updated inValDyn]) (updated valDyn)
259+ & fmapMaybe
260+ ( \case
261+ This x -> Just x
262+ These inV currV | inV /= currV -> Just inV
263+ _ -> Nothing
264+ )
265+
266+ let errorClassesDyn = do
267+ err <- errDyn
268+ case err of
269+ Just _ -> "input--error"
270+ Nothing -> mempty
271+
272+ let classDyn = do
273+ errClasses <- errorClassesDyn
274+ additionalClasses <- divClasses
275+ pure . destructClasses $ "input" <> errClasses <> additionalClasses
276+
277+ inVal <- sample . current $ inValDyn
278+ disabled <- sample . current $ disabledDyn
279+ let actualInputClassesDyn = do
280+ inpClass <- inpClassesDyn
281+ pure $ destructClasses $ "input__widget" <> inpClass
282+ clss <- sample . current $ actualInputClassesDyn
283+ (inp, selectedValue) <- elDynClass "div" classDyn $ do
284+ inp' <-
285+ inputElement $
286+ def
287+ & initialAttributes
288+ .~ ( "type" =: "text"
289+ <> "class" =: clss
290+ <> "placeholder" =: placeholder
291+ <> "spellcheck" =: "false"
292+ )
293+ & inputElementConfig_setValue .~ inValEv
294+ & inputElementConfig_initialValue .~ inVal
295+ & inputElementConfig_elementConfig . elementConfig_initialAttributes
296+ %~ (if disabled then M.insert "disabled" "disabled" else id)
297+ & inputElementConfig_elementConfig . elementConfig_modifyAttributes
298+ <>~ updated
299+ ( do
300+ disabled' <- disabledDyn
301+ classes <- actualInputClassesDyn
302+ pure $
303+ ( "disabled" =: (if disabled' then Just "disabled" else Nothing)
304+ <> "class" =: Just classes
305+ )
306+ )
307+ void $
308+ simpleList ((maybeToList >=> NE.toList) <$> errDyn) $ \err ->
309+ divClass "input__output" $ dynText err
310+
311+ let searchValuesEv = catMaybes . updated $ do
312+ hasFocus <- _inputElement_hasFocus inp'
313+ values <- valuesDyn
314+ currVal <- valDyn
315+ pure $ case hasFocus of
316+ True | (_ : _) <- values -> Just (values, currVal)
317+ _ -> Nothing
318+ searchResultEv <- asyncEventLast searchValuesEv $ \(values, currVal) ->
319+ fuzzySearchMany [T.unpack currVal] values
320+ selectedValueEv <-
321+ (>>= joinEvM) $
322+ runWithReplace' (pure ()) $
323+ updated (_inputElement_hasFocus inp') <&> \case
324+ False -> pure never
325+ True ->
326+ (>>= joinEvM) $
327+ runWithReplace' (pure ()) $
328+ searchResultEv <&> \case
329+ [] -> pure never
330+ searchResult -> do
331+ elClass "ul" "input__dropdown" $ do
332+ fmap leftmost $
333+ for searchResult $ \(res, initialText) -> do
334+ (resEl, ()) <- elClass' "li" "input__suggest" $
335+ for_ res $ \case
336+ Matched t -> elAttr "span" ("style" =: "font-weight: bold;") $ text t
337+ NotMatched t -> text t
338+ -- Because 'Click' would fire after the text field looses
339+ -- focus and the popup disappears.
340+ pure $ domEvent Mousedown resEl $> initialText
341+
342+ pure (inp', selectedValueEv)
343+ pure inp
344+
345+-- | Dark unclickable background for opened sidebar.
346+popupOverlay :: DomBuilder t m => m ()
347+popupOverlay =
348+ elAttr "div" ("class" =: "popup__overlay" <> "aria-hidden" =: "true") blank
349+
350+showFlatConfig ::
351+ (MonadWidget t m, Renderable tk, Renderable tv) =>
352+ [(tk, OverrideValue' tv)] ->
353+ m ()
354+showFlatConfig l =
355+ for_ l $ \(k, v) ->
356+ let keyClasses = if is (_Ctor' @"ValueDeleted") v then "key-deleted" else "key-default-pristine"
357+ in divClass "row" $ do
358+ elClass "span" (destructClasses keyClasses) $ do
359+ rndr k
360+ text ": "
361+ pure ()
362+ case v of
363+ ValueAdded t -> elClass "span" "value-edited" $ rndr t
364+ ValueDeleted -> elClass "div" "listing__placeholder listing__placeholder__value" $ pure ()
365+ pure ()
366+
367+showNonEditableWorkingOverrideTree ::
368+ (MonadWidget t m, Renderable tk, Renderable tv, Ord tk, Ord (ConfigValue tv)) =>
369+ Dynamic t (WorkingConfigTree tk tv) ->
370+ m ()
371+showNonEditableWorkingOverrideTree treeDyn = do
372+ memoRef <- newRef mempty
373+ flip runReaderT memoRef $
374+ untilReady (lift nonEditableLoading) $
375+ networkView $
376+ treeDyn <&> \tree ->
377+ untilReady (lift nonEditableLoading) $
378+ divClass "padded" $
379+ showWorkingOverrideTree'
380+ (const False)
381+ (is (_Ctor' @"CustomConfigValue"))
382+ (\_ _ -> ())
383+ (\_ k v -> renderRow k v $> pure ())
384+ tree
385+ (pure True)
386+ never
387+ pure ()
388+
389+nonEditableLoading :: DomBuilder t m => m ()
390+nonEditableLoading = for_ [1 :: Int .. 3] $ \_ ->
391+ divClass "padded" $
392+ divClass "row" $ do
393+ elClass "div" "listing__placeholder" $ pure ()
394+ elClass "div" "listing__spinner" $ pure ()
395+
396+showOverrideTree ::
397+ forall m t tv tk x.
398+ ( MonadWidget t m
399+ , Renderable tk
400+ , Show x
401+ , Ord tk
402+ , Ord tv
403+ ) =>
404+ -- | Value should be shown by default
405+ (tv -> Bool) ->
406+ -- | Value is modified
407+ (tv -> Bool) ->
408+ -- | Prepare the fixpoint data
409+ (tk -> tv -> x) ->
410+ -- | Render row
411+ (x -> tk -> tv -> m (Dynamic t x)) ->
412+ ConfigTree tk tv ->
413+ -- | Should we render anything
414+ m [Dynamic t x]
415+showOverrideTree isShown isModified prepareData renderValue ct = do
416+ memoRef <- newRef mempty
417+ flip runReaderT memoRef $
418+ showWorkingOverrideTree' isShown isModified prepareData ((fmap . fmap . fmap) lift renderValue) ct (pure True) never
419+
420+showWorkingOverrideTree' ::
421+ ( MonadWidget t m
422+ , Renderable tk
423+ , Show x
424+ , MonadReader (Ref m (Map (Text, ConfigTree tk tv) Bool)) m
425+ , Ord tv
426+ , Ord tk
427+ ) =>
428+ -- | Value should be shown by default
429+ (tv -> Bool) ->
430+ -- | Value is modified
431+ (tv -> Bool) ->
432+ -- | Prepare the fixpoint data
433+ (tk -> tv -> x) ->
434+ -- | Render row
435+ (x -> tk -> tv -> m (Dynamic t x)) ->
436+ ConfigTree tk tv ->
437+ -- | Parent is open
438+ Dynamic t Bool ->
439+ -- | Force open/close all subtrees
440+ Event t Bool ->
441+ m [Dynamic t x]
442+showWorkingOverrideTree' isShown isModified prepareData renderValue (ConfigTree m) shouldRenderDyn' forceSubtreesEv = do
443+ shouldRenderDyn <- holdUniqDyn shouldRenderDyn'
444+ (appEndo . fold -> f) <- for (OM.assocs m) $ \(k, (mv, _)) -> do
445+ xMaybe <- for mv $ \v -> mdo
446+ let initialData = prepareData k v
447+ x <-
448+ networkView $
449+ shouldRenderDyn <&> \render -> do
450+ lastX <- sample $ current x'
451+ if render
452+ then elClass "div" "row" $ renderValue lastX k v
453+ else pure (pure lastX)
454+ x' <- (join <$> (holdDyn (pure initialData) x))
455+ pure $ x'
456+ pure $ Endo $ maybe id (:) xMaybe
457+ fmap (f . concat) $
458+ for (OM.assocs m) $ \case
459+ (_, (_, subtree)) | CT.null subtree -> pure []
460+ (k, (_, subtree)) -> mdo
461+ let wrapperClass = do
462+ open <- openDyn
463+ pure $
464+ destructClasses $
465+ "collapse--project" <> "collapse"
466+ <> if open
467+ then "collapse--expanded"
468+ else mempty
469+ (ovs, openDyn) <- elDynClass "div" wrapperClass $ do
470+ modified <- configTreeHasLeaf "isModified" isModified subtree
471+ shown <- configTreeHasLeaf "isShown" isShown subtree
472+ (traceEvent "clickEv" -> clickEv) <-
473+ treeButton
474+ TreeButtonConfig
475+ { buttonText = TextBuilder $ rndr k
476+ , subtreeHasChanges = modified
477+ , visible = shouldRenderDyn
478+ , forceState = forceSubtreesEv
479+ }
480+ buttonIsOpen <- holdDyn shown $ leftmost [forceSubtreesEv, fst <$> clickEv]
481+ let selfForceSubtrees = fst <$> ffilter (isRight . snd) clickEv
482+ ovs' <-
483+ divClass "collapse__inner" $
484+ showWorkingOverrideTree'
485+ isShown
486+ isModified
487+ prepareData
488+ renderValue
489+ subtree
490+ ((&&) <$> buttonIsOpen <*> shouldRenderDyn)
491+ (leftmost [selfForceSubtrees, forceSubtreesEv])
492+ pure (ovs', buttonIsOpen)
493+ pure ovs
494+
495+renderRow ::
496+ (DomBuilder t m, Renderable te, Renderable k) =>
497+ k ->
498+ ConfigValue te ->
499+ m ()
500+renderRow k v = do
501+ case v of
502+ DefaultConfigValue v' -> do
503+ elClass "span" "key-default-pristine" $ do
504+ rndr k
505+ text ": "
506+ pure ()
507+ elClass "span" "value-pristine" $ rndr v'
508+ pure ()
509+ CustomConfigValue (Left (CustomKey v')) -> do
510+ elClass "span" "key-custom-edited" $ do
511+ rndr k
512+ text ": "
513+ pure ()
514+ elClass "span" "value-edited" $ rndr v'
515+ pure ()
516+ CustomConfigValue (Right (CustomValue v')) -> do
517+ elClass "span" "key-default-edited" $ do
518+ rndr k
519+ text ": "
520+ pure ()
521+ elClass "span" "value-edited" $ rndr v'
522+ pure ()
523+ CustomConfigValue (Right (DeletedValue mv)) -> do
524+ elClass "span" "key-deleted" $ do
525+ rndr k
526+ text ": "
527+ pure ()
528+ elClass "span" "value-deleted" $ rndr `traverse_` mv
529+ pure ()
530+
531+untilReadyEv' ::
532+ (Adjustable t m, PostBuild t m, MonadHold t m) =>
533+ m a ->
534+ m (Event t b) ->
535+ m (Event t b)
536+untilReadyEv' m m' = do
537+ (_, bEvEv) <- untilReady m m'
538+ switchHold never bEvEv
539+
540+untilReady' ::
541+ (Adjustable t m, PostBuild t m) =>
542+ m a ->
543+ m b ->
544+ m (Event t b)
545+untilReady' m m' = do
546+ (_, bEv) <- untilReady m m'
547+ pure bEv
548+
549+runWithReplace' :: Adjustable t m => m a -> Event t (m b) -> m (Event t b)
550+runWithReplace' ma mbEv = do
551+ (_, bEv) <- runWithReplace ma mbEv
552+ pure bEv
553+
554+joinEvM :: (MonadHold t m, Reflex t) => Event t (Event t a) -> m (Event t a)
555+joinEvM = switchHold never
octopod-frontend/src/Frontend/UIKit/Button/Action.hsnew file+63
@@ -0,0 +1,63 @@
1+module Frontend.UIKit.Button.Action
2+ ( actionButton,
3+ ActionButtonConfig (..),
4+ ActionButtonType (..),
5+ )
6+where
7+
8+import Control.Lens
9+import Data.Default
10+import Data.Generics.Labels ()
11+import Data.Text (Text)
12+import Frontend.Classes
13+import Frontend.UIKit.Button.Common
14+import GHC.Generics (Generic)
15+import Reflex.Dom
16+
17+data ActionButtonConfig t = ActionButtonConfig
18+ { buttonText :: Text
19+ , buttonEnabled :: Dynamic t Bool
20+ , buttonType :: Maybe ActionButtonType
21+ , buttonBaseTag :: BaseButtonTag t
22+ }
23+ deriving stock (Generic)
24+
25+instance Reflex t => Default (ActionButtonConfig t) where
26+ def =
27+ ActionButtonConfig
28+ { buttonText = ""
29+ , buttonEnabled = pure True
30+ , buttonType = Nothing
31+ , buttonBaseTag = ButtonTag
32+ }
33+
34+data ActionButtonType
35+ = ArchiveActionButtonType
36+ | EditActionButtonType
37+ | RestoreActionButtonType
38+ | LogsActionButtonType
39+
40+buttonTypeClasses :: ActionButtonType -> Classes
41+buttonTypeClasses = \case
42+ ArchiveActionButtonType -> "action--archive"
43+ EditActionButtonType -> "action--edit"
44+ RestoreActionButtonType -> "action--restore"
45+ LogsActionButtonType -> "action--logs"
46+
47+actionButton ::
48+ (DomBuilder t m, PostBuild t m) =>
49+ ActionButtonConfig t ->
50+ m (Event t (Either () ()))
51+actionButton cfg =
52+ buttonEl
53+ CommonButtonConfig
54+ { constantClasses =
55+ pure $
56+ "action"
57+ <> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
58+ , enabledClasses = mempty
59+ , disabledClasses = "action--disabled"
60+ , buttonEnabled = cfg ^. #buttonEnabled
61+ , buttonText = textBuilder $ cfg ^. #buttonText
62+ , buttonBaseTag = cfg ^. #buttonBaseTag
63+ }
octopod-frontend/src/Frontend/UIKit/Button/Common.hsnew file+101
@@ -0,0 +1,101 @@
1+module Frontend.UIKit.Button.Common
2+ ( CommonButtonConfig (..),
3+ BaseButtonTag (..),
4+ buttonEl,
5+ TextBuilder (..),
6+ textBuilder,
7+ )
8+where
9+
10+import Control.Lens hiding (element)
11+import Data.Generics.Labels ()
12+import Data.Map (Map)
13+import Data.Proxy
14+import Data.String
15+import Data.Text (Text)
16+import qualified Data.Text as T
17+import Frontend.Classes
18+import GHC.Generics (Generic)
19+import Reflex.Dom
20+
21+textBuilder :: Text -> TextBuilder t
22+textBuilder t = TextBuilder $ text t
23+
24+newtype TextBuilder t = TextBuilder
25+ { unTextBuilder ::
26+ forall m.
27+ ( DomBuilder t m
28+ , PostBuild t m
29+ ) =>
30+ m ()
31+ }
32+
33+instance IsString (TextBuilder t) where
34+ fromString t = TextBuilder (text . T.pack $ t)
35+
36+data CommonButtonConfig t = CommonButtonConfig
37+ { constantClasses :: Dynamic t Classes
38+ , enabledClasses :: Classes
39+ , disabledClasses :: Classes
40+ , buttonEnabled :: Dynamic t Bool
41+ , buttonText :: TextBuilder t
42+ , buttonBaseTag :: BaseButtonTag t
43+ }
44+ deriving stock (Generic)
45+
46+data BaseButtonTag t = ButtonTag | ATag (Dynamic t Text)
47+
48+baseTag :: Reflex t => BaseButtonTag t -> (Text, Dynamic t (Map Text Text))
49+baseTag ButtonTag = ("button", pure $ "type" =: "button")
50+baseTag (ATag urlDyn) =
51+ ( "a"
52+ , urlDyn <&> \url -> "href" =: url <> "target" =: "_blank"
53+ )
54+
55+buttonEl ::
56+ forall m t.
57+ (DomBuilder t m, PostBuild t m) =>
58+ CommonButtonConfig t ->
59+ m (Event t (Either () ()))
60+buttonEl cfg = do
61+ let (t, tagAttrsDyn) = baseTag (cfg ^. #buttonBaseTag)
62+ attrsDyn = do
63+ enabled <- cfg ^. #buttonEnabled
64+ let (enabledClasses, enabledAttrs) = case enabled of
65+ True -> (cfg ^. #enabledClasses, mempty)
66+ False -> (cfg ^. #disabledClasses, "disabled" =: "")
67+ cs <- cfg ^. #constantClasses
68+ tagAttrs <- tagAttrsDyn
69+ pure $
70+ tagAttrs
71+ <> "class" =: destructClasses (enabledClasses <> cs)
72+ <> enabledAttrs
73+ modAttrs <- dynamicAttributesToModifyAttributes attrsDyn
74+ let elCfg =
75+ (def @(ElementConfig EventResult t (DomBuilderSpace m)))
76+ & elementConfig_modifyAttributes .~ fmapCheap mapKeysToAttributeName modAttrs
77+ & elementConfig_eventSpec
78+ %~~ addEventSpecFlags
79+ (Proxy @(DomBuilderSpace m))
80+ Contextmenu
81+ ( const
82+ EventFlags
83+ { _eventFlags_propagation = Propagation_StopImmediate
84+ , _eventFlags_preventDefault = False
85+ }
86+ )
87+ & elementConfig_eventSpec
88+ %~~ addEventSpecFlags
89+ (Proxy @(DomBuilderSpace m))
90+ Click
91+ ( const
92+ EventFlags
93+ { _eventFlags_propagation = Propagation_StopImmediate
94+ , _eventFlags_preventDefault = False
95+ }
96+ )
97+ (bEl, _) <- element t elCfg (unTextBuilder (cfg ^. #buttonText))
98+ pure $ leftmost [Left <$> domEvent Click bEl, Right <$> domEvent Contextmenu bEl]
99+
100+(%~~) :: ASetter' s a -> (a -> a) -> s -> s
101+(%~~) = (%~)
octopod-frontend/src/Frontend/UIKit/Button/Dash.hsnew file+71
@@ -0,0 +1,71 @@
1+module Frontend.UIKit.Button.Dash
2+ ( dashButton,
3+ DashButtonType (..),
4+ DashButtonConfig (..),
5+ DashButtonStyle (..),
6+ )
7+where
8+
9+import Control.Lens
10+import Data.Default
11+import Data.Generics.Labels ()
12+import Data.Text (Text)
13+import Frontend.Classes
14+import Frontend.UIKit.Button.Common
15+import GHC.Generics (Generic)
16+import Reflex.Dom
17+
18+data DashButtonConfig t = DashButtonConfig
19+ { buttonText :: Text
20+ , buttonEnabled :: Dynamic t Bool
21+ , buttonType :: Maybe DashButtonType
22+ , buttonStyle :: DashButtonStyle
23+ }
24+ deriving stock (Generic)
25+
26+data DashButtonStyle
27+ = RegularDashButtonStyle
28+ | SmallDashButtonStyle
29+ | OverridesDashButtonStyle
30+
31+dashButtonStyleClasses :: DashButtonStyle -> Classes
32+dashButtonStyleClasses RegularDashButtonStyle = mempty
33+dashButtonStyleClasses SmallDashButtonStyle = "dash--smaller"
34+dashButtonStyleClasses OverridesDashButtonStyle = "overrides__add"
35+
36+instance Reflex t => Default (DashButtonConfig t) where
37+ def =
38+ DashButtonConfig
39+ { buttonText = ""
40+ , buttonEnabled = pure True
41+ , buttonType = Nothing
42+ , buttonStyle = RegularDashButtonStyle
43+ }
44+
45+data DashButtonType
46+ = AddDashButtonType
47+ | BackDashButtonType
48+
49+buttonTypeClasses :: DashButtonType -> Classes
50+buttonTypeClasses = \case
51+ AddDashButtonType -> "dash--add"
52+ BackDashButtonType -> "dash--back"
53+
54+dashButton ::
55+ (DomBuilder t m, PostBuild t m) =>
56+ DashButtonConfig t ->
57+ m (Event t (Either () ()))
58+dashButton cfg =
59+ buttonEl
60+ CommonButtonConfig
61+ { constantClasses =
62+ pure $
63+ "dash"
64+ <> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
65+ <> dashButtonStyleClasses (cfg ^. #buttonStyle)
66+ , enabledClasses = mempty
67+ , disabledClasses = "dash--disabled"
68+ , buttonEnabled = cfg ^. #buttonEnabled
69+ , buttonText = textBuilder $ cfg ^. #buttonText
70+ , buttonBaseTag = ButtonTag
71+ }
octopod-frontend/src/Frontend/UIKit/Button/Expander.hsnew file+86
@@ -0,0 +1,86 @@
1+module Frontend.UIKit.Button.Expander
2+ ( expanderButton,
3+ ExpanderButtonConfig (..),
4+ ExpanderButtonStyle (..),
5+ ExpanderState (..),
6+ ExpanderButtonType (..),
7+ )
8+where
9+
10+import Control.Lens
11+import Control.Monad.Fix
12+import Data.Default
13+import Data.Functor
14+import Data.Generics.Labels ()
15+import Data.Text (Text)
16+import Frontend.Classes
17+import Frontend.UIKit.Button.Common
18+import GHC.Generics (Generic)
19+import Reflex.Dom
20+
21+data ExpanderButtonConfig t = ExpanderButtonConfig
22+ { buttonText :: Dynamic t Text
23+ , buttonInitialState :: ExpanderState
24+ , buttonType :: Maybe ExpanderButtonType
25+ , buttonStyle :: ExpanderButtonStyle
26+ }
27+ deriving stock (Generic)
28+
29+data ExpanderState = ExpandedState | ContractedState
30+ deriving stock (Eq)
31+
32+toggleState :: ExpanderState -> ExpanderState
33+toggleState ExpandedState = ContractedState
34+toggleState ContractedState = ExpandedState
35+
36+expanderButtonStateClasses :: ExpanderState -> Classes
37+expanderButtonStateClasses ExpandedState = "expander--open"
38+expanderButtonStateClasses ContractedState = mempty
39+
40+instance Reflex t => Default (ExpanderButtonConfig t) where
41+ def =
42+ ExpanderButtonConfig
43+ { buttonText = ""
44+ , buttonInitialState = ContractedState
45+ , buttonType = Nothing
46+ , buttonStyle = RegularExpanderButtonStyle
47+ }
48+
49+data ExpanderButtonType
50+ = ListingExpanderButton
51+
52+buttonTypeClasses :: ExpanderButtonType -> Classes
53+buttonTypeClasses = \case
54+ ListingExpanderButton -> "listing__more"
55+
56+buttonStyleClasses :: ExpanderButtonStyle -> Classes
57+buttonStyleClasses RegularExpanderButtonStyle = mempty
58+buttonStyleClasses StandaloneExpanderButtonStyle = "expander--stand-alone"
59+
60+data ExpanderButtonStyle
61+ = RegularExpanderButtonStyle
62+ | StandaloneExpanderButtonStyle
63+
64+expanderButton ::
65+ (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m) =>
66+ ExpanderButtonConfig t ->
67+ m (Dynamic t ExpanderState)
68+expanderButton cfg = mdo
69+ stateDyn <- foldDyn (\() -> toggleState) (cfg ^. #buttonInitialState) (toggleEv $> ())
70+ let constantClasses =
71+ "expander"
72+ <> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
73+ <> buttonStyleClasses (cfg ^. #buttonStyle)
74+ toggleEv <-
75+ buttonEl
76+ CommonButtonConfig
77+ { constantClasses = do
78+ state <- stateDyn
79+ pure $ constantClasses <> expanderButtonStateClasses state
80+ , enabledClasses = mempty
81+ , disabledClasses = mempty
82+ , buttonEnabled = pure True
83+ , buttonText = TextBuilder $ dynText $ cfg ^. #buttonText
84+ , buttonBaseTag = ButtonTag
85+ }
86+ pure stateDyn
octopod-frontend/src/Frontend/UIKit/Button/Large.hsnew file+96
@@ -0,0 +1,96 @@
1+module Frontend.UIKit.Button.Large
2+ ( largeButton,
3+ LargeButtonType (..),
4+ LargeButtonConfig (..),
5+ LargeButtonStyle (..),
6+ LargeButtonPriority (..),
7+ BaseButtonTag (..),
8+ )
9+where
10+
11+import Control.Lens
12+import Data.Default
13+import Data.Generics.Labels ()
14+import Data.Text (Text)
15+import Frontend.Classes
16+import Frontend.UIKit.Button.Common
17+import GHC.Generics (Generic)
18+import Reflex.Dom
19+
20+data LargeButtonConfig t = LargeButtonConfig
21+ { buttonText :: Text
22+ , buttonEnabled :: Dynamic t Bool
23+ , buttonType :: Dynamic t (Maybe LargeButtonType)
24+ , buttonPriority :: LargeButtonPriority
25+ , buttonStyle :: LargeButtonStyle
26+ , buttonBaseTag :: BaseButtonTag t
27+ }
28+ deriving stock (Generic)
29+
30+data LargeButtonPriority = PrimaryLargeButton | SecondaryLargeButton
31+
32+buttonPriorityClasses :: LargeButtonPriority -> Classes
33+buttonPriorityClasses PrimaryLargeButton = mempty
34+buttonPriorityClasses SecondaryLargeButton = "button--secondary"
35+
36+data LargeButtonStyle
37+ = RegularLargeButtonStyle
38+ | PopupActionLargeButtonStyle
39+ | DialogActionLargeButtonStyle
40+ | PageActionLargeButtonStyle
41+
42+buttonStyleClasses :: LargeButtonStyle -> Classes
43+buttonStyleClasses RegularLargeButtonStyle = mempty
44+buttonStyleClasses PopupActionLargeButtonStyle = "popup__action"
45+buttonStyleClasses DialogActionLargeButtonStyle = "dialog__action"
46+buttonStyleClasses PageActionLargeButtonStyle = "page__action"
47+
48+instance Reflex t => Default (LargeButtonConfig t) where
49+ def =
50+ LargeButtonConfig
51+ { buttonText = ""
52+ , buttonEnabled = pure True
53+ , buttonType = pure Nothing
54+ , buttonStyle = RegularLargeButtonStyle
55+ , buttonPriority = PrimaryLargeButton
56+ , buttonBaseTag = ButtonTag
57+ }
58+
59+data LargeButtonType
60+ = AddLargeButtonType
61+ | ArchiveLargeButtonType
62+ | RestoreLargeButtonType
63+ | EditLargeButtonType
64+ | LogsLargeButtonType
65+ | SaveLargeButtonType
66+ | LoadingLargeButtonType
67+
68+buttonTypeClasses :: LargeButtonType -> Classes
69+buttonTypeClasses AddLargeButtonType = "button--add"
70+buttonTypeClasses ArchiveLargeButtonType = "button--archive"
71+buttonTypeClasses RestoreLargeButtonType = "button--restore"
72+buttonTypeClasses EditLargeButtonType = "button--edit"
73+buttonTypeClasses LogsLargeButtonType = "button--logs"
74+buttonTypeClasses SaveLargeButtonType = "button--save"
75+buttonTypeClasses LoadingLargeButtonType = "button--save-loading"
76+
77+largeButton ::
78+ (DomBuilder t m, PostBuild t m) =>
79+ LargeButtonConfig t ->
80+ m (Event t (Either () ()))
81+largeButton cfg =
82+ buttonEl
83+ CommonButtonConfig
84+ { constantClasses = do
85+ bType <- cfg ^. #buttonType
86+ pure $
87+ "button"
88+ <> maybe mempty buttonTypeClasses bType
89+ <> buttonStyleClasses (cfg ^. #buttonStyle)
90+ <> buttonPriorityClasses (cfg ^. #buttonPriority)
91+ , enabledClasses = mempty
92+ , disabledClasses = "button--disabled"
93+ , buttonEnabled = cfg ^. #buttonEnabled
94+ , buttonText = textBuilder $ cfg ^. #buttonText
95+ , buttonBaseTag = cfg ^. #buttonBaseTag
96+ }
octopod-frontend/src/Frontend/UIKit/Button/Sort.hsnew file+58
@@ -0,0 +1,58 @@
1+module Frontend.UIKit.Button.Sort
2+ ( sortButton,
3+ SortButtonConfig (..),
4+ SortButtonState (..),
5+ )
6+where
7+
8+import Control.Lens
9+import Data.Default
10+import Data.Generics.Labels ()
11+import Data.Text (Text)
12+import Frontend.Classes
13+import Frontend.UIKit.Button.Common
14+import GHC.Generics (Generic)
15+import Reflex.Dom
16+
17+data SortButtonConfig t = SortButtonConfig
18+ { buttonText :: Text
19+ , buttonEnabled :: Dynamic t Bool
20+ , buttonState :: Dynamic t (Maybe SortButtonState)
21+ }
22+ deriving stock (Generic)
23+
24+instance Reflex t => Default (SortButtonConfig t) where
25+ def =
26+ SortButtonConfig
27+ { buttonText = ""
28+ , buttonEnabled = pure True
29+ , buttonState = pure Nothing
30+ }
31+
32+data SortButtonState
33+ = SortAscButtonState
34+ | SortDescButtonState
35+
36+buttonSortStateClasses :: SortButtonState -> Classes
37+buttonSortStateClasses = \case
38+ SortAscButtonState -> "sort--active sort--asc"
39+ SortDescButtonState -> "sort--active sort--desc"
40+
41+sortButton ::
42+ (DomBuilder t m, PostBuild t m) =>
43+ SortButtonConfig t ->
44+ m (Event t (Either () ()))
45+sortButton cfg =
46+ buttonEl
47+ CommonButtonConfig
48+ { constantClasses = do
49+ state <- cfg ^. #buttonState
50+ pure $
51+ "sort"
52+ <> maybe mempty buttonSortStateClasses state
53+ , enabledClasses = mempty
54+ , disabledClasses = mempty
55+ , buttonEnabled = cfg ^. #buttonEnabled
56+ , buttonText = textBuilder $ cfg ^. #buttonText
57+ , buttonBaseTag = ButtonTag
58+ }
octopod-frontend/src/Frontend/UIKit/Button/Static.hsnew file+84
@@ -0,0 +1,84 @@
1+module Frontend.UIKit.Button.Static
2+ ( closeNotificationButton,
3+ closeClassicPopupButton,
4+ closePopupButton,
5+ deleteOverrideButton,
6+ undoOverrideButton,
7+ dropButton,
8+ )
9+where
10+
11+import Frontend.UIKit.Button.Common
12+import Reflex.Dom
13+
14+closeNotificationButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
15+closeNotificationButton =
16+ buttonEl
17+ CommonButtonConfig
18+ { constantClasses = pure "notification__close"
19+ , enabledClasses = mempty
20+ , disabledClasses = mempty
21+ , buttonEnabled = pure True
22+ , buttonText = ""
23+ , buttonBaseTag = ButtonTag
24+ }
25+
26+closeClassicPopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
27+closeClassicPopupButton =
28+ buttonEl
29+ CommonButtonConfig
30+ { constantClasses = pure "classic-popup__close"
31+ , enabledClasses = mempty
32+ , disabledClasses = mempty
33+ , buttonEnabled = pure True
34+ , buttonText = ""
35+ , buttonBaseTag = ButtonTag
36+ }
37+
38+closePopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
39+closePopupButton =
40+ buttonEl
41+ CommonButtonConfig
42+ { constantClasses = pure "popup__close"
43+ , enabledClasses = mempty
44+ , disabledClasses = mempty
45+ , buttonEnabled = pure True
46+ , buttonText = ""
47+ , buttonBaseTag = ButtonTag
48+ }
49+
50+deleteOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
51+deleteOverrideButton =
52+ buttonEl
53+ CommonButtonConfig
54+ { constantClasses = pure "editable-row__delete spot spot--cancel"
55+ , enabledClasses = mempty
56+ , disabledClasses = mempty
57+ , buttonEnabled = pure True
58+ , buttonText = ""
59+ , buttonBaseTag = ButtonTag
60+ }
61+
62+undoOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
63+undoOverrideButton =
64+ buttonEl
65+ CommonButtonConfig
66+ { constantClasses = pure "editable-row__delete spot spot--undo"
67+ , enabledClasses = mempty
68+ , disabledClasses = mempty
69+ , buttonEnabled = pure True
70+ , buttonText = ""
71+ , buttonBaseTag = ButtonTag
72+ }
73+
74+dropButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
75+dropButton =
76+ buttonEl
77+ CommonButtonConfig
78+ { constantClasses = pure "drop__handler"
79+ , enabledClasses = mempty
80+ , disabledClasses = mempty
81+ , buttonEnabled = pure True
82+ , buttonText = "Actions"
83+ , buttonBaseTag = ButtonTag
84+ }
octopod-frontend/src/Frontend/UIKit/Button/Tree.hsnew file+84
@@ -0,0 +1,84 @@
1+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
2+
3+{-# HLINT ignore "Use newtype instead of data" #-}
4+module Frontend.UIKit.Button.Tree
5+ ( treeButton,
6+ TreeButtonConfig (..),
7+ TextBuilder (..),
8+ TreeButtonStatus (..),
9+ TreeButtonApply (..),
10+ )
11+where
12+
13+import Control.Lens
14+import Control.Monad
15+import Control.Monad.Fix
16+import Data.Default
17+import Data.Functor
18+import Data.Generics.Labels ()
19+import Frontend.UIKit.Button.Common
20+import GHC.Generics (Generic)
21+import Reflex.Dom
22+import Reflex.Network (networkView)
23+
24+data TreeButtonConfig t = TreeButtonConfig
25+ { buttonText :: TextBuilder t
26+ , subtreeHasChanges :: Bool
27+ , visible :: Dynamic t Bool
28+ , -- | From outside
29+ forceState :: Event t Bool
30+ }
31+ deriving stock (Generic)
32+
33+instance Reflex t => Default (TreeButtonConfig t) where
34+ def =
35+ TreeButtonConfig
36+ { buttonText = ""
37+ , subtreeHasChanges = False
38+ , visible = pure True
39+ , forceState = never
40+ }
41+
42+treeButton ::
43+ (DomBuilder t m, PostBuild t m, MonadHold t m, MonadFix m) =>
44+ TreeButtonConfig t ->
45+ -- | (<Open?>, <Click>)
46+ m (Event t (Bool, Either () ()))
47+treeButton cfg = mdo
48+ (traceEvent "treeButton" -> clickedEv) <-
49+ networkView >=> switchHold never $
50+ (cfg ^. #visible) <&> \visible ->
51+ if visible
52+ then
53+ buttonEl
54+ CommonButtonConfig
55+ { constantClasses = do
56+ open <- openDyn
57+ pure $
58+ "collapse__head"
59+ <> (if cfg ^. #subtreeHasChanges then "collapse__head--has-changes" else mempty)
60+ <> (if open then mempty else "collapse--expanded")
61+ , buttonText = cfg ^. #buttonText
62+ , buttonBaseTag = ButtonTag
63+ , buttonEnabled = pure True
64+ , disabledClasses = mempty
65+ , enabledClasses = mempty
66+ }
67+ else pure never
68+ openDyn <-
69+ foldDyn
70+ ( \case
71+ Left () -> not
72+ Right x -> const x
73+ )
74+ False
75+ $ (leftmost [Right <$> cfg ^. #forceState, clickedEv $> Left ()])
76+ pure $ coincidence $ (\click -> (,click) <$> updated openDyn) <$> clickedEv
77+
78+data TreeButtonStatus
79+ = TreeButtonClosed
80+ | TreeButtonOpen
81+
82+data TreeButtonApply
83+ = TreeButtonApplyToSelf
84+ | TreeButtonApplyToSubtree
octopod-frontend/src/Frontend/Utils.hs+255−479
@@ -6,62 +6,61 @@
66--frontend modules.
77module Frontend.Utils
88 ( sidebar,
9− buttonClass,
10− buttonClassEnabled,
119 wrapRequestErrors,
1210 octopodTextInput,
1311 deploymentPopupBody,
1412 ClickedElement (..),
1513 pageNotification,
16− aButtonClassEnabled,
17− buttonClassEnabled',
1814 kubeDashboardUrl,
19− loadingCommonWidget,
20− errorCommonWidget,
21− aButtonDynClass',
2215 formatPosixToDate,
2316 overridesWidget,
24− aButtonClass',
2517 statusWidget,
2618 elementClick,
2719 showT,
2820 DeploymentPageNotification (..),
2921 formatPosixToDateTime,
3022 dropdownWidget,
31− dropdownWidget',
32− buttonDynClass,
23+ deploymentConfigProgressiveComponents,
24+ -- deploymentConfigProgressive,
25+ holdClearingWith,
26+ unitEv,
27+ (<&&>),
28+ RequestErrorHandler,
29+ debounceDyn,
30+ catchReturns,
31+ holdDynMaybe,
32+ constructWorkingOverridesEv,
3333 )
3434where
3535
3636import Common.Types as CT
37+import Control.Arrow
3738import Control.Lens
3839import Control.Monad
3940import Control.Monad.Reader
40−import Data.Align
41−import qualified Data.Foldable as F
41+import qualified Data.ConfigTree as CT
4242import Data.Functor
4343import Data.Generics.Labels ()
4444import Data.Generics.Sum
45−import Data.Map (Map)
4645import qualified Data.Map as M
47−import qualified Data.Map.Ordered.Strict as OM
4846import Data.Maybe (fromMaybe)
49−import Data.Monoid
50−import Data.Text as T (Text, intercalate, null, pack)
51−import Data.These
47+import Data.String
48+import Data.Text as T (Text, null, pack, strip)
5249import Data.Time
5350import Data.UniqMap
5451import Data.Unique
5552import Data.Witherable
5653import Data.WorkingOverrides
54+import Debug.Trace
5755import Frontend.API
5856import Frontend.GHCJS
57+import Frontend.UIKit
5958import GHCJS.DOM
6059import GHCJS.DOM.Element as DOM
6160import GHCJS.DOM.EventM (on, target)
6261import GHCJS.DOM.GlobalEventHandlers as Events (click)
63−import GHCJS.DOM.Node as DOM
6462import Reflex.Dom as R
63+import Reflex.Dom.Renderable
6564import Reflex.Network
6665import Servant.Common.Req
6766import Servant.Reflex.Extra
@@ -76,49 +75,30 @@ elementClick = do
7675 doc <- currentDocumentUnchecked
7776 wrapDomEvent doc (`on` Events.click) $ ClickedElement <$> target
7877
79−-- | Dropdown widget which binds its own @document click@ event.
80−dropdownWidget ::
81− MonadWidget t m =>
82− -- | Button widget which opens the dropdown widget.
83− m () ->
84− -- | Widget with the dropdown list.
85− -- Returns an event carrying the user's selection.
86− m (Event t a) ->
87− m (Event t a)
88−dropdownWidget btn body = mdo
89− clickedEl <- elementClick
90− dropdownWidget' clickedEl btn body
91−
9278-- | Similar to 'dropdownWidget' but uses @document click@ event that may be
9379-- shared between other widgets.
94−dropdownWidget' ::
80+dropdownWidget ::
9581 MonadWidget t m =>
96− -- | Document click event that may be shared between other widgets.
97− Event t ClickedElement ->
98− -- | Button widget which opens the dropdown widget.
99− m () ->
10082 -- | Widget with the dropdown list.
10183 -- Returns an event carrying the user's selection.
10284 m (Event t a) ->
10385 m (Event t a)
104−dropdownWidget' clickedEl btn body = mdo
105− clickInsideEv <- performEvent $
106− ffor clickedEl $ \(ClickedElement clicked) ->
107− DOM.contains (_element_raw btnEl) clicked
108− openedDyn <- foldDyn switchState False $ clickInsideEv
109− let switchState ev cur = ev && not cur
110− wrapperClassDyn = ffor openedDyn $ \case
86+dropdownWidget body = mdo
87+ openedDyn <- toggle False $ btnEl
88+ let wrapperClassDyn = ffor openedDyn $ \case
11189 True -> "class" =: "drop drop--actions drop--expanded"
11290 False -> "class" =: "drop drop--actions"
113− (btnEl, (_, wEv)) <- elDynAttr'
91+ (btnEl, wEv) <- elDynAttr
11492 "div"
11593 wrapperClassDyn
11694 $ do
117− btn
118− elDynAttr'
119− "div"
120− (constDyn $ "class" =: "drop__dropdown")
121− body
95+ clickedEv <- dropButton
96+ aEv <-
97+ elDynAttr
98+ "div"
99+ (constDyn $ "class" =: "drop__dropdown")
100+ body
101+ pure (clickedEv, aEv)
122102 pure wEv
123103
124104showT :: Show a => a -> Text
@@ -157,148 +137,6 @@ sidebar showEv closeEv m = mdo
157137 ]
158138 pure $ switchDyn $ fst <$> resultEvDyn
159139
160−-- | Dark unclickable background for opened sidebar.
161−popupOverlay :: DomBuilder t m => m ()
162−popupOverlay =
163− elAttr "div" ("class" =: "popup__overlay" <> "aria-hidden" =: "true") blank
164−
165−-- | Button with customizable classes and label text.
166−buttonClass ::
167− (DomBuilder t m, PostBuild t m) =>
168− -- | Classes.
169− Text ->
170− -- | Label text.
171− Text ->
172− m (Event t ())
173−buttonClass cl lbl = do
174− (bEl, _) <-
175− elDynAttr'
176− "button"
177− (constDyn $ "class" =: cl <> "type" =: "button")
178− $ text lbl
179− return $ domEvent Click bEl
180−
181−-- | Advanced version of 'buttonClass' with dynamic arguments.
182−buttonDynClass ::
183− (DomBuilder t m, PostBuild t m) =>
184− -- | Classes.
185− Dynamic t Text ->
186− -- | Label text.
187− Dynamic t Text ->
188− m (Event t ())
189−buttonDynClass clDyn lblDyn = do
190− let attrDyn = ffor clDyn $ \cl -> "class" =: cl <> "type" =: "button"
191− (bEl, _) <-
192− elDynAttr' "button" attrDyn $
193− dynText lblDyn
194− return $ domEvent Click bEl
195−
196−-- | Advanced version of 'buttonClass' with a disabled state.
197−buttonClassEnabled ::
198− (DomBuilder t m, PostBuild t m) =>
199− -- | Classes.
200− Text ->
201− -- | Label text.
202− Text ->
203− -- | Enabled flag.
204− Dynamic t Bool ->
205− m (Event t ())
206−buttonClassEnabled cl lbl dDyn = do
207− let attrDyn = ffor dDyn $ \case
208− True -> "class" =: cl <> "type" =: "button"
209− False ->
210− "class" =: (cl <> " button--disabled")
211− <> "type" =: "button"
212− <> "disabled" =: ""
213− (bEl, _) <-
214− elDynAttr' "button" attrDyn $
215− text lbl
216− return $ domEvent Click bEl
217−
218−-- | Special version of 'buttonClassEnabled' that supports custom classes for
219−-- the disabled state.
220−buttonClassEnabled' ::
221− (DomBuilder t m, PostBuild t m) =>
222− -- | Classes.
223− Text ->
224− -- | Label text.
225− Text ->
226− -- | Enabled flag.
227− Dynamic t Bool ->
228− -- | Custom classes for disabled state.
229− Text ->
230− m (Event t ())
231−buttonClassEnabled' cl lbl dDyn disClass = do
232− let attrDyn = ffor dDyn $ \case
233− True -> "class" =: cl <> "type" =: "button"
234− False ->
235− "class" =: (cl <> " " <> disClass)
236− <> "type" =: "button"
237− <> "disabled" =: ""
238− (bEl, _) <-
239− elDynAttr' "button" attrDyn $
240− text lbl
241− return $ domEvent Click bEl
242−
243−-- | Version of 'buttonClass' for links that should look like buttons.
244−aButtonClass' ::
245− (DomBuilder t m, PostBuild t m) =>
246− -- | Classes.
247− Text ->
248− -- | Label text.
249− Text ->
250− -- | Extra attributes
251− Dynamic t (Map Text Text) ->
252− m (Event t ())
253−aButtonClass' cl lbl eAttrs = do
254− (bEl, _) <-
255− elDynAttr'
256− "a"
257− (fmap (<> ("class" =: cl)) eAttrs)
258− $ text lbl
259− return $ domEvent Click bEl
260−
261−-- | Version of 'buttonDynClass' for links that should look like
262−-- buttons.
263−aButtonDynClass' ::
264− (DomBuilder t m, PostBuild t m) =>
265− -- | Classes.
266− Dynamic t Text ->
267− -- | Label text.
268− Dynamic t Text ->
269− -- | Extra attributes
270− Dynamic t (Map Text Text) ->
271− m (Event t ())
272−aButtonDynClass' clDyn lblDyn eAttrs = do
273− let attrDyn' = ffor clDyn $ \cl -> "class" =: cl
274− attrDyn = (<>) <$> eAttrs <*> attrDyn'
275− (bEl, _) <-
276− elDynAttr' "a" attrDyn $
277− dynText lblDyn
278− return $ domEvent Click bEl
279−
280−-- | Version of 'buttonClassEnabled' for links that should look like
281−-- buttons.
282−aButtonClassEnabled ::
283− (DomBuilder t m, PostBuild t m) =>
284− -- | Classes.
285− Text ->
286− -- | Label text.
287− Text ->
288− -- | Enabled flag.
289− Dynamic t Bool ->
290− m (Event t ())
291−aButtonClassEnabled cl lbl dDyn = do
292− let attrDyn = ffor dDyn $ \case
293− True -> "class" =: cl
294− False ->
295− "class" =: (cl <> " button--disabled")
296− <> "disabled" =: ""
297− (bEl, _) <-
298− elDynAttr' "a" attrDyn $
299− text lbl
300− return $ domEvent Click bEl
301−
302140-- | Formats posix seconds to date in iso8601.
303141formatPosixToDate :: FormatTime t => t -> Text
304142formatPosixToDate = pack . formatTime defaultTimeLocale (iso8601DateFormat Nothing)
@@ -322,12 +160,13 @@ statusWidget stDyn = do
322160 DeploymentNotPending UpdatePending -> loadingWidget $ text "Updating..."
323161 DeploymentNotPending ArchivePending -> loadingWidget $ text "Archiving..."
324162 DeploymentNotPending Archived -> divClass "status status--archived" $ text "Archived"
163+ DeploymentNotPending CleanupFailed -> divClass "status status--failure" $ text "Cleanup failed (contact admin)"
325164
326165-- | Text input field with label.
327166octopodTextInput ::
328167 MonadWidget t m =>
329168 -- | Input field classes.
330− Text ->
169+ Classes ->
331170 -- | Label text.
332171 Text ->
333172 -- | Placeholder for input field.
@@ -340,144 +179,46 @@ octopodTextInput ::
340179octopodTextInput clss lbl placeholder val errEv =
341180 elClass "section" "deployment__section" $ do
342181 elClass "h3" "deployment__sub-heading" $ text lbl
343− elClass "div" "deployment__widget" $
344− octopodTextInput' (pure False) clss placeholder (pure . fromMaybe "" $ val) errEv
345−
346−-- | The only text input field that is used in project forms. This input
347−-- provides automatic error message hiding after user starts typing.
348−octopodTextInput' ::
349− MonadWidget t m =>
350− -- | Disabled?
351− Dynamic t Bool ->
352− -- | Input field classes.
353− Text ->
354− -- | Placeholder for input field.
355− Text ->
356− -- | Possible init value.
357− (Dynamic t Text) ->
358− -- | Event carrying the error message.
359− Event t Text ->
360− m (Dynamic t Text, Dynamic t Bool)
361−octopodTextInput' disabledDyn clss placeholder inValDyn' errEv = mdo
362− inValDyn <- holdUniqDyn inValDyn'
363− let inValEv =
364− align (updated inValDyn) (updated valDyn)
365− & fmapMaybe
366− ( \case
367− This x -> Just x
368− These inV currV | inV /= currV -> Just inV
369− _ -> Nothing
370− )
371− let inpClass = " input"
372− inpErrClass = " input input--error"
373− isValid <-
374− holdDyn True $
375− leftmost
376− [ False <$ errEv
377− , True <$ updated valDyn
378− ]
379− classDyn <-
380− holdDyn (clss <> inpClass) $
381− leftmost
382− [ (clss <> inpErrClass) <$ errEv
383− , (clss <> inpClass) <$ updated valDyn
384− ]
385− inVal <- sample . current $ inValDyn
386− disabled <- sample . current $ disabledDyn
387− valDyn <- elDynClass "div" classDyn $ do
388− inp <-
389− inputElement $
390− def
391− & initialAttributes
392− .~ ( "type" =: "text"
393− <> "class" =: "input__widget"
394− <> "placeholder" =: placeholder
395− )
396− & inputElementConfig_setValue .~ inValEv
397− & inputElementConfig_initialValue .~ inVal
398− & inputElementConfig_elementConfig . elementConfig_initialAttributes
399− %~ (if disabled then M.insert "disabled" "disabled" else id)
400− & inputElementConfig_elementConfig . elementConfig_modifyAttributes
401− <>~ updated
402− ( do
403− disabled' <- disabledDyn
404− pure $
405− M.singleton "disabled" $
406− if disabled' then Just "disabled" else Nothing
407− )
408− widgetHold_ blank $
409− leftmost
410− [ divClass "input__output" . text <$> errEv
411− , blank <$ updated valDyn
412− ]
413− pure $ value inp
414− pure (valDyn, isValid)
415−
416−-- | Widget with a loading spinner.
417−loadingCommonWidget :: MonadWidget t m => m ()
418−loadingCommonWidget =
419− divClass "loading loading--enlarged loading--alternate" $
420− text "Loading..."
421−
422−-- | Widget with an error message.
423−errorCommonWidget :: MonadWidget t m => m ()
424−errorCommonWidget =
425− divClass "null null--data" $
426− divClass "null__content" $ do
427− elClass "b" "null__heading" $ text "Cannot retrieve the data"
428− divClass "null__message" $ text "Try to reload the page"
182+ elClass "div" "deployment__widget" $ do
183+ (value -> valDyn, validDyn) <- octopodTextInput' (pure []) (pure False) (pure mempty) (pure clss) placeholder (pure . fromMaybe "" $ val) errEv
184+ pure (valDyn, validDyn)
429185
430186-- | Widget that can show and hide overrides if there are more than 3. This
431187-- widget is used in the deployments table and the deployment action table.
432188overridesWidget ::
433− MonadWidget t m =>
189+ (MonadWidget t m, Renderable te, IsString te, Monoid te) =>
434190 -- | List of overrides.
435− Overrides l ->
191+ Overrides' te l ->
436192 m ()
437−overridesWidget (Overrides (OM.assocs -> envs)) = divClass "listing listing--for-text" $ do
438− let visible = take 3 envs
439− envLength = length envs
440− listing visible
441− when (envLength > 3) $ mdo
442− let hidden = drop 3 envs
443− showDyn <- toggle False toggleEv
444− dyn_ $
445− showDyn <&> \case
446− True -> listing hidden
447− False -> blank
448− let btnClassDyn =
449− ifThenElseDyn
450− showDyn
451− "listing__more expander expander--open"
452− "listing__more expander"
453− btnTextDyn =
454− ifThenElseDyn showDyn "Hide" $
455− "Show all (" <> showT envLength <> ")"
456− toggleEv <- buttonDynClass btnClassDyn btnTextDyn
457− blank
458− where
459− listing envs' = do
460− forM_ envs' $ \(var, val) ->
461− divClass "listing__item" $ do
462− el "b" $ text $ var <> ": "
463− case val of
464− ValueAdded v -> text v
465− ValueDeleted -> el "i" $ text "<deleted>"
466−
467−-- | @if-then-else@ helper for cases when bool value is wrapped in 'Dynamic'.
468−ifThenElseDyn ::
469− Reflex t =>
470− -- | Condition wrapped in `Dynamic`.
471− Dynamic t Bool ->
472− -- | `then` branch.
473− b ->
474− -- | `else` branch.
475− b ->
476− Dynamic t b
477−ifThenElseDyn bDyn t f =
478− bDyn <&> \case
479− True -> t
480− False -> f
193+overridesWidget (Overrides ovs) = mdo
194+ let l = CT.toFlatList ovs
195+ expandable = length l > 3
196+
197+ if expandable
198+ then mdo
199+ dyn_ $
200+ expandState <&> \s -> do
201+ let l' = case s of
202+ ExpandedState -> l
203+ ContractedState -> take 3 l
204+ showFlatConfig l'
205+ pure ()
206+
207+ expandState <-
208+ expanderButton
209+ ExpanderButtonConfig
210+ { buttonText = do
211+ s <- expandState
212+ pure $ case s of
213+ ExpandedState -> "Hide default configuration"
214+ ContractedState -> "Show full configuration"
215+ , buttonInitialState = ContractedState
216+ , buttonType = Just ListingExpanderButton
217+ , buttonStyle = RegularExpanderButtonStyle
218+ }
219+ pure ()
220+ else showFlatConfig l
221+ pure ()
481222
482223-- | Type of notification at the top of pages.
483224data DeploymentPageNotification
@@ -501,7 +242,7 @@ pageNotification notEv = mdo
501242 messageClassWidget txt cl =
502243 divClass ("page__output notification " <> cl) $ do
503244 text txt
504− buttonClass "notification__close" ""
245+ closeNotificationButton
505246 closeEv = switchDyn closeEvDyn
506247 closeEvDyn <-
507248 widgetHold (pure never) $
@@ -524,84 +265,104 @@ deploymentPopupBody ::
524265 forall t m tag.
525266 MonadWidget t m =>
526267 RequestErrorHandler t m ->
527− Maybe DeploymentTag ->
528268 Overrides 'ApplicationLevel ->
529269 Overrides 'DeploymentLevel ->
530270 -- | \"Edit request\" failure event.
531271 Event t (ReqResult tag CommandResponse) ->
532272 -- | Returns deployment update and validation state.
533273 m (Dynamic t (Maybe DeploymentUpdate))
534−deploymentPopupBody hReq defTag defAppOv defDepOv errEv = mdo
274+deploymentPopupBody hReq defAppOv defDepOv errEv = mdo
535275 pb <- getPostBuild
536− defDep <- defaultDeploymentOverrides pb >>= hReq >>= holdDynMaybe
276+ (defDepEv, defAppEv, depCfgEv) <-
277+ deploymentConfigProgressiveComponents hReq deploymentOvsDynDebounced
278+
279+ defAppEvM <- holdClearingWith defAppEv (unitEv deploymentOvsDyn)
280+
537281 depKeys <- deploymentOverrideKeys pb >>= hReq
538282
539283 let commandResponseEv = fmapMaybe commandResponse errEv
540− tagErrEv = getTagError commandResponseEv tagDyn
541284 void $ hReq (errEv `R.difference` commandResponseEv)
542285
543− (tagDyn, tOkEv) <- octopodTextInput "tag" "Tag" "Tag" (unDeploymentTag <$> defTag) tagErrEv
544−
545− let holdDCfg :: Dynamic t (Maybe (DefaultConfig l)) -> Overrides l -> m (Dynamic t (Overrides l))
546− holdDCfg dCfgDyn ovs = mdo
547− ovsDyn <- holdDyn ovs ovsEv
548− x <- attachDyn (current ovsDyn) dCfgDyn
549− ovsEv <- dyn (x <&> \(ovs', dCfg) -> envVarsInput dCfg ovs') >>= switchHold never
550− pure ovsDyn
551− holdDKeys n keysDyn = do
552− let loading = loadingCommonWidget
553− popUpSection n $
554− widgetHold_ loading $
555− keysDyn <&> \case
556− Just keys -> el "ul" $
557− forM_ keys $ \key -> el "li" $ text key
558− Nothing -> loading
559−
560− deploymentOvsDyn <- popUpSection "Deployment overrides" $ holdDCfg defDep defDepOv
561− holdDKeys "Deployment keys" (Just <$> depKeys)
562− readyDeploymentCfgDebounced <- debounce 0.5 . catMaybes $
563− updated $ do
564− defDep >>= \case
565− Nothing -> pure Nothing
566− Just depDCfg -> do
567− depOv <- deploymentOvsDyn
568− pure $ Just $ applyOverrides depOv depDCfg
569− defApp <-
570− waitForValuePromptly
571− readyDeploymentCfgDebounced
572− ( \deploymentCfg -> do
573− pb' <- getPostBuild
574− defaultApplicationOverrides (pure $ Right deploymentCfg) pb' >>= hReq >>= immediateNothing
575− )
576− >>= holdDyn Nothing
577− appKeys <- waitForValuePromptly readyDeploymentCfgDebounced $ \deploymentCfg -> do
286+ let holdDCfg ::
287+ Dynamic t [Text] ->
288+ (DefaultConfig l) ->
289+ Overrides l ->
290+ m (Dynamic t (Overrides l), Dynamic t (Overrides l))
291+ holdDCfg (traceDyn "values" -> values) cfgDyn@(DefaultConfig ct) ovs = mdo
292+ let ovs' = constructWorkingOverrides cfgDyn ovs
293+ lookupOverride k = CT.lookup (CT.deconstructConfigKey k) ct
294+ (resCfgDyn :: Dynamic t [Dynamic t WorkingOverride]) <- envVarsInput values lookupOverride ovs'
295+ (resEvDebounced :: Event t [Dynamic t WorkingOverride]) <- debounce 2 $ updated resCfgDyn
296+ let resDyn' = join . fmap destructWorkingOverridesDyn $ resCfgDyn
297+ debouncedResDyn <- fmap join . holdDyn (pure ovs) . fmap destructWorkingOverridesDyn $ resEvDebounced
298+ pure (resDyn', debouncedResDyn)
299+
300+ depKeysDyn <- holdDyn [] depKeys
301+
302+ (join . fmap fst &&& join . fmap snd -> (deploymentOvsDyn, deploymentOvsDynDebounced)) <-
303+ deploymentSection "Deployment configuration" $
304+ networkHold (loadingOverrides $> (pure defDepOv, pure defDepOv)) $
305+ defDepEv <&> \defDep -> holdDCfg depKeysDyn defDep defDepOv
306+
307+ appKeys <- waitForValuePromptly depCfgEv $ \deploymentCfg -> do
578308 pb' <- getPostBuild
579309 applicationOverrideKeys (pure $ Right deploymentCfg) pb' >>= hReq >>= immediateNothing
580− applicationOvsDyn <- popUpSection "App overrides" $ holdDCfg defApp defAppOv
581− holdDKeys "App keys" appKeys
582310
583− validDyn <- holdDyn True $ updated tOkEv
311+ appKeysDyn <- holdDyn [] $ catMaybes appKeys
312+ (join -> applicationOvsMDyn) <-
313+ deploymentSection "App configuration" >=> holdDyn (pure Nothing) . fmap sequenceA $
314+ networkView $
315+ defAppEvM <&> \case
316+ Nothing -> loadingOverrides $> Nothing
317+ Just defApp -> Just . fst <$> holdDCfg appKeysDyn defApp defAppOv
318+
319+ pure $ do
320+ depCfg <- deploymentOvsDyn
321+ appOvsM <- applicationOvsMDyn
322+ pure $ do
323+ appOvs <- appOvsM
324+ Just $
325+ DeploymentUpdate
326+ { appOverrides = appOvs
327+ , deploymentOverrides = depCfg
328+ }
329+
330+deploymentConfigProgressiveComponents ::
331+ MonadWidget t m =>
332+ RequestErrorHandler t m ->
333+ Dynamic t (Overrides 'DeploymentLevel) ->
334+ m
335+ ( Event t (DefaultConfig 'DeploymentLevel)
336+ , Event t (DefaultConfig 'ApplicationLevel)
337+ , Event t (Config 'DeploymentLevel)
338+ )
339+deploymentConfigProgressiveComponents hReq depOvsDyn = do
340+ pb <- getPostBuild
341+ defDepEv <- defaultDeploymentOverrides pb >>= hReq
342+ defDepMDyn <- holdDynMaybe defDepEv
343+ let depCfgEv = catMaybes . updated $ do
344+ defDepM <- defDepMDyn
345+ depOvs <- depOvsDyn
346+ pure $ defDepM <&> applyOverrides depOvs
347+ defAppEv <-
348+ fmap switchDyn $
349+ networkHold (pure never) $
350+ depCfgEv <&> \depCfg -> do
351+ pb' <- getPostBuild
352+ defaultApplicationOverrides (pure $ Right depCfg) pb' >>= hReq
353+ pure (defDepEv, defAppEv, depCfgEv)
354+
355+constructWorkingOverridesEv ::
356+ MonadWidget t m => Event t (DefaultConfig l) -> Dynamic t (Overrides l) -> m (Event t WorkingOverrides)
357+constructWorkingOverridesEv defCfgEv ovsDyn = do
358+ defCfgMDyn <- holdDynMaybe defCfgEv
584359 pure $
585− validDyn >>= \case
586− False -> pure Nothing
587− True -> do
588− depCfg <- deploymentOvsDyn
589− appOvs <- applicationOvsDyn
590− tag' <- DeploymentTag <$> tagDyn
591− pure $
592− Just $
593− DeploymentUpdate
594− { newTag = tag'
595− , appOverrides = appOvs
596− , deploymentOverrides = depCfg
597− }
598− where
599− getTagError crEv tagDyn =
600− let tagErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _2)) crEv
601− tagErrEv = ffilter (/= "") $ T.intercalate ". " <$> tagErrEv'
602− badTagText = "Tag should not be empty"
603− badNameEv = badTagText <$ ffilter (== "") (updated tagDyn)
604− in leftmost [tagErrEv, badNameEv]
360+ catMaybes . updated $ do
361+ defCfgM <- defCfgMDyn
362+ ovs <- ovsDyn
363+ pure $ do
364+ defCfg <- defCfgM
365+ pure $ constructWorkingOverrides defCfg ovs
605366
606367waitForValuePromptly :: (MonadHold t m, Adjustable t m) => Event t x -> (x -> m (Event t y)) -> m (Event t y)
607368waitForValuePromptly ev f = fmap switchPromptlyDyn $ networkHold (pure never) $ f <$> ev
@@ -622,9 +383,14 @@ wrapRequestErrors f = mdo
622383 (x, ev :: Event t (PatchMap Unique Text)) <- runEventWriterT $
623384 f $ \reqEv -> do
624385 k <- liftIO newUnique
625− tellEvent $ fmapCheap (PatchMap . M.singleton k . reqErrorBody) reqEv
386+ tellEvent $ fmap (PatchMap . M.singleton k . fmap catchEmptyErrors . reqErrorBody) reqEv
626387 pure $ fmapMaybeCheap reqSuccess reqEv
627388 pure x
389+ where
390+ catchEmptyErrors t
391+ | (T.null . T.strip) t =
392+ "Something went wrong, but no explanation was provided."
393+ catchEmptyErrors t = t
628394
629395-- | The widget used to display errors.
630396errorHeader ::
@@ -632,104 +398,90 @@ errorHeader ::
632398 -- | Message text.
633399 Dynamic t Text ->
634400 m ()
635−errorHeader appErr = do
636− divClass "deployment__output notification notification--danger" $ do
637− el "b" $ text "App error: "
401+errorHeader appErr =
402+ elClass "pre" "deployment__output notification notification--danger" $
638403 dynText appErr
639404
640405-- | Widget with override fields. This widget supports adding and
641406-- removing key-value pairs.
642407envVarsInput ::
643− forall l t m.
408+ forall t m.
644409 MonadWidget t m =>
645− Maybe (DefaultConfig l) ->
410+ Dynamic t [Text] ->
411+ (Text -> Maybe Text) ->
646412 -- | Initial deployment overrides.
647− Overrides l ->
648− -- | Updated deployment overrides.
649− m (Event t (Overrides l))
650−envVarsInput dCfg ovs = mdo
651− envsDyn <- foldDyn appEndo (constructWorkingOverrides dCfg ovs) $ leftmost [addEv, updEv]
652− let addEv = clickEv $> Endo (fst . insertUniqStart newWorkingOverride)
413+ WorkingOverrides ->
414+ -- | Updated deployment overrides. [(<full deconstructed key>, <value>)]
415+ m (Dynamic t [Dynamic t WorkingOverride])
416+envVarsInput (traceDyn "values" -> values) lookupOverride ovs = divClass "padded" $ mdo
417+ let initialConfig = CT.markValues ovs
653418 clickEv <-
654− buttonClassEnabled'
655− "overrides__add dash dash--add"
656− "Add an override"
657− addingIsEnabled
658− "dash--disabled"
659− updEv <-
660− switchDyn . fmap F.fold
661− <$> listWithKey
662− (uniqMap <$> envsDyn)
663− (\i x -> fmap (performUserOverrideAction (lookupDefaultConfig <$> dCfg) i) <$> envVarInput x)
664− let addingIsEnabled = all (\(WorkingOverrideKey _ x, _) -> not . T.null $ x) . elemsUniq <$> envsDyn
665− case dCfg of
666− Just _ -> pure ()
667− Nothing -> loadingCommonWidget
668− pure . updated $ destructWorkingOverrides <$> envsDyn
669−
670−popUpSection :: DomBuilder t m => Text -> m a -> m a
671−popUpSection n m = elClass "section" "deployment__section" $ do
672− elClass "h3" "deployment__sub-heading" $ text n
673− elClass "div" "deployment__widget" $
674− elClass "div" "overrides" m
675−
676−-- | Widget for entering a key-value pair. The updated overrides list is
677−-- written to the 'EventWriter'.
419+ dashButton
420+ DashButtonConfig
421+ { buttonText = "Add an override"
422+ , buttonEnabled = addingIsEnabled
423+ , buttonType = Just AddDashButtonType
424+ , buttonStyle = OverridesDashButtonStyle
425+ }
426+ let emptyItem = ("", CustomConfigValue $ Left $ CustomKey "")
427+ addedItemsCount <- foldDyn (const (fst . insertUniqStart ())) emptyUniqKeyMap clickEv
428+ addedOvsListDyn <- (fmap . fmap) (fmap snd . M.toList)
429+ <$> list (uniqMap <$> addedItemsCount)
430+ $ \_ ->
431+ envVarInput lookupOverride values emptyItem
432+ treeResCfg <-
433+ showOverrideTree
434+ ((== Just True) . fmap (T.null . T.strip) . getConfigValueText . snd)
435+ (isn't (_Ctor' @"DefaultConfigValue") . snd)
436+ (\_ v -> v)
437+ (\a _ _ -> envVarInput lookupOverride values (trace "a" a))
438+ initialConfig
439+ let resCfg :: Dynamic t [Dynamic t (Text, ConfigValue Text)]
440+ resCfg = (<> treeResCfg) <$> addedOvsListDyn
441+ addingIsEnabled = join $ fmap and . sequenceA . (fmap . fmap) (not . T.null . fst) <$> resCfg
442+ pure resCfg
443+
444+-- | Widget for entering a key-value pair.
678445envVarInput ::
679446 (MonadWidget t m) =>
447+ (Text -> Maybe Text) ->
448+ -- | The key values to suggest to the user
449+ Dynamic t [Text] ->
680450 -- | Current variable key and value.
681− Dynamic t WorkingOverride ->
682− m (Event t UserOverrideAction)
683−envVarInput val = do
684− let v =
685− val <&> snd <&> \case
686− WorkingCustomValue x -> x
687− WorkingDefaultValue x -> x
688− WorkingDeletedValue (Just x) -> x
689− WorkingDeletedValue Nothing -> "<loading deleted>"
690− k = val <&> \(WorkingOverrideKey _ x, _) -> x
691− disabledKey = val <&> \(WorkingOverrideKey t _, _) -> t == DefaultWorkingOverrideKey
692−
693− divClass "overrides__item" $ do
694− (keyTextDyn, _) <-
695− octopodTextInput' disabledKey "overrides__key" "key" k never
696− (valTextDyn, _) <-
697− octopodTextInput' (pure False) "overrides__value" "value" v never
698− closeEv <- buttonClass "overrides__delete spot spot--cancel" "Delete"
699− pure $
700− leftmost
701− [ UpdateKey <$> updated keyTextDyn
702− , UpdateValue <$> updated valTextDyn
703− , closeEv $> DeleteOverride
704− ]
705−
706−data UserOverrideAction = UpdateKey !Text | UpdateValue !Text | DeleteOverride
707−
708−performUserOverrideAction ::
709− Maybe (Text -> Maybe Text) ->
710− Int ->
711− UserOverrideAction ->
712− Endo WorkingOverrides
713−performUserOverrideAction f i (UpdateValue v) = Endo $
714− updateUniq i $ \(k@(WorkingOverrideKey _ kt), _) ->
715− ( k
716− , case f >>= ($ kt) of
717− Just v' | v == v' -> WorkingDefaultValue v
718− _ -> WorkingCustomValue v
719− )
720−performUserOverrideAction f i (UpdateKey k) = Endo $ updateUniq i $ \(_, v) -> (WorkingOverrideKey t k, v)
721− where
722− t = case f >>= ($ k) of
723− Nothing -> CustomWorkingOverrideKey
724− Just _ -> DefaultWorkingOverrideKey
725−performUserOverrideAction f i DeleteOverride = Endo $ \m ->
726− case f of
727− Nothing -> updateUniq i (\(k, _) -> (k, WorkingDeletedValue Nothing)) m
728− Just f' -> case lookupUniq i m of
729− Nothing -> m
730− Just (WorkingOverrideKey _ k, _) -> case f' k of
731− Nothing -> deleteUniq i m
732− Just v -> updateUniq i (const (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDeletedValue (Just v))) m
451+ WorkingOverride ->
452+ m (Dynamic t WorkingOverride)
453+envVarInput f keys val = mdo
454+ value' <- holdDyn val (leftmost [buttonValue, updated inputValue])
455+ (keyTextDyn, valTextDyn, buttonPressEv) <- configField keys value'
456+ let inputValue = do
457+ v <- valTextDyn
458+ k <- keyTextDyn
459+ pure $
460+ ( k
461+ , case f k of
462+ Nothing -> CustomConfigValue $ Left $ CustomKey v
463+ Just x | x == v -> DefaultConfigValue v
464+ Just _ -> CustomConfigValue $ Right $ CustomValue v
465+ )
466+ buttonValue = flip push buttonPressEv $ \x ->
467+ Just <$> do
468+ k <- sample . current $ keyTextDyn
469+ (k,) <$> case x of
470+ ConfigFieldButtonClear -> do
471+ v <- case f k of
472+ Nothing -> sample . current $ valTextDyn
473+ Just v -> pure v
474+ pure $ CustomConfigValue $ Right $ DeletedValue $ Just v
475+ ConfigFieldButtonRecover -> do
476+ case f k of
477+ Nothing -> do
478+ -- the value is not in the default config
479+ v <- sample . current $ valTextDyn
480+ pure $ CustomConfigValue $ Left $ CustomKey v
481+ Just v -> do
482+ -- the value is in the default config
483+ pure $ DefaultConfigValue v
484+ pure $ value'
733485
734486holdDynMaybe :: (Reflex t, MonadHold t m) => Event t a -> m (Dynamic t (Maybe a))
735487holdDynMaybe ev = holdDyn Nothing $ fmapCheap Just ev
@@ -739,8 +491,32 @@ immediateNothing ev = do
739491 pb <- getPostBuild
740492 pure $ leftmost [pb $> Nothing, fmapCheap Just ev]
741493
742−attachDyn :: (Reflex t, MonadHold t m) => Behavior t a -> Dynamic t b -> m (Dynamic t (a, b))
743−attachDyn b d = do
494+holdClearingWith :: (Reflex t, MonadHold t m) => Event t a -> Event t () -> m (Dynamic t (Maybe a))
495+holdClearingWith aEv clear =
496+ holdDyn Nothing $ leftmost [fmapCheap Just aEv, fmapCheap (const Nothing) clear]
497+
498+unitEv :: Reflex t => Dynamic t a -> Event t ()
499+unitEv = fmapCheap (const ()) . updated
500+
501+(<&&>) :: (Functor f1, Functor f2) => f1 (f2 a) -> (a -> b) -> f1 (f2 b)
502+x <&&> f = (fmap . fmap) f x
503+
504+debounceDyn ::
505+ ( PerformEvent t m
506+ , TriggerEvent t m
507+ , MonadIO (Performable m)
508+ , MonadHold t m
509+ , MonadFix m
510+ ) =>
511+ NominalDiffTime ->
512+ Dynamic t a ->
513+ m (Dynamic t a)
514+debounceDyn t d = do
744515 currD <- sample . current $ d
745− currB <- sample b
746− holdDyn (currB, currD) (attach b $ updated d)
516+ ev <- debounce t (updated d)
517+ holdDyn currD ev
518+
519+catchReturns :: (DomBuilder t m, MonadFix m) => (Event t () -> m a) -> m a
520+catchReturns f = mdo
521+ (divEl, a) <- el' "div" $ f $ ($> ()) . ffilter (== 13) $ domEvent Keypress divEl
522+ pure a
octopod-frontend/src/Main.hs+3−3
@@ -18,11 +18,10 @@ import qualified Data.Text as T
1818import Frontend.API
1919import Frontend.GHCJS
2020import Frontend.Route
21−import Frontend.Utils (errorCommonWidget, loadingCommonWidget)
21+import Frontend.UIKit
2222import Page.Deployment
2323import Page.Deployments
2424
25−
2625main :: IO ()
2726main = mdo
2827 mainWidgetWithHead'
@@ -93,7 +92,8 @@ headWidget ::
9392 Event t ProjectName ->
9493 m ()
9594headWidget projectNameEv = do
96− elAttr "meta" ("charset" =: "urf8 ") blank
95+ elAttr "meta" ("charset" =: "UTF-8") blank
96+ elAttr "meta" ("name" =: "theme-color" <> "content" =: "#3671E3") blank
9797 elAttr
9898 "meta"
9999 ( "http-equiv" =: "x-ua-compatible"
octopod-frontend/src/Page/ClassicPopup.hs+16−6
@@ -12,7 +12,9 @@ where
1212
1313import Reflex.Dom
1414
15−import Frontend.Utils
15+import Data.Functor
16+import Data.Generics.Labels ()
17+import Frontend.UIKit
1618
1719-- | The root function of a popup.
1820classicPopup ::
@@ -51,8 +53,8 @@ popupWidget m =
5153 divClass "classic-popup__viewport" $
5254 divClass "classic-popup__slot" $ do
5355 (okEv, cancelEv) <- m
54− closeEv <- buttonClass "classic-popup__close" "Close"
55− pure $ (okEv, leftmost [cancelEv, closeEv])
56+ closeEv <- closeClassicPopupButton
57+ pure $ (okEv, leftmost [cancelEv, closeEv $> ()])
5658
5759-- | Popup that requires confirmation of deployment deletion.
5860confirmArchivePopup ::
@@ -65,8 +67,16 @@ confirmArchivePopup showEv txt = do
6567 divClass "dialog dialog--archive" $ do
6668 divClass "dialog__content" txt
6769 divClass "dialog__footer" $ do
68− okEv <- buttonClass "dialog__action button" "Archive"
70+ okEv <-
71+ largeButton $
72+ def
73+ & #buttonStyle .~~ DialogActionLargeButtonStyle
74+ & #buttonText .~~ "Archive"
6975 cancelEv <-
70− buttonClass "dialog__action button--secondary button" "Cancel"
71− pure (okEv, cancelEv)
76+ largeButton $
77+ def
78+ & #buttonStyle .~~ DialogActionLargeButtonStyle
79+ & #buttonPriority .~~ SecondaryLargeButton
80+ & #buttonText .~~ "Cancel"
81+ pure (okEv $> (), cancelEv $> ())
7282 classicPopup showEv body
octopod-frontend/src/Page/Deployment.hs+91−100
@@ -5,29 +5,30 @@
55--This module contains the definition of a deployment page.
66module Page.Deployment (deploymentPage) where
77
8−import Control.Lens
9−import Control.Monad
10−import Data.Coerce
11−import Data.Generics.Product (field)
12−import Data.Text as T (Text, pack)
13−import Obelisk.Route.Frontend
14−import Reflex.Dom as R
15−import Servant.Reflex
16−
178import Common.Types as CT
189import Common.Utils
10+import Control.Lens
11+import Control.Monad
1912import Control.Monad.Reader
2013import Data.Align
14+import Data.Coerce
15+import Data.Functor
2116import Data.Generics.Labels ()
22−import qualified Data.Map.Ordered.Strict as OM
17+import Data.Generics.Product (field)
18+import Data.Text as T (Text, pack)
2319import Data.Time
2420import Frontend.API
2521import Frontend.GHCJS
2622import Frontend.Route
23+import Frontend.UIKit
2724import Frontend.Utils
25+import Obelisk.Route.Frontend
2826import Page.ClassicPopup
2927import Page.Elements.Links
3028import Page.Popup.EditDeployment
29+import Reflex.Dom as R
30+import Reflex.Network
31+import Servant.Reflex
3132import Servant.Reflex.Extra
3233
3334-- | The root widget of a deployment page. It requests the deployment data.
@@ -116,29 +117,33 @@ deploymentHead dfiDyn sentEv =
116117 let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
117118 btnEnabledDyn <- holdDyn btnState $ leftmost [False <$ btnEv, sentEv]
118119 btnEv <-
119− aButtonClassEnabled
120− "page__action button button--secondary button--restore \
121− \classic-popup-handler"
122− "Recover from archive"
123− btnEnabledDyn
124− void $ restoreEndpoint (Right . coerce <$> dname) btnEv
120+ largeButton $
121+ def
122+ & #buttonType .~~ pure (Just RestoreLargeButtonType)
123+ & #buttonPriority .~~ SecondaryLargeButton
124+ & #buttonStyle .~~ PageActionLargeButtonStyle
125+ & #buttonText .~~ "Recover from archive"
126+ & #buttonEnabled .~~ btnEnabledDyn
127+ void $ restoreEndpoint (Right . coerce <$> dname) $ btnEv $> ()
125128 pure (never, never)
126129 else mdo
127130 let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
128131 btnEnabledDyn <- holdDyn btnState $ not <$> sentEv
129132 editEv <-
130− buttonClassEnabled'
131− "page__action button button--edit popup-handler"
132− "Edit deployment"
133− btnEnabledDyn
134− "button--disabled"
133+ largeButton $
134+ def
135+ & #buttonType .~~ pure (Just EditLargeButtonType)
136+ & #buttonStyle .~~ PageActionLargeButtonStyle
137+ & #buttonText .~~ "Edit deployment"
138+ & #buttonEnabled .~~ btnEnabledDyn
135139 archEv <-
136− buttonClassEnabled'
137− "page__action button button--secondary button--archive \
138− \classic-popup-handler"
139− "Move to archive"
140− btnEnabledDyn
141− "button--disabled"
140+ largeButton $
141+ def
142+ & #buttonType .~~ pure (Just ArchiveLargeButtonType)
143+ & #buttonPriority .~~ SecondaryLargeButton
144+ & #buttonStyle .~~ PageActionLargeButtonStyle
145+ & #buttonText .~~ "Move to archive"
146+ & #buttonEnabled .~~ btnEnabledDyn
142147 pure (R.tag (current dfiDyn) editEv, archEv)
143148 url' <- kubeDashboardUrl dfiDyn
144149 void . dyn $
@@ -147,12 +152,16 @@ deploymentHead dfiDyn sentEv =
147152 blank
148153 ( \url ->
149154 void $
150− aButtonDynClass'
151− "page__action button button--secondary button--logs"
152− "Details"
153− (pure $ "href" =: url <> "target" =: "_blank")
155+ largeButton $
156+ def
157+ { buttonText = "Details"
158+ , buttonType = pure $ Just LogsLargeButtonType
159+ , buttonPriority = SecondaryLargeButton
160+ , buttonStyle = PageActionLargeButtonStyle
161+ , buttonBaseTag = ATag $ pure url
162+ }
154163 )
155− delEv <- confirmArchivePopup archEv $ do
164+ delEv <- confirmArchivePopup (archEv $> ()) $ do
156165 text "Are you sure you want to archive the"
157166 el "br" blank
158167 dynText dname
@@ -178,72 +187,56 @@ deploymentBody ::
178187 -- | Deployment data.
179188 Dynamic t DeploymentFullInfo ->
180189 m ()
181−deploymentBody updEv dfiDyn = deploymentBodyWrapper $ do
182− let nameDyn = dfiDyn <^.> dfiName
183− cfg = dfiDyn <&> getDeploymentConfig
184− divClass "deployment__summary" $ do
185− divClass "deployment__stat" $ do
186− elClass "b" "deployment__param" $ text "Status"
187− divClass "deployment__value" $ do
188− statusWidget $ dfiDyn <^.> field @"status"
189− divClass "deployment__stat" $ do
190− elClass "b" "deployment__param" $ text "Created"
191− divClass "deployment__value" $ do
192− let createdAtDyn = dfiDyn <^.> field @"createdAt"
193− dynText $ formatPosixToDate <$> createdAtDyn
194− divClass "deployment__stat" $ do
195− elClass "b" "deployment__param" $ text "Changed"
196− divClass "deployment__value" $ do
197− let createdAtDyn = dfiDyn <^.> field @"updatedAt"
198− dynText $ formatPosixToDate <$> createdAtDyn
199− elClass "section" "deployment__section" $ do
200− let tagDyn = dfiDyn <^.> field @"deployment" . field @"tag" . coerced
201− elClass "h3" "deployment__sub-heading" $ text "Tag"
202− divClass "deployment__widget" $ dynText tagDyn
203− elClass "section" "deployment__section" $ do
204− let urlsDyn = dfiDyn <^.> field @"metadata" . to unDeploymentMetadata
205− elClass "h3" "deployment__sub-heading" $ text "Links"
206− divClass "deployment__widget" $
207− divClass "listing" $
208− void $ simpleList urlsDyn renderMetadataLink
209− elClass "section" "deployment__section" $ do
210− let envsDyn = cfg <^.> #appConfig
211− allEnvsWidget "App overrides" envsDyn
212− elClass "section" "deployment__section" $ do
213− let envsDyn = cfg <^.> #depConfig
214− allEnvsWidget "Deployment overrides" envsDyn
215− elClass "section" "deployment__section" $ do
216− elClass "h3" "deployment__sub-heading" $ text "Actions"
217− divClass "deployment__widget" $
190+deploymentBody updEv dfiDyn = deploymentBodyWrapper $
191+ wrapRequestErrors $ \hReq -> do
192+ let nameDyn = dfiDyn <^.> dfiName
193+ depDyn = dfiDyn <^.> #deployment
194+ (defDepCfgEv, defAppCfgEv, _) <- deploymentConfigProgressiveComponents hReq (depDyn <^.> #deploymentOverrides)
195+ divClass "deployment__summary" $ do
196+ divClass "deployment__stat" $ do
197+ elClass "b" "deployment__param" $ text "Status"
198+ divClass "deployment__value" $ do
199+ statusWidget $ dfiDyn <^.> field @"status"
200+ divClass "deployment__stat" $ do
201+ elClass "b" "deployment__param" $ text "Created"
202+ divClass "deployment__value" $ do
203+ let createdAtDyn = dfiDyn <^.> field @"createdAt"
204+ dynText $ formatPosixToDate <$> createdAtDyn
205+ divClass "deployment__stat" $ do
206+ elClass "b" "deployment__param" $ text "Changed"
207+ divClass "deployment__value" $ do
208+ let createdAtDyn = dfiDyn <^.> field @"updatedAt"
209+ dynText $ formatPosixToDate <$> createdAtDyn
210+ deploymentSection "Links" $ do
211+ let urlsDyn = dfiDyn <^.> field @"metadata" . to unDeploymentMetadata
212+ divClass "deployment__widget" $
213+ divClass "listing" $
214+ void $ simpleList urlsDyn renderMetadataLink
215+ deploymentSection "Deployment configuration" $ showVars defDepCfgEv $ depDyn <^.> #deploymentOverrides
216+ deploymentSection "App configuration" $ showVars defAppCfgEv $ depDyn <^.> #appOverrides
217+ deploymentSection "Actions" $
218218 divClass "table table--actions" $
219219 actionsTable updEv nameDyn
220220
221−-- | Widget that shows overrides list. It does not depend on their type.
222−allEnvsWidget ::
221+showVars ::
223222 MonadWidget t m =>
224− -- | Widget header.
225− Text ->
226− -- | Overrides list.
227− Dynamic t (Config l) ->
223+ Event t (DefaultConfig l) ->
224+ Dynamic t (Overrides l) ->
228225 m ()
229−allEnvsWidget headerText envsDyn = do
230− elClass "h3" "deployment__sub-heading" $ text headerText
231− divClass "deployment__widget" $
232− divClass "listing listing--for-text listing--larger" $
233− void $
234− simpleList (OM.assocs . unConfig <$> envsDyn) $ \envDyn -> do
235− let varDyn = fst <$> envDyn
236− valDyn = snd <$> envDyn
237− divClass "listing__item" $ do
238− el "b" $ do
239− dynText varDyn
240− text ": "
241− dynText valDyn
242−-- ^ Widget with a table of actions that can be performed on a deployment.
226+showVars defCfgEv ovsDyn = do
227+ workingOvsEv <- constructWorkingOverridesEv defCfgEv ovsDyn
228+ _ <-
229+ divClass "deployment__widget" $
230+ networkHold nonEditableLoading $
231+ workingOvsEv <&> \x -> do
232+ showNonEditableWorkingOverrideTree (pure x)
233+ pure ()
234+ pure ()
235+
236+-- | Widget with a table of actions that can be performed on a deployment.
243237-- It requests deployment data.
244238-- If a request fails it shows an error message,
245239-- otherwise it calls 'actionsTableData', passing the received data.
246−
247240actionsTable ::
248241 MonadWidget t m =>
249242 -- | Event notifying about the need to update data.
@@ -269,9 +262,8 @@ actionsTableHead =
269262 el "thead" $
270263 el "tr" $ do
271264 el "th" $ text "Action type"
272− el "th" $ text "Image tag"
273− el "th" $ text "App overrides"
274− el "th" $ text "Deployment overrides"
265+ el "th" $ text "Deployment configuration"
266+ el "th" $ text "App configuration"
275267 el "th" $ text "Exit code"
276268 el "th" $ text "Created"
277269 el "th" $ text "Deployment duration"
@@ -307,16 +299,16 @@ actionsTableData ::
307299 m ()
308300actionsTableData updEv nameDyn initLogs = do
309301 respEv <- infoEndpoint (Right <$> nameDyn) updEv
310− let okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
302+ let okEv = (>>= logs) <$> fmapMaybe reqSuccess respEv
311303 logsDyn <- holdDyn initLogs okEv
312304 el "tbody" $
313305 void $
314306 simpleList logsDyn $ \logDyn -> do
315− dyn_ $ actinRow <$> logDyn
307+ dyn_ $ actionRow <$> logDyn
316308
317309-- | Data row of the actions table.
318−actinRow :: MonadWidget t m => DeploymentLog -> m ()
319−actinRow DeploymentLog {..} = do
310+actionRow :: MonadWidget t m => DeploymentLog -> m ()
311+actionRow DeploymentLog {..} = do
320312 el "tr" $ do
321313 el "td" $ do
322314 text $ actionToText action
@@ -324,9 +316,8 @@ actinRow DeploymentLog {..} = do
324316 "status "
325317 <> if exitCode == 0 then "status--success" else "status--failure"
326318 divClass statusClass blank
327− el "td" $ text $ coerce deploymentTag
328− el "td" $ overridesWidget $ deploymentAppOverrides
329− el "td" $ overridesWidget $ deploymentDepOverrides
319+ el "td" $ overridesWidget deploymentDepOverrides
320+ el "td" $ overridesWidget deploymentAppOverrides
330321 el "td" $ text $ showT $ exitCode
331322 el "td" $ text $ formatPosixToDateTime createdAt
332323 el "td" $ text $ formatDuration duration
octopod-frontend/src/Page/Deployments.hs+103−166
@@ -24,7 +24,7 @@ import Common.Utils
2424import Control.Applicative
2525import Control.Monad.Reader
2626import Data.Align
27−import Data.Char
27+import Data.Foldable
2828import Data.Functor
2929import qualified Data.Semigroup as S
3030import qualified Data.Text as T
@@ -33,11 +33,14 @@ import Data.These
3333import Frontend.API
3434import Frontend.GHCJS
3535import Frontend.Route
36+import Frontend.UIKit
3637import Frontend.Utils
3738import Page.ClassicPopup
3839import Page.Elements.Links
3940import Page.Popup.EditDeployment
4041import Page.Popup.NewDeployment
42+import Reflex.Dom.AsyncEvent
43+import Reflex.Dom.Renderable
4144import Reflex.MultiEventWriter.Class
4245import Servant.Reflex.Extra
4346
@@ -86,10 +89,13 @@ deploymentsWidget updAllEv dfis = do
8689 <$ errUpdEv
8790 , DPMClear <$ okUpdEv
8891 ]
89− (showNewDeploymentEv', termDyn) <- deploymentsHeadWidget True okUpdEv
92+ (showNewDeploymentEv', termDyn') <- deploymentsHeadWidget True okUpdEv
93+ termDyn <- debounceDyn 0.3 termDyn'
9094 (okUpdEv, errUpdEv, editEv) <- deploymentsListWidget updAllEv termDyn dfis
91− pure (showNewDeploymentEv', editEv)
92− void $ newDeploymentPopup showNewDeploymentEv never
95+ pure (showNewDeploymentEv', deSearch <$> editEv)
96+ newDepEv <- newDeploymentPopup showNewDeploymentEv never
97+ setRoute $ newDepEv <&> \newDep -> DashboardRoute :/ Just (newDep ^. #name)
98+
9399 void $ editDeploymentPopup editEv never
94100
95101-- | Div wrappers.
@@ -176,58 +182,25 @@ deploymentsListWidget ::
176182 Dynamic t Text ->
177183 -- | Initial deployment data
178184 [DeploymentFullInfo] ->
179− m (Event t (), Event t (), Event t DeploymentFullInfo)
185+ m (Event t (), Event t (), Event t SearchedDeploymentInfo)
180186deploymentsListWidget updAllEv termDyn ds = dataWidgetWrapper $ mdo
181187 retryEv <- delay 10 errUpdEv
182188 updRespEv <- listEndpoint $ leftmost [updAllEv, () <$ retryEv]
183189 let okUpdEv = fmapMaybe reqSuccess updRespEv
184190 errUpdEv = fmapMaybe reqErrorBody updRespEv
185191 dsDyn <- holdDyn ds okUpdEv
186− let isArchived = isDeploymentArchived . view #deployment
187− filteredDyn = ffor2 termDyn dsDyn $ \term ds' ->
188− mapMaybe (searchDeployments . T.filter (not . isSpace) $ term) ds'
189− (archivedDsDyn, activeDsDyn) =
190− splitDynPure $
191− L.partition isArchived
192− <$> filteredDyn
193− searchSorting = termDyn $> Just (SortDesc (view #score))
194− clickedEv <- elementClick
195− editEv <- activeDeploymentsWidget searchSorting clickedEv activeDsDyn
196− archivedDeploymentsWidget searchSorting clickedEv archivedDsDyn
192+ let searchInput = updated $ ffor2 termDyn dsDyn (,)
193+ searchedEv <- asyncEventLast searchInput $ \(term, ds') ->
194+ searchMany (T.unpack <$> T.words term) ds'
195+ searchedDyn <- holdDyn (wrapResult <$> ds) searchedEv
196+ let (archivedDsDyn, activeDsDyn) =
197+ splitDynPure $ L.partition isDeploymentArchived <$> searchedDyn
198+ searchSorting = termDyn $> Nothing
199+ editEv <- activeDeploymentsWidget searchSorting activeDsDyn
200+ archivedDeploymentsWidget searchSorting archivedDsDyn
197201 pure (() <$ okUpdEv, () <$ errUpdEv, editEv)
198202
199−data SearchedDeploymentFullInfo = SearchedDeploymentFullInfo
200− { deployment :: DeploymentFullInfo
201− , searchResult :: DeploymentSearchResult
202− , score :: Int
203− }
204− deriving stock (Generic, Show, Eq)
205−
206−-- | Compares deployment fields (name and tag) with a search term.
207−searchDeployments ::
208− -- | Search term.
209− Text ->
210− -- | Deployment data.
211− DeploymentFullInfo ->
212− Maybe SearchedDeploymentFullInfo
213−searchDeployments term d = case (tagResult, nameResult) of
214− (Nothing, Nothing) -> Nothing
215− (Just (res, score), Nothing) ->
216− Just $ SearchedDeploymentFullInfo d (DeploymentTagResult res) score
217− (Nothing, Just (res, score)) ->
218− Just $ SearchedDeploymentFullInfo d (DeploymentNameResult res) score
219− (Just (tRes, tScore), Just (nRes, nScore)) ->
220− if tScore > nScore
221− then Just $ SearchedDeploymentFullInfo d (DeploymentTagResult tRes) tScore
222− else Just $ SearchedDeploymentFullInfo d (DeploymentNameResult nRes) nScore
223− where
224− tagResult = fuzzySearch term . unDeploymentTag $ d ^. field @"deployment" . field @"tag"
225− nameResult = fuzzySearch term . unDeploymentName $ d ^. dfiName
226−
227−data DeploymentSearchResult
228− = DeploymentNameResult [FuzzySearchStringChunk Text]
229− | DeploymentTagResult [FuzzySearchStringChunk Text]
230− deriving stock (Show, Eq)
203+type SearchedDeploymentInfo = DeploymentFullInfo' SearchResult
231204
232205-- | Table with active deployments.
233206activeDeploymentsWidget ::
@@ -235,19 +208,15 @@ activeDeploymentsWidget ::
235208 , SetRoute t (R Routes) m
236209 , MonadReader ProjectConfig m
237210 ) =>
238− Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo)) ->
239− -- | Event that carries the clicked DOM element. This event is required by
240− -- `dropdownWidget'`.
241− Event t ClickedElement ->
242− Dynamic t [SearchedDeploymentFullInfo] ->
211+ Dynamic t (Maybe (SortDir DeploymentFullInfo)) ->
212+ Dynamic t [SearchedDeploymentInfo] ->
243213 -- | Returns an event carrying editable deployment
244214 -- to \"edit deployment\" sidebar.
245− m (Event t DeploymentFullInfo)
246−activeDeploymentsWidget searchSorting clickedEv dsDyn =
215+ m (Event t SearchedDeploymentInfo)
216+activeDeploymentsWidget searchSorting dsDyn =
247217 divClass "data__primary" $
248218 tableWrapper (updated searchSorting $> SortingChanged) $ \sortDyn -> do
249− let colSortDyn = fmap (contramap (view #deployment)) <$> sortDyn
250− sorting <- holdDyn Nothing (mergeWith (<|>) [updated colSortDyn, updated searchSorting])
219+ sorting <- holdDyn Nothing (mergeWith (<|>) [updated sortDyn, updated searchSorting])
251220 let emptyDyn' = L.null <$> dsDyn
252221 dsSortedDyn = zipDynWith sortDeployments dsDyn sorting
253222 emptyDyn <- holdUniqDyn emptyDyn'
@@ -255,10 +224,10 @@ activeDeploymentsWidget searchSorting clickedEv dsDyn =
255224 dyn $
256225 emptyDyn <&> \case
257226 False -> do
258− editEvs <- simpleList dsSortedDyn (activeDeploymentWidget clickedEv)
227+ editEvs <- simpleList dsSortedDyn activeDeploymentWidget
259228 pure $ switchDyn $ leftmost <$> editEvs
260229 True -> do
261− emptyTableBody $ noDeploymentsWidget
230+ emptyTableBody noDeploymentsWidget
262231 pure never
263232 switchHold never editEvEv
264233
@@ -275,65 +244,64 @@ activeDeploymentWidget ::
275244 , SetRoute t (R Routes) m
276245 , MonadReader ProjectConfig m
277246 ) =>
278− -- | Event that carries the clicked DOM element. This event is required by
279− -- `dropdownWidget'`.
280− Event t ClickedElement ->
281− Dynamic t SearchedDeploymentFullInfo ->
247+ Dynamic t SearchedDeploymentInfo ->
282248 -- | Returns event carrying editable deployment
283249 -- that is required by \"edit deployment\" sidebar.
284− m (Event t DeploymentFullInfo)
285−activeDeploymentWidget clickedEv dDyn' = do
250+ m (Event t SearchedDeploymentInfo)
251+activeDeploymentWidget dDyn' = do
286252 dDyn <- holdUniqDyn dDyn'
287253 editEvEv <- dyn $
288− ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = d@DeploymentFullInfo {..}} -> do
289− let (name, tag') = displaySearchHighlighting s
290− dName = deployment ^. #name
254+ ffor dDyn $ \d@DeploymentFullInfo {..} -> do
255+ let desearchedDeployment = deSearch d
256+ dName = desearchedDeployment ^. #deployment . #name
291257 (linkEl, dropdownEv) <- el' "tr" $ do
292258 el "td" $ do
293− name
259+ rndr . unDeploymentName $ d ^. #deployment . #name
294260 statusWidget $ constDyn status
295261 el "td" $
296262 divClass "listing" $
297− forM_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
298− el "td" tag'
263+ for_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
299264 el "td" $
300− overridesWidget $ deployment ^. field @"appOverrides" . coerced
265+ overridesWidget (deployment ^. field @"deploymentOverrides" . coerced)
301266 el "td" $
302− overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
267+ overridesWidget (deployment ^. field @"appOverrides" . coerced)
303268 el "td" $
304269 text $ formatPosixToDate createdAt
305270 el "td" $
306271 text $ formatPosixToDate updatedAt
307272 el "td" $ do
308273 let enabled = not . isPending . recordedStatus $ status
309− elId = "deployment_row_" <> unDeploymentName dName
310− btn =
311− elAttr
312− "button"
313− ( "class" =: "drop__handler"
314− <> "type" =: "button"
315− <> "id" =: elId
316− )
317− $ text "Actions"
318274 body = do
319− btnEditEv <- buttonClassEnabled' "action action--edit" "Edit" (pure enabled) "action--disabled"
320− btnArcEv <- buttonClassEnabled' "action action--archive" "Move to archive" (pure enabled) "action--disabled"
321− url' <- kubeDashboardUrl (view #deployment <$> dDyn)
275+ btnEditEv <-
276+ actionButton $
277+ def
278+ & #buttonText .~~ "Edit"
279+ & #buttonEnabled .~~ pure enabled
280+ & #buttonType .~~ Just EditActionButtonType
281+ btnArcEv <-
282+ actionButton $
283+ def
284+ & #buttonText .~~ "Move to archive"
285+ & #buttonEnabled .~~ pure enabled
286+ & #buttonType .~~ Just ArchiveActionButtonType
287+ url' <- kubeDashboardUrl (pure desearchedDeployment)
322288 void . dyn $
323289 url'
324290 <&> maybe
325291 blank
326292 ( \url ->
327293 void $
328− aButtonClass'
329− "action action--logs"
330− "Details"
331− (pure $ "href" =: url <> "target" =: "_blank")
294+ actionButton
295+ def
296+ { buttonText = "Details"
297+ , buttonType = Just LogsActionButtonType
298+ , buttonBaseTag = ATag $ pure url
299+ }
332300 )
333301 pure $
334302 leftmost
335303 [ArchiveDeployment <$ btnArcEv, EditDeployment <$ btnEditEv]
336− dropdownWidget' clickedEv btn body
304+ dropdownWidget body
337305 let archEv = () <$ ffilter (is (_Ctor @"ArchiveDeployment")) dropdownEv
338306 editEv = d <$ ffilter (is (_Ctor @"EditDeployment")) dropdownEv
339307 delEv <- confirmArchivePopup archEv $ do
@@ -342,49 +310,27 @@ activeDeploymentWidget clickedEv dDyn' = do
342310 text $ unDeploymentName dName <> " deployment?"
343311 void $ archiveEndpoint (constDyn . Right $ dName) delEv
344312 let route = DashboardRoute :/ Just dName
345− setRoute $ route <$ domEvent Dblclick linkEl
313+ setRoute $ route <$ domEvent Click linkEl
346314 pure editEv
347315 switchHold never editEvEv
348316
349−displaySearchHighlighting :: DomBuilder t m => SearchedDeploymentFullInfo -> (m (), m ())
350−displaySearchHighlighting
351− SearchedDeploymentFullInfo {searchResult = DeploymentNameResult res, deployment = d} =
352− ( markMatches res
353− , text . unDeploymentTag $ d ^. #deployment . #tag
354− )
355−displaySearchHighlighting
356− SearchedDeploymentFullInfo {searchResult = DeploymentTagResult res, deployment = d} =
357− ( text . unDeploymentName $ d ^. #deployment . #name
358− , markMatches res
359− )
360−
361−markMatches :: DomBuilder t m => [FuzzySearchStringChunk Text] -> m ()
362−markMatches [] = blank
363−markMatches (NotMatched x : xs) = text x >> markMatches xs
364−markMatches (Matched x : xs) =
365− elAttr "span" ("style" =: "text-decoration: underline;") (text x) >> markMatches xs
366−
367317-- | Table with archived deployments.
368318archivedDeploymentsWidget ::
369319 forall t m.
370320 ( MonadWidget t m
371321 , SetRoute t (R Routes) m
372322 ) =>
373− Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo)) ->
374− -- | Event that carries the clicked DOM element. This event is required by
375− -- `dropdownWidget'`.
376− Event t ClickedElement ->
377− Dynamic t [SearchedDeploymentFullInfo] ->
323+ Dynamic t (Maybe (SortDir DeploymentFullInfo)) ->
324+ Dynamic t [SearchedDeploymentInfo] ->
378325 m ()
379−archivedDeploymentsWidget searchSorting clickedEv dsDyn = do
326+archivedDeploymentsWidget searchSorting dsDyn = do
380327 showDyn <- toggleButton
381328 let classDyn = ffor showDyn $ \case
382329 True -> "data__archive data__archive--open"
383330 False -> "data__archive"
384331 elDynClass "div" classDyn $
385332 tableWrapper (updated searchSorting $> SortingChanged) $ \sortDyn -> do
386− let colSortDyn = fmap (contramap (view #deployment)) <$> sortDyn
387− sorting <- holdDyn Nothing (mergeWith (<|>) [updated colSortDyn, updated searchSorting])
333+ sorting <- holdDyn Nothing (mergeWith (<|>) [updated sortDyn, updated searchSorting])
388334 let emptyDyn' = L.null <$> dsDyn
389335 dsSortedDyn = zipDynWith sortDeployments dsDyn sorting
390336 emptyDyn <- holdUniqDyn emptyDyn'
@@ -394,70 +340,59 @@ archivedDeploymentsWidget searchSorting clickedEv dsDyn = do
394340 void $
395341 simpleList
396342 dsSortedDyn
397− (archivedDeploymentWidget clickedEv)
398− True -> emptyTableBody $ noDeploymentsWidget
343+ archivedDeploymentWidget
344+ True -> emptyTableBody noDeploymentsWidget
399345
400346-- | Row with archived deployment.
401347archivedDeploymentWidget ::
402348 ( MonadWidget t m
403349 , SetRoute t (R Routes) m
404350 ) =>
405− Event t ClickedElement ->
406− Dynamic t SearchedDeploymentFullInfo ->
351+ Dynamic t SearchedDeploymentInfo ->
407352 m ()
408−archivedDeploymentWidget clickedEv dDyn' = do
353+archivedDeploymentWidget dDyn' = do
409354 dDyn <- holdUniqDyn dDyn'
410355 dyn_ $
411− ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = DeploymentFullInfo {..}} -> do
412− let (name, tag') = displaySearchHighlighting s
413− dName = deployment ^. #name
356+ ffor dDyn $ \d@DeploymentFullInfo {..} -> do
357+ let desearchedDeployment = deSearch d
358+ dName = desearchedDeployment ^. #deployment . #name
414359 (linkEl, _) <- el' "tr" $ do
415360 el "td" $ do
416− name
417− divClass "status status--archived" $ text "Archived"
361+ rndr . unDeploymentName $ deployment ^. #name
362+ statusWidget (pure status)
418363 el "td" $ text "..."
419− el "td" tag'
420364 el "td" $
421− overridesWidget $ deployment ^. field @"appOverrides" . coerced
365+ overridesWidget (deployment ^. field @"deploymentOverrides" . coerced)
422366 el "td" $
423− overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
367+ overridesWidget (deployment ^. field @"appOverrides" . coerced)
424368 el "td" $
425369 text $ formatPosixToDate createdAt
426370 el "td" $
427371 text $ formatPosixToDate updatedAt
428372 el "td" $ do
429− let elId = "deployment_row_" <> (unDeploymentName dName)
430− btn =
431− elAttr
432− "button"
433− ( "class" =: "drop__handler"
434− <> "type" =: "button"
435− <> "id" =: elId
436− )
437− $ text "Actions"
438− body = do
439− btnArcEv <-
440− buttonClass
441− "action action--archive"
442− "Restore from archive"
443− pure btnArcEv
444− btnEv <- dropdownWidget' clickedEv btn body
445− void $ restoreEndpoint (constDyn $ Right $ dName) btnEv
373+ let body =
374+ actionButton
375+ def
376+ { buttonText = "Restore from archive"
377+ , buttonType = Just ArchiveActionButtonType
378+ }
379+ btnEv <- dropdownWidget body
380+ void $ restoreEndpoint (constDyn $ Right $ dName) (btnEv $> ())
446381 let route = DashboardRoute :/ Just dName
447− setRoute $ route <$ domEvent Dblclick linkEl
382+ setRoute $ route <$ domEvent Click linkEl
448383
449384-- | Sort deployments by the supplied condition.
450385sortDeployments ::
451− [SearchedDeploymentFullInfo] ->
386+ [SearchedDeploymentInfo] ->
452387 -- | Sorting condition.
453− Maybe (SortDir SearchedDeploymentFullInfo) ->
454− [SearchedDeploymentFullInfo]
455−sortDeployments items s = L.sortBy sortFunc items
388+ Maybe (SortDir DeploymentFullInfo) ->
389+ [SearchedDeploymentInfo]
390+sortDeployments items Nothing = items
391+sortDeployments items (Just (contramap deSearch -> s)) = L.sortBy sortFunc items
456392 where
457393 sortFunc a b = case s of
458− Just (SortAsc get) -> compare (get a) (get b)
459− Just (SortDesc get) -> compare (get b) (get a)
460− Nothing -> EQ
394+ (SortAsc get) -> compare (get a) (get b)
395+ (SortDesc get) -> compare (get b) (get a)
461396
462397-- | Each constructor contains a getter
463398-- that extracts the field that is used for sorting.
@@ -484,9 +419,8 @@ tableHeader = do
484419 el "tr" $ do
485420 sortHeader (view dfiName) "Name" SortAsc
486421 el "th" $ text "Links"
487− el "th" $ text "Tag"
488− el "th" $ text "App overrides"
489− el "th" $ text "Deployment overrides"
422+ el "th" $ text "Deployment configuration"
423+ el "th" $ text "App configuration"
490424 sortHeader (view $ field @"createdAt") "Created" SortDesc
491425 sortHeaderInitially (view $ field @"updatedAt") "Changed" SortDesc
492426 el "th" $
@@ -570,13 +504,16 @@ sortHeaderWithInitial f l defaultSorting initSortingM = do
570504 sortBtnEv
571505 sortingChanged
572506 tellMultiEvent . fmapMaybe id $ updated sortDyn
573− let classDyn =
574− fmap ("sort " <>) $
575− sortDyn <&> \case
576− Just (SortDesc _) -> "sort--active sort--desc"
577− Just (SortAsc _) -> "sort--active sort--asc"
578− Nothing -> ""
579− sortBtnEv <- buttonDynClass classDyn (pure l)
507+ sortBtnEv <-
508+ sortButton $
509+ def
510+ & #buttonText .~~ l
511+ & #buttonState
512+ .~~ ( sortDyn <&&> \case
513+ SortDesc _ -> SortDescButtonState
514+ SortAsc _ -> SortAscButtonState
515+ )
516+
580517 tellMultiEvent $ sortBtnEv $> SortingChanged
581518 pure ()
582519
@@ -588,7 +525,7 @@ tableWrapper ::
588525 (Dynamic t (Maybe (SortDir DeploymentFullInfo)) -> m a) ->
589526 m a
590527tableWrapper sChanged ma =
591− divClass "table table--deployments table--clickable table--double-click" $
528+ divClass "table table--deployments table--clickable" $
592529 el "table" $ mdo
593530 ((), sDyn') <- runSortableTableGroup sChanged tableHeader
594531 sDyn <- holdDyn Nothing $ Just <$> sDyn'
octopod-frontend/src/Page/Elements/Links.hs+20−11
@@ -4,8 +4,11 @@ module Page.Elements.Links
44where
55
66import Common.Types
7+import Common.Utils ((<^.>))
78import Control.Lens
9+import Control.Monad
810import qualified Data.Text as T
11+import Frontend.UIKit.Button.Common
912import Reflex.Dom
1013
1114renderMetadataLink ::
@@ -13,14 +16,20 @@ renderMetadataLink ::
1316 Dynamic t DeploymentMetadatum ->
1417 m ()
1518renderMetadataLink metadataD = do
16− let attrDyn =
17− metadataD <&> \metadata ->
18− "class" =: "listing__item external bar bar--larger"
19− <> "href" =: metadata ^. #link
20− <> "target" =: "_blank"
21− elDynAttr "a" attrDyn . dynText $
22− metadataD <&> \case
23− -- If the name is empty, then use the url
24− DeploymentMetadatum {name = name}
25− | (not . T.null . T.strip) name -> name
26− DeploymentMetadatum {link = url} -> url
19+ void $
20+ buttonEl
21+ CommonButtonConfig
22+ { constantClasses = pure $ "listing__item" <> "external" <> "bar" <> "bar--larger"
23+ , enabledClasses = mempty
24+ , disabledClasses = "button--disabled"
25+ , buttonEnabled = pure True
26+ , buttonText =
27+ TextBuilder $
28+ dynText $
29+ metadataD <&> \case
30+ -- If the name is empty, then use the url
31+ DeploymentMetadatum {name = name}
32+ | (not . T.null . T.strip) name -> name
33+ DeploymentMetadatum {link = url} -> url
34+ , buttonBaseTag = ATag $ metadataD <^.> #link
35+ }
octopod-frontend/src/Page/Popup/EditDeployment.hs+27−13
@@ -16,8 +16,10 @@ import Prelude as P
1616
1717import Common.Types
1818import Common.Utils
19+import Data.Generics.Labels ()
1920import Data.Maybe
2021import Frontend.API
22+import Frontend.UIKit
2123import Frontend.Utils
2224import Reflex.Network
2325import Servant.Reflex
@@ -31,10 +33,10 @@ editDeploymentPopup ::
3133 Event t () ->
3234 -- | Event with a flag showing the current state of the request.
3335 m (Event t Bool)
34−editDeploymentPopup showEv hideEv = sidebar showEv hideEv $ \dfi -> mdo
36+editDeploymentPopup showEv hideEv = catchReturns $ \enterEv -> sidebar showEv hideEv $ \dfi -> mdo
3537 divClass "popup__body" $ mdo
3638 let dname = dfi ^. dfiName
37− (closeEv', saveEv) <- editDeploymentPopupHeader dname enabledDyn
39+ (closeEv', (enterEv <>) -> saveEv) <- editDeploymentPopupHeader dname enabledDyn sentDyn
3840 deploymentMDyn <- editDeploymentPopupBody dfi respEv
3941 respEv <-
4042 holdDyn (pure never) >=> networkView >=> switchHold never $
@@ -64,17 +66,29 @@ editDeploymentPopupHeader ::
6466 DeploymentName ->
6567 -- | Form validation state.
6668 Dynamic t Bool ->
69+ -- | Loading
70+ Dynamic t Bool ->
6771 -- | \"Close\" event and \"Save\" click event.
6872 m (Event t (), Event t ())
69−editDeploymentPopupHeader dname validDyn =
73+editDeploymentPopupHeader dname validDyn loadingDyn =
7074 divClass "popup__head" $ do
71− closeEv <- buttonClass "popup__close" "Close popup"
75+ closeEv <- closePopupButton
7276 elClass "h2" "popup__project" $ text $ "Edit " <> coerce dname
7377 saveEv <-
7478 divClass "popup__operations" $
75− buttonClassEnabled "popup__action button button--save" "Save" validDyn
79+ largeButton $
80+ def
81+ & #buttonStyle .~~ PopupActionLargeButtonStyle
82+ & #buttonText .~~ "Save"
83+ & #buttonEnabled .~~ validDyn
84+ & #buttonType
85+ .~~ ( loadingDyn <&> \case
86+ False -> Just SaveLargeButtonType
87+ True -> Just LoadingLargeButtonType
88+ )
89+
7690 divClass "popup__menu drop drop--actions" blank
77− pure (closeEv, saveEv)
91+ pure (closeEv $> (), saveEv $> ())
7892
7993-- | The body of the sidebar containing the edit form. Contains a tag field and
8094-- an override field.
@@ -86,12 +100,12 @@ editDeploymentPopupBody ::
86100 Event t (ReqResult tag CommandResponse) ->
87101 -- | Returns deployment update and validation state.
88102 m (Dynamic t (Maybe DeploymentUpdate))
89−editDeploymentPopupBody dfi errEv = wrapRequestErrors $ \hReq -> do
103+editDeploymentPopupBody dfi errEv = do
90104 divClass "popup__content" $
91105 divClass "deployment" $
92− deploymentPopupBody
93− hReq
94− (dfi ^. #deployment . #tag . coerced . to Just)
95− (dfi ^. #deployment . #appOverrides)
96− (dfi ^. #deployment . #deploymentOverrides)
97− errEv
106+ wrapRequestErrors $ \hReq ->
107+ deploymentPopupBody
108+ hReq
109+ (dfi ^. #deployment . #appOverrides)
110+ (dfi ^. #deployment . #deploymentOverrides)
111+ errEv
octopod-frontend/src/Page/Popup/NewDeployment.hs+54−33
@@ -8,7 +8,6 @@ module Page.Popup.NewDeployment (newDeploymentPopup) where
88import Control.Lens
99import Control.Monad
1010import Data.Functor
11−import Data.Generics.Sum
1211import Data.Monoid
1312import qualified Data.Text as T
1413import Reflex.Dom as R
@@ -18,6 +17,7 @@ import Common.Types
1817import Common.Validation (isNameValid)
1918import Data.Maybe
2019import Frontend.API
20+import Frontend.UIKit
2121import Frontend.Utils
2222import Reflex.Network
2323import Servant.Reflex
@@ -29,46 +29,61 @@ newDeploymentPopup ::
2929 Event t () ->
3030 -- | \"Close\" event.
3131 Event t () ->
32− m ()
33−newDeploymentPopup showEv hideEv = void $
34− sidebar showEv hideEv $
35− const $ mdo
36− divClass "popup__body" $ mdo
37− (closeEv', saveEv) <- newDeploymentPopupHeader enabledDyn
38− deploymentMDyn <- newDeploymentPopupBody respEv
39− respEv <-
40− holdDyn (pure never) >=> networkView >=> switchHold never $
41− tagMaybe (current deploymentMDyn) saveEv <&> \dep -> do
42− pb <- getPostBuild
43− createEndpoint
44− (pure $ Right dep)
45− pb
46− sentDyn <-
47− holdDyn False $
48− leftmost
49− [ True <$ saveEv
50− , False <$ respEv
51− ]
52− let successEv =
53− fmapMaybe (preview (_Ctor @"Success") <=< commandResponse) respEv
54− closeEv = leftmost [closeEv', successEv]
55− enabledDyn = zipDynWith (&&) (not <$> sentDyn) (isJust <$> deploymentMDyn)
56− pure (never, closeEv)
32+ m (Event t Deployment)
33+newDeploymentPopup showEv hideEv =
34+ catchReturns $ \enterEv ->
35+ sidebar showEv hideEv $
36+ const $ mdo
37+ divClass "popup__body" $ mdo
38+ (closeEv', (enterEv <>) -> saveEv) <- newDeploymentPopupHeader enabledDyn sentDyn
39+ deploymentMDyn <- newDeploymentPopupBody (snd <$> respEv)
40+ respEv <-
41+ holdDyn (pure never) >=> networkView >=> switchHold never $
42+ tagMaybe (current deploymentMDyn) saveEv <&> \dep -> do
43+ pb <- getPostBuild
44+ fmap (dep,)
45+ <$> createEndpoint
46+ (pure $ Right dep)
47+ pb
48+ sentDyn <-
49+ holdDyn False $
50+ leftmost
51+ [ True <$ saveEv
52+ , False <$ respEv
53+ ]
54+ let successEv =
55+ respEv `fforMaybe` \case
56+ (dep, commandResponse -> Just Success {}) -> Just dep
57+ _ -> Nothing
58+ closeEv = leftmost [closeEv', successEv $> ()]
59+ enabledDyn = zipDynWith (&&) (not <$> sentDyn) (isJust <$> deploymentMDyn)
60+ pure (successEv, closeEv)
5761
5862-- | The header of sidebar contains control buttons: \"Save\" and \"Close\".
5963newDeploymentPopupHeader ::
6064 MonadWidget t m =>
6165 Dynamic t Bool ->
66+ -- | Loading
67+ Dynamic t Bool ->
6268 m (Event t (), Event t ())
63−newDeploymentPopupHeader enabledDyn =
69+newDeploymentPopupHeader enabledDyn loadingDyn =
6470 divClass "popup__head" $ do
65− closeEv <- buttonClass "popup__close" "Close popup"
71+ closeEv <- closePopupButton
6672 elClass "h2" "popup__project" $ text "Create new deployment"
6773 saveEv <-
6874 divClass "popup__operations" $
69− buttonClassEnabled "popup__action button button--save" "Save" enabledDyn
75+ largeButton $
76+ def
77+ & #buttonStyle .~~ PopupActionLargeButtonStyle
78+ & #buttonText .~~ "Save"
79+ & #buttonEnabled .~~ enabledDyn
80+ & #buttonType
81+ .~~ ( loadingDyn <&> \case
82+ False -> Just SaveLargeButtonType
83+ True -> Just LoadingLargeButtonType
84+ )
7085 divClass "popup__menu drop drop--actions" blank
71− pure (closeEv, saveEv)
86+ pure (closeEv $> (), saveEv $> ())
7287
7388-- | The body of the sidebar contains the creation form. It contains: a name field,
7489-- a tag field and overrides fields. The name field is validated with the regexp:
@@ -85,20 +100,26 @@ newDeploymentPopupBody errEv = divClass "popup__content" $
85100 let commandResponseEv = fmapMaybe commandResponse errEv
86101 nameErrEv = getNameError commandResponseEv nameDyn
87102 (nameDyn, validNameDyn) <- octopodTextInput "tag" "Name" "Name" Nothing nameErrEv
88− depDyn <- deploymentPopupBody hReq Nothing mempty mempty errEv
103+ depDyn <- deploymentPopupBody hReq mempty mempty errEv
89104 let dep = do
90105 depDyn >>= \case
91106 Nothing -> pure Nothing
92107 Just du' -> do
93108 name <- DeploymentName <$> nameDyn
94− pure . Just $ Deployment name (du' ^. #newTag) (du' ^. #appOverrides) (du' ^. #deploymentOverrides)
109+ pure . Just $ Deployment name (du' ^. #appOverrides) (du' ^. #deploymentOverrides)
95110 pure $ do
96111 validNameDyn >>= \case
97112 False -> pure Nothing
98113 True -> dep
99114 where
100115 getNameError crEv nameDyn =
101− let nameErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _1)) crEv
116+ let nameErrEv' =
117+ fmapMaybe
118+ ( \case
119+ ValidationError nameErr -> Just nameErr
120+ _ -> Nothing
121+ )
122+ crEv
102123 isNameValidDyn = isNameValid . DeploymentName <$> nameDyn
103124 badNameText =
104125 "Deployment name length should be longer than 2 characters \
octopod-frontend/src/Reflex/Dom/AsyncEvent.hsnew file+30
@@ -0,0 +1,30 @@
1+module Reflex.Dom.AsyncEvent
2+ ( NFData,
3+ asyncEventLast,
4+ )
5+where
6+
7+import Control.Concurrent
8+import Control.DeepSeq (NFData, force)
9+import Control.Exception (evaluate)
10+import Control.Monad.IO.Class
11+import Data.Functor
12+import Data.IORef
13+import Reflex
14+
15+asyncEventLast ::
16+ (TriggerEvent t m, PerformEvent t m, MonadIO (Performable m), MonadIO m, NFData b) =>
17+ Event t a ->
18+ (a -> b) ->
19+ m (Event t b)
20+asyncEventLast aEv f = do
21+ lastThreadIdRef <- liftIO $ newIORef Nothing
22+ performEventAsync $
23+ aEv <&> \a fire -> liftIO $ do
24+ readIORef lastThreadIdRef >>= \case
25+ Nothing -> pure ()
26+ Just lastThreadId -> killThread lastThreadId
27+ newThreadId <- forkIO $ do
28+ b <- evaluate $ force $ f a
29+ fire b
30+ writeIORef lastThreadIdRef (Just newThreadId)
octopod-frontend/src/Reflex/Dom/Renderable.hsnew file+35
@@ -0,0 +1,35 @@
1+module Reflex.Dom.Renderable
2+ ( Renderable (..),
3+ )
4+where
5+
6+import Data.Sequence (Seq (..))
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import Reflex.Dom
10+
11+class Renderable a where
12+ rndr :: DomBuilder t m => a -> m ()
13+ rndrList :: DomBuilder t m => [a] -> m ()
14+ rndrList [] = pure ()
15+ rndrList (a : aa) = rndr a >> rndr aa
16+ {-# INLINE rndrList #-}
17+
18+instance Renderable Text where
19+ rndr = text
20+ {-# INLINE rndr #-}
21+
22+instance Renderable a => Renderable [a] where
23+ rndr = rndrList
24+ {-# INLINE rndr #-}
25+
26+instance Renderable Char where
27+ rndr = text . T.singleton
28+ {-# INLINE rndr #-}
29+ rndrList = text . T.pack
30+ {-# INLINE rndrList #-}
31+
32+instance Renderable a => Renderable (Seq a) where
33+ rndr Empty = pure ()
34+ rndr (a :<| aa) = rndr a >> rndr aa
35+ {-# INLINE rndr #-}
octopod-frontend/src/Reflex/MultiEventWriter/Class.hs+10−8
@@ -1,11 +1,11 @@
11module Reflex.MultiEventWriter.Class
2− ( MultiEventWriter(..)
3− ) where
4−
5−import Control.Monad.Reader (ReaderT, lift)
6−import Data.Type.Equality
7−import Reflex.Dom
2+ ( MultiEventWriter (..),
3+ )
4+where
85
6+import Control.Monad.Reader (ReaderT, lift)
7+import Data.Type.Equality
8+import Reflex.Dom
99
1010-- | Same as 'EventWriter' but without a fundep.
1111class (Monad m, Semigroup w) => MultiEventWriter t w m | m -> t where
@@ -24,8 +24,10 @@ type MultiEventWriter' t w u m = MultiEventWriter'' t w u m (w == u)
2424class (u == w) ~ f => MultiEventWriter'' t w u m f where
2525 tellMultiEvent' :: Event t w -> EventWriterT t u m ()
2626
27−instance ((w == w) ~ 'True, Reflex t, Monad m, Semigroup w)
28− => MultiEventWriter'' t w w m 'True where
27+instance
28+ ((w == w) ~ 'True, Reflex t, Monad m, Semigroup w) =>
29+ MultiEventWriter'' t w w m 'True
30+ where
2931 tellMultiEvent' = tellEvent
3032
3133instance ((u == w) ~ 'False, MultiEventWriter t w m) => MultiEventWriter'' t w u m 'False where
octopod-frontend/src/Servant/Reflex/Extra.hs+18−14
@@ -1,27 +1,31 @@
11{-# OPTIONS_GHC -Wno-orphans #-}
22
33module Servant.Reflex.Extra
4− ( reqErrorBody
5− ) where
4+ ( reqErrorBody,
5+ )
6+where
67
7−import Data.Functor.Identity
8−import Data.Proxy
9−import Data.Text (Text)
8+import Data.Functor.Identity
9+import Data.Proxy
10+import Data.Text (Text)
1011import qualified Data.Text.Encoding as E
11−import Reflex.Dom
12−import Servant.API
13−import Servant.Reflex
12+import Reflex.Dom
13+import Servant.API
14+import Servant.Reflex
1415
15−instance (SupportsServantReflex t m)
16− => HasClient t m (NoContentVerb 'GET) tag where
16+instance
17+ (SupportsServantReflex t m) =>
18+ HasClient t m (NoContentVerb 'GET) tag
19+ where
1720 type Client t m (NoContentVerb 'GET) tag = Event t tag -> m (Event t (ReqResult tag NoContent))
1821 clientWithRouteAndResultHandler Proxy _ _ req baseurl opts wrap' trigs =
19− wrap' =<< fmap runIdentity <$> performRequestsNoBody method (constDyn $ Identity req) baseurl opts trigs
20− where method = E.decodeUtf8 $ reflectMethod (Proxy :: Proxy 'GET)
22+ wrap' =<< fmap runIdentity <$> performRequestsNoBody method (constDyn $ Identity req) baseurl opts trigs
23+ where
24+ method = E.decodeUtf8 $ reflectMethod (Proxy :: Proxy 'GET)
2125
2226reqErrorBody :: ReqResult tag a -> Maybe Text
23−reqErrorBody (ResponseFailure _ _ XhrResponse {_xhrResponse_responseText = Just b})
24− = Just b
27+reqErrorBody (ResponseFailure _ _ XhrResponse {_xhrResponse_responseText = Just b}) =
28+ Just b
2529reqErrorBody (ResponseFailure _ _ XhrResponse {_xhrResponse_response = Just (XhrResponseBody_Default t)}) =
2630 Just t
2731reqErrorBody (ResponseFailure _ _ XhrResponse {_xhrResponse_response = Just (XhrResponseBody_Text t)}) =
octopod_local_install.sh+38−8
@@ -3,6 +3,15 @@ set -e
33message () {
44 echo "==> $1"
55}
6+
7+echo
8+echo " 🔖 If you encounter any issues during the installation please open an issue:"
9+echo " https://github.com/typeable/octopod/issues/new"
10+echo
11+echo " ℹ️ Please note that you might experience performance issues if you have low resouce limits:"
12+echo " https://github.com/typeable/octopod/blob/master/docs/en/Octopod_deployment_guide.md#if-you-want-to-try-it-locally"
13+echo
14+
615for cmd in kind kubectl helm; do
716 if ! command -v $cmd &> /dev/null; then
817 message "$cmd is not installed. Please, install it!"
@@ -50,7 +59,7 @@ nodes:
5059 hostPort: 443
5160 protocol: TCP
5261EOF
53−cat <<EOF | kubectl apply -f -
62+cat <<EOF | kubectl --context=kind-octopod apply -f -
5463apiVersion: v1
5564kind: ConfigMap
5665metadata:
@@ -80,9 +89,26 @@ data:
8089 }
8190EOF
8291message "Installing ingress-nginx controller"
83−kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.45.0/deploy/static/provider/kind/deploy.yaml
92+kubectl --context=kind-octopod apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.45.0/deploy/static/provider/kind/deploy.yaml
93+has_error=0
8494while true; do
85− if [ $(kubectl -n ingress-nginx get deploy | awk -F ' ' '$1 ~ /^ingress-nginx/ {print $4}') == '1' ]; then
95+ set +e
96+ kube_out=`kubectl --context=kind-octopod -n ingress-nginx get deploy 2>&1`
97+ err_code=$?
98+ set -e
99+ if [ $err_code != 0 ] && [ $has_error == 0 ]; then
100+ echo "kubectl returned: $kube_out"
101+ echo "Do you want to continue installation? [y/n] "
102+ read confirm
103+ if [ "$confirm" != 'y' ]; then
104+ message "Deleting cluster..."
105+ kind delete cluster --name octopod
106+ exit 0;
107+ else
108+ has_error=1
109+ fi
110+ fi
111+ if [ $err_code == 0 ] && [ $( echo -n "$kube_out" | awk -F ' ' '$1 ~ /^ingress-nginx/ {print $4}') == '1' ]; then
86112 message 'ingress-controller is ready'
87113 break;
88114 else
@@ -90,12 +116,16 @@ while true; do
90116 fi
91117done
92118message "Creating nessesary namespaces"
93−kubectl create ns octopod
94−kubectl create ns octopod-deployment
119+kubectl --context=kind-octopod create ns octopod
120+kubectl --context=kind-octopod create ns octopod-deployment
95121message "Adding typeable helm repository"
96122helm repo add typeable https://typeable.github.io/octopod
97123helm repo update
98124message "Installing octopod helm chart"
99−helm -n octopod install octopod typeable/octopod --wait --timeout=5m --set octopod.baseDomain="lvh.me" --set ingress.tls.enabled=false
100−message "Octopod was successfully installed! Use instruction above to connect to it."
101−message "To uninstall octopod just run: kind delete cluster --name octopod"
125+helm --kube-context=kind-octopod -n octopod install octopod typeable/octopod --wait --timeout=5m --set octopod.baseDomain="lvh.me" --set ingress.tls.enabled=false
126+echo
127+echo " 🌟 Don't forget to star Octopod on GitHub:"
128+echo " https://github.com/typeable/octopod"
129+echo
130+echo " 🗑️ To uninstall Octopod just run:"
131+echo " kind delete cluster --name octopod"
stack.yaml+3−1
@@ -1,4 +1,4 @@
1−resolver: lts-18.2
1+resolver: lts-18.28
22
33packages:
44 - octo-cli
@@ -10,3 +10,5 @@ extra-deps:
1010 - servant-websockets-2.0.0
1111 - table-layout-0.9.1.0
1212 - data-default-instances-base-0.1.0.1
13+ - git: https://github.com/typeable/ordered-containers.git
14+ commit: 3eb05fb2f44fe482e9092aff447e9c68fbd6a7f6