git.iko.soy/typeable/octopod
on master help sign in
Removed TLS Auth in favour of headers (#54)

* Removed TLS Auth in favour of headers * chart changes to work with auth header instead of certs * chart version bump up * Warnings and formatting * Removed extra deps * Split module sources * Removed extra TLS args * Slimmed down docker images and added certs to octo cli * Simplified docker images * Increased swap * Added entrypoint to env veriables in docker images * refactor cli options + get rid of tls options Co-authored-by: Aleksei Sizov <a.sizov@typeable.io> Co-authored-by: iko

commit93097940790502f3b1770dbff83c88a2d16364f2 authoriko <ilyakooo0@gmail.com> dateMon 26 Jul 2021, 18:16 (5 years ago) parentee8b278
48 files changed+2979−2994
.github/workflows/build.yaml+1−1 .stylish-haskell.yaml−319 .vscode/settings.json+2−1 charts/helm3/octopod/Chart.yaml+2−2 charts/helm3/octopod/README.md+1−1 charts/helm3/octopod/templates/NOTES.txt+8 charts/helm3/octopod/templates/_helpers.tpl+12 charts/helm3/octopod/templates/cli-secret.yaml+11 charts/helm3/octopod/templates/deployment.yaml+11−37 charts/helm3/octopod/templates/ingress-powerapp.yaml+4−2 charts/helm3/octopod/values.yaml−1 default.nix+28−4 dev/dev_backend.sh+1 fourmolu.yaml+8 nix/default.nix+36−53 nix/sources.json+12 octo-cli/octo-cli.cabal+20−7 octo-cli/src/Main.hs+2−2 octo-cli/src/Octopod/CLI.hs+187−170 octo-cli/src/Octopod/CLI/Args.hs+158−122 octo-cli/src/Octopod/CLI/TLS.hs−39 octo-cli/src/Octopod/PowerAPI/Auth/Client.hs+28 octopod-api/octopod-api.cabal+13−2 octopod-api/src/Octopod/API/WebSocket.hs+7−3 octopod-api/src/Octopod/PowerAPI.hs+51−42 octopod-backend/app/Main.hs+2−2 octopod-backend/octopod-backend.cabal+32−103 octopod-backend/src/Octopod/PowerAPI/Auth/Server.hs+23 octopod-backend/src/Octopod/Server.hs+769−672 octopod-backend/src/Octopod/Server/Args.hs+46−58 octopod-backend/src/Octopod/Server/Logger.hs+16−12 octopod-backend/src/Orphans.hs+13−10 octopod-backend/src/TLS.hs−68 octopod-backend/src/Types.hs+49−55 octopod-backend/test/Spec.hs−2 octopod-common/octopod-common.cabal+11−1 octopod-common/src/Common/Types.hs+90−101 octopod-common/src/Common/Utils.hs+24−16 octopod-common/src/Common/Validation.hs+15−15 octopod-frontend/octopod-frontend.cabal+16−8 octopod-frontend/src/Data/Text/Search.hs+16−15 octopod-frontend/src/Frontend/API.hs+123−105 octopod-frontend/src/Frontend/GHCJS.hs+41−28 octopod-frontend/src/Frontend/Route.hs+37−37 octopod-frontend/src/Frontend/Utils.hs+397−340 octopod-frontend/src/Main.hs+207−135 octopod-frontend/src/Page/ClassicPopup.hs+52−46 octopod-frontend/src/Page/Deployments.hs+397−357
.github/workflows/build.yaml+1−1
@@ -62,7 +62,7 @@ jobs:
6262 free -h
6363
6464 sudo swapoff /mnt/swapfile
65− sudo fallocate -l 8G /mnt/swapfile
65+ sudo fallocate -l 12G /mnt/swapfile
6666 sudo mkswap /mnt/swapfile
6767 sudo swapon /mnt/swapfile
6868
.stylish-haskell.yamldeleted−319
@@ -1,319 +0,0 @@
1−# stylish-haskell configuration file
2−# ==================================
3−
4−# The stylish-haskell tool is mainly configured by specifying steps. These steps
5−# are a list, so they have an order, and one specific step may appear more than
6−# once (if needed). Each file is processed by these steps in the given order.
7−steps:
8− # Format record definitions. This is disabled by default.
9− #
10− # You can control the layout of record fields. The only rules that can't be configured
11− # are these:
12− #
13− # - "|" is always aligned with "="
14− # - "," in fields is always aligned with "{"
15− # - "}" is likewise always aligned with "{"
16− #
17− # - records:
18− # # How to format equals sign between type constructor and data constructor.
19− # # Possible values:
20− # # - "same_line" -- leave "=" AND data constructor on the same line as the type constructor.
21− # # - "indent N" -- insert a new line and N spaces from the beginning of the next line.
22− # equals: "same_line"
23−
24− # # How to format first field of each record constructor.
25− # # Possible values:
26− # # - "same_line" -- "{" and first field goes on the same line as the data constructor.
27− # # - "indent N" -- insert a new line and N spaces from the beginning of the data constructor
28− # first_field: "indent 2"
29−
30− # # How many spaces to insert between the column with "," and the beginning of the comment in the next line.
31− # field_comment: 2
32−
33− # # # How many spaces to insert before "deriving" clause. Deriving clauses are always on separate lines.
34− # deriving: 2
35−
36− # Align the right hand side of some elements. This is quite conservative
37− # and only applies to statements where each element occupies a single
38− # line. All default to true.
39− - simple_align:
40− cases: false
41− top_level_patterns: false
42− records: false
43−
44− # Import cleanup
45− - imports:
46− # There are different ways we can align names and lists.
47− #
48− # - global: Align the import names and import list throughout the entire
49− # file.
50− #
51− # - file: Like global, but don't add padding when there are no qualified
52− # imports in the file.
53− #
54− # - group: Only align the imports per group (a group is formed by adjacent
55− # import lines).
56− #
57− # - none: Do not perform any alignment.
58− #
59− # Default: global.
60− align: global
61−
62− # The following options affect only import list alignment.
63− #
64− # List align has following options:
65− #
66− # - after_alias: Import list is aligned with end of import including
67− # 'as' and 'hiding' keywords.
68− #
69− # > import qualified Data.List as List (concat, foldl, foldr, head,
70− # > init, last, length)
71− #
72− # - with_alias: Import list is aligned with start of alias or hiding.
73− #
74− # > import qualified Data.List as List (concat, foldl, foldr, head,
75− # > init, last, length)
76− #
77− # - with_module_name: Import list is aligned `list_padding` spaces after
78− # the module name.
79− #
80− # > import qualified Data.List as List (concat, foldl, foldr, head,
81− # init, last, length)
82− #
83− # This is mainly intended for use with `pad_module_names: false`.
84− #
85− # > import qualified Data.List as List (concat, foldl, foldr, head,
86− # init, last, length, scanl, scanr, take, drop,
87− # sort, nub)
88− #
89− # - new_line: Import list starts always on new line.
90− #
91− # > import qualified Data.List as List
92− # > (concat, foldl, foldr, head, init, last, length)
93− #
94− # Default: after_alias
95− list_align: after_alias
96−
97− # Right-pad the module names to align imports in a group:
98− #
99− # - true: a little more readable
100− #
101− # > import qualified Data.List as List (concat, foldl, foldr,
102− # > init, last, length)
103− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
104− # > init, last, length)
105− #
106− # - false: diff-safe
107− #
108− # > import qualified Data.List as List (concat, foldl, foldr, init,
109− # > last, length)
110− # > import qualified Data.List.Extra as List (concat, foldl, foldr,
111− # > init, last, length)
112− #
113− # Default: true
114− pad_module_names: false
115−
116− # Long list align style takes effect when import is too long. This is
117− # determined by 'columns' setting.
118− #
119− # - inline: This option will put as much specs on same line as possible.
120− #
121− # - new_line: Import list will start on new line.
122− #
123− # - new_line_multiline: Import list will start on new line when it's
124− # short enough to fit to single line. Otherwise it'll be multiline.
125− #
126− # - multiline: One line per import list entry.
127− # Type with constructor list acts like single import.
128− #
129− # > import qualified Data.Map as M
130− # > ( empty
131− # > , singleton
132− # > , ...
133− # > , delete
134− # > )
135− #
136− # Default: inline
137− long_list_align: new_line
138−
139− # Align empty list (importing instances)
140− #
141− # Empty list align has following options
142− #
143− # - inherit: inherit list_align setting
144− #
145− # - right_after: () is right after the module name:
146− #
147− # > import Vector.Instances ()
148− #
149− # Default: inherit
150− empty_list_align: right_after
151−
152− # List padding determines indentation of import list on lines after import.
153− # This option affects 'long_list_align'.
154− #
155− # - <integer>: constant value
156− #
157− # - module_name: align under start of module name.
158− # Useful for 'file' and 'group' align settings.
159− #
160− # Default: 4
161− list_padding: 2
162−
163− # Separate lists option affects formatting of import list for type
164− # or class. The only difference is single space between type and list
165− # of constructors, selectors and class functions.
166− #
167− # - true: There is single space between Foldable type and list of it's
168− # functions.
169− #
170− # > import Data.Foldable (Foldable (fold, foldl, foldMap))
171− #
172− # - false: There is no space between Foldable type and list of it's
173− # functions.
174− #
175− # > import Data.Foldable (Foldable(fold, foldl, foldMap))
176− #
177− # Default: true
178− separate_lists: false
179−
180− # Space surround option affects formatting of import lists on a single
181− # line. The only difference is single space after the initial
182− # parenthesis and a single space before the terminal parenthesis.
183− #
184− # - true: There is single space associated with the enclosing
185− # parenthesis.
186− #
187− # > import Data.Foo ( foo )
188− #
189− # - false: There is no space associated with the enclosing parenthesis
190− #
191− # > import Data.Foo (foo)
192− #
193− # Default: false
194− space_surround: false
195−
196− # Language pragmas
197− - language_pragmas:
198− # We can generate different styles of language pragma lists.
199− #
200− # - vertical: Vertical-spaced language pragmas, one per line.
201− #
202− # - compact: A more compact style.
203− #
204− # - compact_line: Similar to compact, but wrap each line with
205− # `{-#LANGUAGE #-}'.
206− #
207− # Default: vertical.
208− style: vertical
209−
210− # Align affects alignment of closing pragma brackets.
211− #
212− # - true: Brackets are aligned in same column.
213− #
214− # - false: Brackets are not aligned together. There is only one space
215− # between actual import and closing bracket.
216− #
217− # Default: true
218− align: false
219−
220− # stylish-haskell can detect redundancy of some language pragmas. If this
221− # is set to true, it will remove those redundant pragmas. Default: true.
222− remove_redundant: true
223−
224− # Language prefix to be used for pragma declaration, this allows you to
225− # use other options non case-sensitive like "language" or "Language".
226− # If a non correct String is provided, it will default to: LANGUAGE.
227− language_prefix: LANGUAGE
228−
229− # Replace tabs by spaces. This is disabled by default.
230− # - tabs:
231− # # Number of spaces to use for each tab. Default: 8, as specified by the
232− # # Haskell report.
233− # spaces: 8
234−
235− # Remove trailing whitespace
236− - trailing_whitespace: {}
237−
238− # Squash multiple spaces between the left and right hand sides of some
239− # elements into single spaces. Basically, this undoes the effect of
240− # simple_align but is a bit less conservative.
241− - squash: {}
242−
243−# A common setting is the number of columns (parts of) code will be wrapped
244−# to. Different steps take this into account.
245−#
246−# Set this to null to disable all line wrapping.
247−#
248−# Default: 80.
249−columns: 80
250−
251−# By default, line endings are converted according to the OS. You can override
252−# preferred format here.
253−#
254−# - native: Native newline format. CRLF on Windows, LF on other OSes.
255−#
256−# - lf: Convert to LF ("\n").
257−#
258−# - crlf: Convert to CRLF ("\r\n").
259−#
260−# Default: native.
261−newline: native
262−
263−# Sometimes, language extensions are specified in a cabal file or from the
264−# command line instead of using language pragmas in the file. stylish-haskell
265−# needs to be aware of these, so it can parse the file correctly.
266−#
267−# No language extensions are enabled by default.
268−language_extensions:
269− - BangPatterns
270− - ConstraintKinds
271− - CPP
272− - DataKinds
273− - DefaultSignatures
274− - DeriveFoldable
275− - DeriveFunctor
276− - DeriveGeneric
277− - DeriveTraversable
278− - DerivingVia
279− - DuplicateRecordFields
280− - ExistentialQuantification
281− - ExplicitNamespaces
282− - FlexibleInstances
283− - FunctionalDependencies
284− - GADTs
285− - GeneralizedNewtypeDeriving
286− - KindSignatures
287− - LambdaCase
288− - MultiParamTypeClasses
289− - MultiWayIf
290− - NamedFieldPuns
291− - NoMonadFailDesugaring
292− - NumDecimals
293− - OverloadedStrings
294− - PartialTypeSignatures
295− - PolyKinds
296− - QuantifiedConstraints
297− - QuasiQuotes
298− - RankNTypes
299− - RecordWildCards
300− - RecursiveDo
301− - RoleAnnotations
302− - ScopedTypeVariables
303− - StandaloneDeriving
304− - TemplateHaskell
305− - TupleSections
306− - TypeApplications
307− - TypeFamilies
308− - TypeInType
309− - TypeOperators
310− - UndecidableInstances
311− - ViewPatterns
312− - OverloadedLabels
313− - FlexibleContexts
314−
315−# Attempt to find the cabal file in ancestors of the current directory, and
316−# parse options (currently only language extensions) from that.
317−#
318−# Default: true
319−cabal: false
.vscode/settings.json+2−1
@@ -1,3 +1,4 @@
11{
2− "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix"
2+ "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix",
3+ "haskell.formattingProvider": "fourmolu"
34}
charts/helm3/octopod/Chart.yaml+2−2
@@ -2,8 +2,8 @@ apiVersion: v2
22name: octopod
33description: An opensource self-hosted solution for managing multiple deployments in a Kubernetes cluster.
44type: application
5−version: 0.3.2
6−appVersion: 1.2.1
5+version: 0.4.0
6+appVersion: 1.3
77keywords:
88 - kubernetes
99 - octopod
charts/helm3/octopod/README.md+1−1
@@ -101,7 +101,7 @@ The following tables lists the configurable parameters of the Octopod chart and
101101| nodeSelector | object | `{}` | Node selector if you want octopod to use specific nodes onn your cluster |
102102| octopod.archiveRetention | int | `1209600` | |
103103| octopod.baseDomain | string | `""` | Domain that will be used as a ase for Octopod deploymets and ingress hosts|
104−| octopod.certsConfigMapName | string | `"octopod-certs"` | Config map with self-signed certificates |
104+| octopod.cliAuthSecret | string | `nil` | Auth Header for accessing octopod using octo CLI |
105105| octopod.deploymentNamespace | string | `"octopod-deployment"` | Name of a namespace which will be used for all Octopod deployments (you need to create it yourself) |
106106| octopod.env | object | `{}` | key value map for supplying additional environment variables for octopod or your control scripts |
107107| octopod.migrations.enabled | bool | `true` | Enable or not automatic DB schema migrations |
charts/helm3/octopod/templates/NOTES.txtnew file+8
@@ -0,0 +1,8 @@
1+Thank you for installing Octopod!
2+You can access UI here: https://{{ include "uiIngressHost" . }}
3+To access octopod via octo CLI you can use this url https://{{ include "powerAppIngressHost" . }}
4+To get you CLI secret you need to execute this command:
5+ kubectl -n {{ .Release.Namespace }} get secret {{ include "octopodCliAuthSecretName" . }} -o jsonpath='{.data.cli-secret}' | base64 -d
6+{{ if and (not .Values.octopod.cliAuthSecret) .Release.IsUpgrade }}
7+{{ fail "To upgrade you need to set octopod.cliAuthSecret variable with your current secret" }}
8+{{- end }}
charts/helm3/octopod/templates/_helpers.tpl+12
@@ -123,4 +123,16 @@ octopod-powerapp.{{ .Values.octopod.baseDomain }}
123123{{- else -}}
124124octopod-app.{{ .Values.octopod.baseDomain }}
125125{{- end }}
126+{{- end }}
127+
128+{{- define "octopodCliAuthSecretName" -}}
129+{{- printf "%s-cli-auth-secret" (include "octopod.fullname" .) }}
130+{{- end }}
131+
132+{{- define "octopodCliAuthSecret" -}}
133+{{- if .Values.octopod.cliAuthSecret -}}
134+{{ .Values.octopod.cliAuthSecret }}
135+{{- else }}
136+{{- randAlphaNum 32 }}
137+{{- end }}
126138{{- end }}
charts/helm3/octopod/templates/cli-secret.yamlnew file+11
@@ -0,0 +1,11 @@
1+{{- if not .Values.vault.enabled }}
2+apiVersion: v1
3+kind: Secret
4+type: Opaque
5+metadata:
6+ name: {{ include "octopodCliAuthSecretName" . }}
7+ labels:
8+ {{- include "octopod.labels" . | nindent 4 }}
9+data:
10+ cli-secret: {{ include "octopodCliAuthSecret" . | b64enc }}
11+{{- end }}
charts/helm3/octopod/templates/deployment.yaml+11−37
@@ -97,40 +97,19 @@ spec:
9797 protocol: TCP
9898 - containerPort: 4000
9999 protocol: TCP
100− {{- if .Values.vault.enabled }}
101100 command:
102101 - "sh"
103102 - "-ec"
104103 - |
104+ {{- if .Values.vault.enabled }}
105105 for i in /vault/secrets/*;do source $i;done
106− exec /app/octopod-exe \
106+ {{- end }}
107+ exec $ENTRYPOINT \
107108 --port {{ .Values.service.ports.powerApp | quote }} \
108109 --ui-port {{ .Values.service.ports.app | quote }} \
109110 --ws-port {{ .Values.service.ports.ws | quote }} \
110111 --db host="'{{ include "postgresqlHost" . }}' port=5432 user='{{ .Values.postgresql.postgresqlUsername }}' password=$(PG_PASS)" \
111− --db-pool-size "10" \
112− --tls-cert-path /tls/server_cert.pem \
113− --tls-key-path /tls/server_key.pem \
114− --tls-store-path /tls_store \
115− {{- else }}
116− args:
117− - --port
118− - {{ .Values.service.ports.powerApp | quote }}
119− - --ui-port
120− - {{ .Values.service.ports.app | quote }}
121− - --ws-port
122− - {{ .Values.service.ports.ws | quote }}
123− - --db
124− - host='{{ include "postgresqlHost" . }}' port=5432 user='{{ .Values.postgresql.postgresqlUsername }}' password=$(PG_PASS)
125− - --db-pool-size
126− - "10"
127− - --tls-cert-path
128− - /tls/server_cert.pem
129− - --tls-key-path
130− - /tls/server_key.pem
131− - --tls-store-path
132− - /tls_store
133− {{- end }}
112+ --db-pool-size "10"
134113 envFrom:
135114 - configMapRef:
136115 name: {{ include "octopod.fullname" . }}
@@ -140,20 +119,18 @@ spec:
140119 secretKeyRef:
141120 name: {{ include "postgresqlSecretName" . }}
142121 key: postgresql-password
122+ {{- if not .Values.vault.enabled }}
123+ - name: POWER_AUTHORIZATION_HEADER
124+ valueFrom:
125+ secretKeyRef:
126+ name: {{ include "octopodCliAuthSecretName" . }}
127+ key: cli-secret
128+ {{- end }}
143129 volumeMounts:
144130 - name: home
145131 mountPath: /home/octopod
146132 - name: control-scripts
147133 mountPath: {{ include "controlScriptsPath" . }}
148− - name: certs
149− mountPath: /tls/server_cert.pem
150− subPath: server_cert.pem
151− - name: certs
152− mountPath: /tls/server_key.pem
153− subPath: server_key.pem
154− - name: certs
155− mountPath: /tls_store/server_cert.pem
156− subPath: server_cert.pem
157134 readinessProbe:
158135 httpGet:
159136 port: {{ .Values.service.ports.app }}
@@ -205,6 +182,3 @@ spec:
205182 - name: nginx-config
206183 configMap:
207184 name: {{ include "octopod.fullname" . }}-nginx-config
208− - name: certs
209− configMap:
210− name: {{ .Values.octopod.certsConfigMapName }}
charts/helm3/octopod/templates/ingress-powerapp.yaml+4−2
@@ -11,13 +11,15 @@ metadata:
1111 labels:
1212 {{- include "octopod.labels" . | nindent 4 }}
1313 annotations:
14+ {{- if .Values.ingress.tls.enabled }}
15+ kubernetes.io/tls-acme: "true"
16+ cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.clusterIssuer | quote }}
17+ {{- end }}
1418 kubernetes.io/ingress.class: {{ .Values.ingress.ingressClass | quote }}
1519 nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
1620 nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
1721 nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
1822 nginx.ingress.kubernetes.io/proxy-next-upstream: "http_502 error timeout"
19− nginx.ingress.kubernetes.io/ssl-passthrough: "true"
20− nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
2123 {{- with .Values.ingress.powerApp.annotations }}
2224 {{- toYaml . | nindent 4 }}
2325 {{- end }}
charts/helm3/octopod/values.yaml−1
@@ -76,7 +76,6 @@ tolerations: []
7676affinity: {}
7777
7878octopod:
79− certsConfigMapName: octopod-certs
8079 projectName: Octopod
8180 deploymentNamespace: octopod-deployment
8281 baseDomain: ""
default.nix+28−4
@@ -1,13 +1,37 @@
11{ sources ? import ./nix/sources.nix
2−, haskellNix ? import sources.haskellNix { }
3−, pkgs ? import haskellNix.sources.nixpkgs-2105 haskellNix.nixpkgsArgs
2+, haskellNix ? import sources.haskellNix { inherit system; }
3+, pkgs ? import haskellNix.sources.nixpkgs-2105 (haskellNix.nixpkgsArgs // { inherit system; })
4+, nix-filter ? import sources.nix-filter
5+, system ? builtins.currentSystem
46}:
57let
68 hsPkgs = pkgs.haskell-nix.cabalProject {
7− src = pkgs.haskell-nix.haskellLib.cleanGit {
9+ src = nix-filter {
10+ root = ./.;
811 name = "octopod";
9− src = ./.;
12+ include = [
13+ ./octopod-backend/octopod-backend.cabal
14+ ./octo-cli/octo-cli.cabal
15+ ./octopod-api/octopod-api.cabal
16+ ./octopod-common/octopod-common.cabal
17+ ./octopod-frontend/octopod-frontend.cabal
18+ ./cabal.project
19+ ];
1020 };
21+
22+ modules = [
23+ {
24+ dontStrip = false;
25+ dontPatchELF = false;
26+ enableDeadCodeElimination = true;
27+ packages.octopod-backend.src = ./octopod-backend;
28+ packages.octo-cli.src = ./octo-cli;
29+ packages.octopod-api.src = ./octopod-api;
30+ packages.octopod-frontend.src = ./octopod-frontend;
31+ packages.octopod-common.src = ./octopod-common;
32+ }
33+ ];
34+
1135 index-state = "2021-07-02T00:00:00Z";
1236 compiler-nix-name = "ghc8105";
1337 };
dev/dev_backend.sh+1
@@ -19,6 +19,7 @@ export ARCHIVE_CHECKING_COMMAND=$MOUNT_DIR/echo.sh
1919export TAG_CHECKING_COMMAND=$MOUNT_DIR/echo.sh
2020export INFO_COMMAND=$MOUNT_DIR/info.sh
2121export NOTIFICATION_COMMAND=$MOUNT_DIR/write.sh
22+export POWER_AUTHORIZATION_HEADER="123"
2223$1/bin/octopod-exe \
2324 --port 4443 \
2425 --ui-port 3002 \
fourmolu.yamlnew file+8
@@ -0,0 +1,8 @@
1+indentation: 2
2+comma-style: leading # for lists, tuples etc. - can also be 'trailing'
3+record-brace-space: true # rec {x = 1} vs. rec{x = 1}
4+indent-wheres: true # 'false' means save space by only half-indenting the 'where' keyword
5+diff-friendly-import-export: false # 'false' uses Ormolu-style lists
6+respectful: true # don't be too opinionated about newlines etc.
7+haddock-style: single-line # '--' vs. '{-'
8+newlines-between-decls: 1 # number of newlines between top-level declarations
nix/default.nix+36−53
@@ -1,16 +1,17 @@
11{ sources ? import ./sources.nix
22, octopod-css ? ../octopod-css
3−, migrations ? "please use '--arg migration <value>'"
3+, migrations ? ../migrations
4+, system ? builtins.currentSystem
45}:
56let
6− hsPkgs = import ./.. { };
7+ hsPkgs = import ./.. { inherit system; };
78
89 pkgs = hsPkgs.pkgs;
910
1011 octo-cli = hsPkgs.octo-cli.components.exes.octo;
1112 octopod-backend = hsPkgs.octopod-backend.components.exes.octopod-exe;
1213
13− terser = (import sources.nixpkgs { }).nodePackages.terser;
14+ terser = (import sources.nixpkgs { inherit system; }).nodePackages.terser;
1415
1516 octopod-frontend-ugly = pkgs.runCommand "octopod-frontend-ugly"
1617 { } ''
@@ -20,23 +21,15 @@ let
2021 ${terser}/bin/terser ${hsPkgs.octopod-frontend-pretty}/all.js -o $out/all.js -mangle -c
2122 '';
2223
23− cacert' = pkgs.cacert.overrideAttrs (o: {
24− fixupPhase = ''
25− cat $out/etc/ssl/certs/* > $out/etc/ssl/certs/ca-certificates.crt
26− '';
27− });
28−
2924 octopod-server-container = pkgs.dockerTools.buildImage {
3025 name = "octopod-server-container-slim";
3126 contents = with pkgs; [
32− octopod-backend
33− octopod-frontend-ugly
3427 git
3528 coreutils
3629 bash
3730 openssh
3831 gnugrep
39− cacert'
32+ cacert
4033 shadow
4134 findutils
4235 ];
@@ -49,14 +42,9 @@ let
4942 useradd octopod -d /home/octopod
5043 chown octopod.octopod /home/octopod
5144
52− mkdir /app
53− cp -av ${octopod-backend}/bin/octopod-exe /app/octopod-exe
54−
5545 mkdir -p /migrations/{deploy,revert,verify}
5646 cp -av ${migrations}/* /migrations/
5747
58− mkdir /tls /tls_store
59−
6048 mkdir -p /www/static/{images,styles,vendors/outline}
6149 cp -av ${octopod-frontend-ugly}/* /www/
6250 cp -av ${octopod-css}/production/images/* /www/static/images/
@@ -65,46 +53,41 @@ let
6553 cp -av ${octopod-css}/favicons/* /www/
6654 '';
6755
68− config = {
69− Entrypoint = [ "/app/octopod-exe" ];
70− Cmd = [
71− "--port"
72− "4443"
73− "--ui-port"
74− "4000"
75− "--ws-port"
76− "4020"
77− "--db"
78− "host='127.0.0.1' port=5432 user='octopod' password='octopod'"
79− "--db-pool-size"
80− "10"
81− "--tls-cert-path"
82− "/tls/server_cert.pem"
83− "--tls-key-path"
84− "/tls/server_key.pem"
85− "--tls-store-path"
86− "/tls_store"
87− ];
88− Env = [
89− "PATH=/utils:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
90− "USER=octopod"
91− ];
92− };
56+ config =
57+ let entrypoint = "${octopod-backend}/bin/octopod-exe"; in
58+ {
59+ Entrypoint = [ entrypoint ];
60+ Cmd = [
61+ "--port"
62+ "4443"
63+ "--ui-port"
64+ "4000"
65+ "--ws-port"
66+ "4020"
67+ "--db"
68+ "host='127.0.0.1' port=5432 user='octopod' password='octopod'"
69+ "--db-pool-size"
70+ "10"
71+ ];
72+ Env = [
73+ "PATH=/utils:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
74+ "USER=octopod"
75+ "ENTRYPOINT=${entrypoint}"
76+ ];
77+ };
9378 };
9479
9580 octo-cli-container = pkgs.dockerTools.buildImage {
9681 name = "octo-cli-container-slim";
97− contents = with pkgs; [ octo-cli coreutils bash ];
98−
99− runAsRoot = ''
100− mkdir /app
101− cp -av ${octo-cli}/bin/octo /app/octo
102− '';
103−
104− config = {
105− Entrypoint = [ "/app/octo" ];
106− Env = [ "EDITOR=${pkgs.vim}/bin/vim" ];
107− };
82+ contents = with pkgs; [ cacert ];
83+
84+ config = let entrypoint = "${octo-cli}/bin/octo"; in
85+ {
86+ Entrypoint = [ entrypoint ];
87+ Env = [
88+ "ENTRYPOINT=${entrypoint}"
89+ ];
90+ };
10891 };
10992in
11093{
nix/sources.json+12
@@ -23,6 +23,18 @@
2323 "url": "https://github.com/nmattia/niv/archive/ba57d5a29b4e0f2085917010380ef3ddc3cf380f.tar.gz",
2424 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
2525 },
26+ "nix-filter": {
27+ "branch": "master",
28+ "description": "a small self-container source filtering lib",
29+ "homepage": "",
30+ "owner": "numtide",
31+ "repo": "nix-filter",
32+ "rev": "3c9e33ed627e009428197b07216613206f06ed80",
33+ "sha256": "19w142crrkywxynmyw4rhz4nglrg64yjawfkw3j91qwkwbfjds84",
34+ "type": "tarball",
35+ "url": "https://github.com/numtide/nix-filter/archive/3c9e33ed627e009428197b07216613206f06ed80.tar.gz",
36+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
37+ },
2638 "nixpkgs": {
2739 "branch": "master",
2840 "description": "Nix Packages collection",
octo-cli/octo-cli.cabal+20−7
@@ -15,8 +15,8 @@ executable octo
1515 other-modules:
1616 Octopod.CLI
1717 Octopod.CLI.Args
18− Octopod.CLI.TLS
1918 Text.Layout.Table.Extras
19+ Octopod.PowerAPI.Auth.Client
2020 default-extensions:
2121 BlockArguments
2222 ConstraintKinds
@@ -34,17 +34,20 @@ executable octo
3434 TypeApplications
3535 TypeOperators
3636 OverloadedLabels
37+ FlexibleInstances
38+ TypeFamilies
39+ MultiParamTypeClasses
40+ FunctionalDependencies
41+ UndecidableInstances
42+ ViewPatterns
3743 build-depends:
3844 aeson,
3945 base,
4046 bytestring,
4147 chronos,
42− connection,
4348 data-default-class,
4449 generic-lens,
4550 http-client-tls,
46− http-client,
47− http-types,
4851 lens,
4952 octopod-api,
5053 octopod-common,
@@ -53,11 +56,21 @@ executable octo
5356 servant-client,
5457 servant,
5558 text,
56− tls,
5759 transformers,
5860 table-layout ^>= 0.9.0.0,
61+ servant-auth,
5962 hs-source-dirs: src
6063 default-language: Haskell2010
6164 ghc-options:
62− -Wall
63− -Werror
65+ -Weverything
66+ -Wno-implicit-prelude
67+ -Wno-missing-safe-haskell-mode
68+ -Wno-safe
69+ -Wno-prepositive-qualified-module
70+ -Wno-missing-import-lists
71+ -Wno-all-missed-specialisations
72+ -Wno-missing-local-signatures
73+ -Wno-partial-fields
74+ -Wno-unsafe
75+ -Wno-monomorphism-restriction
76+ -Wno-missed-specialisations
octo-cli/src/Main.hs+2−2
@@ -1,6 +1,6 @@
1−module Main where
1+module Main (main) where
22
3−import Octopod.CLI
3+import Octopod.CLI
44
55main :: IO ()
66main = runOcto
octo-cli/src/Octopod/CLI.hs+187−170
@@ -1,48 +1,60 @@
1+{-# LANGUAGE PartialTypeSignatures #-}
2+
13module Octopod.CLI (runOcto) where
24
3−import Chronos
4−import Control.Lens hiding (List)
5−import Control.Monad
6−import Control.Monad.IO.Class
7−import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
8−import Data.Aeson (decode)
9−import qualified Data.ByteString.Char8 as BSC
5+import Chronos
6+import Common.Types
7+import Common.Utils (dfiName)
8+import Control.Lens hiding (List)
9+import Control.Monad
10+import Control.Monad.IO.Class
11+import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
12+import Data.Aeson (decode)
1013import qualified Data.ByteString.Lazy.Char8 as LBSC
11−import Data.Coerce
12−import Data.Generics.Labels ()
13−import Data.Generics.Product
14−import Data.Proxy
15−import Data.Text (Text)
14+import Data.Coerce
15+import Data.Generics.Labels ()
16+import Data.Generics.Product
17+import Data.Proxy
18+import Data.Text (Text)
1619import qualified Data.Text as T
1720import qualified Data.Text.IO as T
18−import Data.Text.Lens
19−import GHC.IO.Encoding
20−import Network.HTTP.Client (Manager)
21−import Prelude as P
22−import Servant.API
23−import Servant.Client
24− (BaseUrl(..), ClientEnv, ClientError, ClientM, client, mkClientEnv,
25− runClientM)
26−import Servant.Client.Core
27− (ClientError(FailureResponse), ResponseF(..), parseBaseUrl)
28−import System.Environment (lookupEnv)
29−import System.Exit
30−
31−import Common.Types
32−import Common.Utils (dfiName)
33−import Octopod.CLI.Args
34−import Octopod.CLI.TLS (makeClientManager)
35−import Octopod.PowerAPI
36−import Text.Layout.Table
37−import Text.Layout.Table.Extras ()
21+import Data.Text.Lens
22+import GHC.IO.Encoding
23+import Network.HTTP.Client.TLS
24+import Octopod.CLI.Args
25+import Octopod.PowerAPI
26+import Octopod.PowerAPI.Auth.Client
27+import Servant.API
28+import Servant.Client
29+ ( BaseUrl (..),
30+ ClientEnv,
31+ ClientError,
32+ ClientM,
33+ client,
34+ mkClientEnv,
35+ runClientM,
36+ )
37+import Servant.Client.Core
38+ ( ClientError (FailureResponse),
39+ ResponseF (..),
40+ parseBaseUrl,
41+ )
42+import System.Environment (lookupEnv)
43+import System.Exit
44+import Text.Layout.Table
45+import Text.Layout.Table.Extras ()
46+import Prelude as P
3847
3948-- | Runs the octo CLI.
4049runOcto :: IO ()
4150runOcto = do
4251 setLocaleEncoding utf8
52+ (AuthHeaderAuthCtx -> auth) <-
53+ lookupEnv "OCTO_AUTHORIZATION_HEADER"
54+ >>= maybe (die "OCTO_AUTHORIZATION_HEADER is not set") pure
4355 args <- parseArgs
4456 env <- getBaseUrl
45− manager <- initClientManager env
57+ manager <- newTlsManager
4658 let clientEnv = mkClientEnv manager env
4759 flip runReaderT clientEnv $
4860 case args of
@@ -51,14 +63,11 @@ runOcto = do
5163 setDep <- liftIO $ parseSetDeploymentOverrides Public tSetDep
5264 setPApp <- liftIO $ parseSetApplicationOverrides Private tSetPAp
5365 setPDep <- liftIO $ parseSetDeploymentOverrides Private tSetPDep
54− let
55− appOvs = setApp ++ setPApp
56− depOvs = setDep ++ setPDep
57− handleCreate $ Deployment (coerce tName) (coerce tTag) appOvs depOvs
58− List ->
59− handleList
60− Archive tName ->
61− handleArchive . coerce $ tName
66+ let appOvs = setApp ++ setPApp
67+ depOvs = setDep ++ setPDep
68+ handleCreate auth $ Deployment (coerce tName) (coerce tTag) appOvs depOvs
69+ List -> handleList auth
70+ Archive tName -> handleArchive auth . coerce $ tName
6271 Update tName tTag tSetAp tUnsAp tSetD tUnsD tSetPAp tSetPD -> do
6372 setApp <- liftIO $ parseSetApplicationOverrides Public tSetAp
6473 setDep <- liftIO $ parseSetDeploymentOverrides Public tSetD
@@ -66,131 +75,121 @@ runOcto = do
6675 unsetDep <- liftIO $ parseUnsetDeploymentOverrides Public tUnsD
6776 setPApp <- liftIO $ parseSetApplicationOverrides Private tSetPAp
6877 setPDep <- liftIO $ parseSetDeploymentOverrides Private tSetPD
69− let
70− appOvs = setApp ++ setPApp
71− depOvs = setDep ++ setPDep
72− tName' = coerce tName
73− tTag' = coerce tTag
74− handleUpdate tName' tTag' appOvs unsetApp depOvs unsetDep
78+ let appOvs = setApp ++ setPApp
79+ depOvs = setDep ++ setPDep
80+ tName' = coerce tName
81+ tTag' = coerce tTag
82+ handleUpdate auth tName' tTag' appOvs unsetApp depOvs unsetDep
7583 Info tName ->
76− handleInfo . coerce $ tName
84+ handleInfo auth . coerce $ tName
7785 Cleanup tName ->
78− handleCleanup . coerce $ tName
86+ handleCleanup auth . coerce $ tName
7987 Restore tName ->
80− handleRestore . coerce $ tName
88+ handleRestore auth . coerce $ tName
8189 CleanArchive ->
82− handleCleanArchive
83− GetActionLogs aId l -> handleGetActionInfo aId l
84−
85−-- | Initializes the HTTP connection manager.
86−initClientManager :: BaseUrl -> IO Manager
87−initClientManager baseURL = do
88− let
89− cert <- lookupEnv "TLS_CERT_PATH"
90− >>= maybe (pure $ TLSCertPath "cert.pem") (pure . TLSCertPath . BSC.pack)
91− key <- lookupEnv "TLS_KEY_PATH"
92− >>= maybe (pure $ TLSKeyPath "key.pem") (pure . TLSKeyPath . BSC.pack)
93− makeClientManager (baseUrlHost baseURL) cert key
90+ handleCleanArchive auth
91+ GetActionLogs aId l -> handleGetActionInfo auth aId l
9492
9593-- | Returns BaseUrl from 'OCTOPOD_URL' environment variable
9694-- or exit with exit_code=1.
9795getBaseUrl :: IO BaseUrl
9896getBaseUrl = do
99− octopodURL <- lookupEnv "OCTOPOD_URL"
100− >>= maybe (die "OCTOPOD_URL is not set") pure
97+ octopodURL <-
98+ lookupEnv "OCTOPOD_URL"
99+ >>= maybe (die "OCTOPOD_URL is not set") pure
101100 case parseBaseUrl octopodURL of
102101 Just url -> pure url
103102 Nothing -> die "could not parse OCTOPOD_URL"
104103
105104-- | Handles the 'create' subcommand.
106−handleCreate :: Deployment -> ReaderT ClientEnv IO ()
107−handleCreate createCmd = do
105+handleCreate :: AuthContext AuthHeaderAuth -> Deployment -> ReaderT ClientEnv IO ()
106+handleCreate auth createCmd = do
108107 clientEnv <- ask
109108 liftIO $ do
110− response <- runClientM (createH createCmd) clientEnv
109+ response <- runClientM (createH auth createCmd) clientEnv
111110 handleResponse (const $ pure ()) response
112111
113112-- | Handles the 'list' subcommand.
114−handleList :: ReaderT ClientEnv IO ()
115−handleList = do
113+handleList :: AuthContext AuthHeaderAuth -> ReaderT ClientEnv IO ()
114+handleList auth = do
116115 clientEnv <- ask
117116 liftIO $ do
118− resp <- runClientM listH clientEnv
119− let
120− getName dep = dep ^.dfiName
121− names = T.unlines . coerce . (getName <$>) <$> resp
117+ resp <- runClientM (listH auth) clientEnv
118+ let getName dep = dep ^. dfiName
119+ names = T.unlines . coerce . (getName <$>) <$> resp
122120 handleResponse T.putStr names
123121
124122-- | Handles the 'archive' subcommand.
125−handleArchive :: DeploymentName -> ReaderT ClientEnv IO ()
126−handleArchive dName = do
123+handleArchive :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
124+handleArchive auth dName = do
127125 clientEnv <- ask
128126 liftIO $
129− handleResponse (const $ pure ()) =<< runClientM (archiveH dName) clientEnv
127+ handleResponse (const $ pure ()) =<< runClientM (archiveH auth dName) clientEnv
130128
131129-- | Handles the 'update' subcommand.
132−handleUpdate
133− :: DeploymentName
134− -> DeploymentTag
135− -> ApplicationOverrides
136− -> ApplicationOverrides
137− -> DeploymentOverrides
138− -> DeploymentOverrides
139− -> ReaderT ClientEnv IO ()
140−handleUpdate dName dTag dNewAppOvs dOldAppOvs dNewDepOvs dOldDepOvs = do
130+handleUpdate ::
131+ AuthContext AuthHeaderAuth ->
132+ DeploymentName ->
133+ DeploymentTag ->
134+ ApplicationOverrides ->
135+ ApplicationOverrides ->
136+ DeploymentOverrides ->
137+ DeploymentOverrides ->
138+ ReaderT ClientEnv IO ()
139+handleUpdate auth dName dTag dNewAppOvs dOldAppOvs dNewDepOvs dOldDepOvs = do
141140 clientEnv <- ask
142141 liftIO $ do
143− let
144− dUpdate = DeploymentUpdate
145− { newTag = dTag
146− , newAppOverrides = dNewAppOvs
147− , oldAppOverrides = dOldAppOvs
148− , newDeploymentOverrides = dNewDepOvs
149− , oldDeploymentOverrides = dOldDepOvs
150− }
151− response <- runClientM (updateH dName dUpdate) clientEnv
142+ let dUpdate =
143+ DeploymentUpdate
144+ { newTag = dTag
145+ , newAppOverrides = dNewAppOvs
146+ , oldAppOverrides = dOldAppOvs
147+ , newDeploymentOverrides = dNewDepOvs
148+ , oldDeploymentOverrides = dOldDepOvs
149+ }
150+ response <- runClientM (updateH auth dName dUpdate) clientEnv
152151 handleResponse (const $ pure ()) response
153152
154153-- | Handles the 'info' subcommand.
155−handleInfo :: DeploymentName -> ReaderT ClientEnv IO ()
156−handleInfo dName = do
154+handleInfo :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
155+handleInfo auth dName = do
157156 clientEnv <- ask
158157 liftIO $ do
159− res <- runClientM (infoH dName) clientEnv
158+ res <- runClientM (infoH auth dName) clientEnv
160159 case res of
161160 Right (i : _) -> printInfo i
162161 Right [] -> putStrLn notFoundMsg
163162 Left err -> putStrLn $ "request failed, reason: " ++ show err
164− where
165− notFoundMsg = "deployment " ++ T.unpack (coerce dName) ++ " not found"
163+ where
164+ notFoundMsg = "deployment " ++ T.unpack (coerce dName) ++ " not found"
166165
167166-- | Handles the 'cleanup' subcommand.
168−handleCleanup :: DeploymentName -> ReaderT ClientEnv IO ()
169−handleCleanup dName = do
167+handleCleanup :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
168+handleCleanup auth dName = do
170169 clientEnv <- ask
171170 liftIO $
172− handleResponse (const $ pure ()) =<< runClientM (cleanupH dName) clientEnv
171+ handleResponse (const $ pure ()) =<< runClientM (cleanupH auth dName) clientEnv
173172
174173-- | Handles the 'restore' subcommand.
175−handleRestore :: DeploymentName -> ReaderT ClientEnv IO ()
176−handleRestore dName = do
174+handleRestore :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
175+handleRestore auth dName = do
177176 clientEnv <- ask
178177 liftIO $
179− handleResponse (const $ pure ()) =<< runClientM (restoreH dName) clientEnv
178+ handleResponse (const $ pure ()) =<< runClientM (restoreH auth dName) clientEnv
180179
181180-- | Handles the 'clean-archive' subcommand.
182−handleCleanArchive :: ReaderT ClientEnv IO ()
183−handleCleanArchive = do
181+handleCleanArchive :: AuthContext AuthHeaderAuth -> ReaderT ClientEnv IO ()
182+handleCleanArchive auth = do
184183 clientEnv <- ask
185184 liftIO $
186− handleResponse (const $ pure ()) =<< runClientM cleanArchiveH clientEnv
185+ handleResponse (const $ pure ()) =<< runClientM (cleanArchiveH auth) clientEnv
187186
188187-- | Handles the 'logs' subcommand.
189−handleGetActionInfo :: ActionId -> LogOutput -> ReaderT ClientEnv IO ()
190−handleGetActionInfo aId l = do
188+handleGetActionInfo :: AuthContext AuthHeaderAuth -> ActionId -> LogOutput -> ReaderT ClientEnv IO ()
189+handleGetActionInfo auth aId l = do
191190 clientEnv <- ask
192191 liftIO $ do
193− runClientM (getActionInfoH aId) clientEnv >>= \case
192+ runClientM (getActionInfoH auth aId) clientEnv >>= \case
194193 Left err -> print err
195194 Right x -> case l of
196195 Out -> T.putStrLn $ x ^. #stdout
@@ -201,40 +200,46 @@ handleGetActionInfo aId l = do
201200 T.putStrLn "\t\tstderr:\n"
202201 T.putStrLn $ x ^. #stderr
203202
204−
205−listH :: ClientM [DeploymentFullInfo]
206−
207−createH :: Deployment -> ClientM CommandResponse
208−
209−archiveH :: DeploymentName -> ClientM CommandResponse
210−
211−updateH :: DeploymentName -> DeploymentUpdate -> ClientM CommandResponse
212−
213−infoH :: DeploymentName -> ClientM [DeploymentInfo]
214−
215−_fullInfoH :: DeploymentName -> ClientM DeploymentFullInfo
216−
217−_statusH :: DeploymentName -> ClientM CurrentDeploymentStatus
218−
219−cleanupH :: DeploymentName -> ClientM CommandResponse
220−
221−restoreH :: DeploymentName -> ClientM CommandResponse
222−
223−getActionInfoH :: ActionId -> ClientM ActionInfo
224−
225−cleanArchiveH :: ClientM CommandResponse
226−
203+listH :: AuthContext AuthHeaderAuth -> ClientM [DeploymentFullInfo]
204+createH :: AuthContext AuthHeaderAuth -> Deployment -> ClientM CommandResponse
205+archiveH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CommandResponse
206+updateH :: AuthContext AuthHeaderAuth -> DeploymentName -> DeploymentUpdate -> ClientM CommandResponse
207+infoH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM [DeploymentInfo]
208+_fullInfoH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM DeploymentFullInfo
209+_statusH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CurrentDeploymentStatus
210+cleanupH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CommandResponse
211+restoreH :: AuthContext AuthHeaderAuth -> DeploymentName -> ClientM CommandResponse
212+getActionInfoH :: AuthContext AuthHeaderAuth -> ActionId -> ClientM ActionInfo
213+cleanArchiveH :: AuthContext AuthHeaderAuth -> ClientM CommandResponse
227214( listH
228− :<|> createH
229− :<|> archiveH
230− :<|> updateH
231− :<|> infoH
232− :<|> _fullInfoH
233− :<|> _statusH
234− :<|> cleanupH
235− :<|> restoreH)
236− :<|> getActionInfoH
237− :<|> cleanArchiveH = client (Proxy @PowerAPI)
215+ :<|> createH
216+ :<|> archiveH
217+ :<|> updateH
218+ :<|> infoH
219+ :<|> _fullInfoH
220+ :<|> _statusH
221+ :<|> cleanupH
222+ :<|> restoreH
223+ )
224+ :<|> getActionInfoH
225+ :<|> cleanArchiveH = pushArrowIntoServantAlt $ client (Proxy @PowerAPI)
226+
227+type PushArrowIntoServantAlt a b = PushArrowIntoServantAlt' a b (CanPushArrow a)
228+
229+class CanPushArrow x ~ f => PushArrowIntoServantAlt' x y (f :: Bool) | x f -> y where
230+ pushArrowIntoServantAlt :: x -> y
231+
232+type family CanPushArrow x :: Bool where
233+ CanPushArrow (_ -> (_ :<|> _)) = 'True
234+ CanPushArrow _ = 'False
235+
236+instance (PushArrowIntoServantAlt (x -> a) a', PushArrowIntoServantAlt (x -> b) b') => PushArrowIntoServantAlt' (x -> (a :<|> b)) (a' :<|> b') 'True where
237+ pushArrowIntoServantAlt f =
238+ pushArrowIntoServantAlt (\x -> let (a :<|> _) = f x in a)
239+ :<|> pushArrowIntoServantAlt (\x -> let (_ :<|> b) = f x in b)
240+
241+instance CanPushArrow (x -> y) ~ 'False => PushArrowIntoServantAlt' (x -> y) (x -> y) 'False where
242+ pushArrowIntoServantAlt x = x
238243
239244-- | Handles response from Octopod Server.
240245handleResponse :: (a -> IO ()) -> Either ClientError a -> IO ()
@@ -258,10 +263,12 @@ printInfo :: DeploymentInfo -> IO ()
258263printInfo (DeploymentInfo (Deployment _ dTag dAppOvs dStOvs) dMeta dLogs) = do
259264 T.putStrLn "Current settings:"
260265 T.putStrLn $ "tag: " <> coerce dTag
261− T.putStrLn $ "application overrides: "
262− <> (formatOverrides $ coerce <$> dAppOvs)
263− T.putStrLn $ "deployment overrides: "
264− <> (formatOverrides $ coerce <$> dStOvs)
266+ T.putStrLn $
267+ "application overrides: "
268+ <> (formatOverrides $ coerce <$> dAppOvs)
269+ T.putStrLn $
270+ "deployment overrides: "
271+ <> (formatOverrides $ coerce <$> dStOvs)
265272 T.putStrLn $ "metadata: "
266273 forM_ dMeta $ \m ->
267274 T.putStrLn $
@@ -283,29 +290,39 @@ ppDeploymentLogs ds =
283290 , column expand center noAlign def
284291 ]
285292 unicodeBoldHeaderS
286− (titlesH
287− [ "Created at"
288− , "Action id"
289− , "Action"
290− , "Tag"
291− , "App overrides"
292− , "Deployment overrides"
293− , "Exit code"
294− ])
293+ ( titlesH
294+ [ "Created at"
295+ , "Action id"
296+ , "Action"
297+ , "Tag"
298+ , "App overrides"
299+ , "Deployment overrides"
300+ , "Exit code"
301+ ]
302+ )
295303 $ ppDeploymentLogRow <$> ds
296304
297305-- | Pretty-prints the deployment log.
298306ppDeploymentLogRow :: DeploymentLog -> RowGroup Text
299−ppDeploymentLogRow dLog = colsAllG top
300− [ [encode_YmdHMS SubsecondPrecisionAuto w3c
301− (timeToDatetime . Time . fromIntegral
302− $ dLog ^. field @"createdAt" * 10 ^ (9 :: Int))]
303− , [dLog ^. field @"actionId" . to unActionId . re _Show . packed]
304− , [dLog ^. field @"action" . coerced]
305− , [dLog ^. field @"deploymentTag" . coerced]
306− , dLog ^. field @"deploymentAppOverrides"
307− . to (fmap $ formatOverride . coerce)
308− , dLog ^. field @"deploymentDepOverrides"
309− . to (fmap $ formatOverride . coerce)
310− , [dLog ^. field @"exitCode" . re _Show . packed]
311− ]
307+ppDeploymentLogRow dLog =
308+ colsAllG
309+ top
310+ [
311+ [ encode_YmdHMS
312+ SubsecondPrecisionAuto
313+ w3c
314+ ( timeToDatetime . Time . fromIntegral $
315+ dLog ^. field @"createdAt" * 10 ^ (9 :: Int)
316+ )
317+ ]
318+ , [dLog ^. field @"actionId" . to unActionId . re _Show . packed]
319+ , [dLog ^. field @"action" . coerced]
320+ , [dLog ^. field @"deploymentTag" . coerced]
321+ , dLog
322+ ^. field @"deploymentAppOverrides"
323+ . to (fmap $ formatOverride . coerce)
324+ , dLog
325+ ^. field @"deploymentDepOverrides"
326+ . to (fmap $ formatOverride . coerce)
327+ , [dLog ^. field @"exitCode" . re _Show . packed]
328+ ]
octo-cli/src/Octopod/CLI/Args.hs+158−122
@@ -1,71 +1,71 @@
1−{-|
2−Module : Octopod.CLI.Args
3−Description : octo CLI arguments parser utils.
4−-}
1+{-# OPTIONS_GHC -Wno-missing-export-lists #-}
52
3+-- |
4+--Module : Octopod.CLI.Args
5+--Description : octo CLI arguments parser utils.
66module Octopod.CLI.Args where
77
8−import Common.Types
9−import Data.Text
10−import Options.Applicative
8+import Common.Types
9+import Data.Text
10+import Options.Applicative
1111
1212-- | octo CLI subcommands and arguments definition.
1313data Args
1414 = Create
15− { name :: Text
16− -- ^ deployment name
17− , tag :: Text
18− -- ^ deployment tag
19− , setAppOverrides :: [Text]
20− -- ^ application-level overrides to set
21− , setDeploymentOverrides :: [Text]
22− -- ^ deployment-level overrides to set
23− , setAppPrivateOverrides :: [Text]
24− -- ^ application-level private overrides to set
25− , setDeploymentPrivateOverrides :: [Text]
26− -- ^ deployment-level private overrides to set
27− }
15+ { -- | deployment name
16+ name :: Text
17+ , -- | deployment tag
18+ tag :: Text
19+ , -- | application-level overrides to set
20+ setAppOverrides :: [Text]
21+ , -- | deployment-level overrides to set
22+ setDeploymentOverrides :: [Text]
23+ , -- | application-level private overrides to set
24+ setAppPrivateOverrides :: [Text]
25+ , -- | deployment-level private overrides to set
26+ setDeploymentPrivateOverrides :: [Text]
27+ }
2828 | List
2929 | Archive
30− { name :: Text
31− -- ^ deployment name
32− }
30+ { -- | deployment name
31+ name :: Text
32+ }
3333 | Update
34− { name :: Text
35− -- ^ deployment name
36− , tag :: Text
37− -- ^ deployment tag
38− , setAppOverrides :: [Text]
39− -- ^ application-level overrides to set
40− , unsetAppOverrides :: [Text]
41− -- ^ application-level overrides to unset
42− , setDeploymentOverrides :: [Text]
43− -- ^ deployment-level overrides to set
44− , unsetDeploymentOverrides :: [Text]
45− -- ^ deployment-level overrides to unset
46− , setAppPrivateOverrides :: [Text]
47− -- ^ application-level private overrides to set
48− , setDeploymentPrivateOverrides :: [Text]
49− -- ^ deployment-level private overrides to set
50− }
34+ { -- | deployment name
35+ name :: Text
36+ , -- | deployment tag
37+ tag :: Text
38+ , -- | application-level overrides to set
39+ setAppOverrides :: [Text]
40+ , -- | application-level overrides to unset
41+ unsetAppOverrides :: [Text]
42+ , -- | deployment-level overrides to set
43+ setDeploymentOverrides :: [Text]
44+ , -- | deployment-level overrides to unset
45+ unsetDeploymentOverrides :: [Text]
46+ , -- | application-level private overrides to set
47+ setAppPrivateOverrides :: [Text]
48+ , -- | deployment-level private overrides to set
49+ setDeploymentPrivateOverrides :: [Text]
50+ }
5151 | Info
52− { name :: Text
53− -- ^ deployment name
54− }
52+ { -- | deployment name
53+ name :: Text
54+ }
5555 | Cleanup
56− { name :: Text
57− -- ^ deployment name
58− }
56+ { -- | deployment name
57+ name :: Text
58+ }
5959 | Restore
60− { name :: Text
61− -- ^ deployment name
62− }
60+ { -- | deployment name
61+ name :: Text
62+ }
6363 | CleanArchive
6464 | GetActionLogs ActionId LogOutput
6565 deriving stock (Show)
6666
6767data LogOutput = Err | Out | ErrOut
68− deriving stock Show
68+ deriving stock (Show)
6969
7070readLogOutput :: String -> Maybe LogOutput
7171readLogOutput "stderr" = Just Err
@@ -83,43 +83,58 @@ parseArgs = execParser $ info (commandArgs <**> helper) fullDesc
8383-- | Parses octo CLI subcommands.
8484commandArgs :: Parser Args
8585commandArgs =
86− hsubparser (
87− command "create" (info createArgs (progDesc "create a new deployment"))
88− <> command "list" (info listArgs (progDesc "get names all deployments"))
89− <> command "archive" (info archiveArgs (progDesc "archive the deployment"))
90− <> command "update" (info updateArgs (progDesc "update the deployment"))
91− <> command "info" (info infoArgs (progDesc "get the deployment info"))
92− <> command "cleanup" (info cleanupArgs (progDesc "cleanup the deployment"))
93− <> command "restore" (info restoreArgs (progDesc "restore the deployment"))
94− <> command "clean-archive"
95− (info cleanupArchiveArgs (progDesc "cleanup all archived deployments"))
96− <> command "logs"
97− (info actionLogsArgs (progDesc "get deployment logs of a given action")))
86+ hsubparser
87+ ( command "create" (info createArgs (progDesc "create a new deployment"))
88+ <> command "list" (info listArgs (progDesc "get names all deployments"))
89+ <> command "archive" (info archiveArgs (progDesc "archive the deployment"))
90+ <> command "update" (info updateArgs (progDesc "update the deployment"))
91+ <> command "info" (info infoArgs (progDesc "get the deployment info"))
92+ <> command "cleanup" (info cleanupArgs (progDesc "cleanup the deployment"))
93+ <> command "restore" (info restoreArgs (progDesc "restore the deployment"))
94+ <> command
95+ "clean-archive"
96+ (info cleanupArchiveArgs (progDesc "cleanup all archived deployments"))
97+ <> command
98+ "logs"
99+ (info actionLogsArgs (progDesc "get deployment logs of a given action"))
100+ )
98101
99102-- | Parses arguments of 'create' subcommand.
100103createArgs :: Parser Args
101104createArgs =
102105 Create
103− <$> strOption (long "name" <> short 'n' <> help "deployment name")
104− <*> strOption (long "tag" <> short 't' <> help "deployment tag")
105− <*> many (strOption
106− (long "set-app-env-override"
107− <> short 'e'
108− <> help "set application level override"))
109− <*> many (strOption
110− (long "set-deployment-override"
111− <> short 'o'
112− <> help "set deployment level override"))
113− <*> many (strOption
114− (long "set-app-env-private-override"
115− <> short 'a'
116− <> help "set application level private override"
117− <> internal))
118− <*> many (strOption
119− (long "set-deployment-private-override"
120− <> short 's'
121− <> help "set deployment level private override"
122− <> internal))
106+ <$> strOption (long "name" <> short 'n' <> help "deployment name")
107+ <*> strOption (long "tag" <> short 't' <> help "deployment tag")
108+ <*> many
109+ ( strOption
110+ ( long "set-app-env-override"
111+ <> short 'e'
112+ <> help "set application level override"
113+ )
114+ )
115+ <*> many
116+ ( strOption
117+ ( long "set-deployment-override"
118+ <> short 'o'
119+ <> help "set deployment level override"
120+ )
121+ )
122+ <*> many
123+ ( strOption
124+ ( long "set-app-env-private-override"
125+ <> short 'a'
126+ <> help "set application level private override"
127+ <> internal
128+ )
129+ )
130+ <*> many
131+ ( strOption
132+ ( long "set-deployment-private-override"
133+ <> short 's'
134+ <> help "set deployment level private override"
135+ <> internal
136+ )
137+ )
123138
124139-- | Parses arguments of 'list' subcommand.
125140listArgs :: Parser Args
@@ -130,58 +145,76 @@ listArgs =
130145archiveArgs :: Parser Args
131146archiveArgs =
132147 Archive
133− <$> strOption (long "name" <> short 'n' <> help "deployment name")
148+ <$> strOption (long "name" <> short 'n' <> help "deployment name")
134149
135150-- | Parses arguments of 'update' subcommand.
136151updateArgs :: Parser Args
137152updateArgs =
138153 Update
139− <$> strOption (long "name" <> short 'n' <> help "deployment name")
140− <*> strOption (long "tag" <> short 't' <> help "deployment tag")
141− <*> many (strOption
142− (long "set-app-env-override"
143− <> short 'e'
144− <> help "set application level override"))
145− <*> many (strOption
146− (long "unset-app-env-override"
147− <> short 'E'
148− <> help "unset an application level override"))
149− <*> many (strOption
150− (long "set-deployment-override"
151− <> short 'o'
152− <> help "set deployment level override"))
153− <*> many (strOption
154− (long "unset-deployment-override"
155− <> short 'O'
156− <> help "unset a deployment level override"))
157− <*> many (strOption
158− (long "set-app-env-private-override"
159− <> short 'a'
160− <> help "set application level private override"
161− <> internal))
162− <*> many (strOption
163− (long "set-deployment-private-override"
164− <> short 's'
165− <> help "set deployment level private override"
166− <> internal))
154+ <$> strOption (long "name" <> short 'n' <> help "deployment name")
155+ <*> strOption (long "tag" <> short 't' <> help "deployment tag")
156+ <*> many
157+ ( strOption
158+ ( long "set-app-env-override"
159+ <> short 'e'
160+ <> help "set application level override"
161+ )
162+ )
163+ <*> many
164+ ( strOption
165+ ( long "unset-app-env-override"
166+ <> short 'E'
167+ <> help "unset an application level override"
168+ )
169+ )
170+ <*> many
171+ ( strOption
172+ ( long "set-deployment-override"
173+ <> short 'o'
174+ <> help "set deployment level override"
175+ )
176+ )
177+ <*> many
178+ ( strOption
179+ ( long "unset-deployment-override"
180+ <> short 'O'
181+ <> help "unset a deployment level override"
182+ )
183+ )
184+ <*> many
185+ ( strOption
186+ ( long "set-app-env-private-override"
187+ <> short 'a'
188+ <> help "set application level private override"
189+ <> internal
190+ )
191+ )
192+ <*> many
193+ ( strOption
194+ ( long "set-deployment-private-override"
195+ <> short 's'
196+ <> help "set deployment level private override"
197+ <> internal
198+ )
199+ )
167200
168201-- | Parses arguments of 'info' subcommand.
169202infoArgs :: Parser Args
170203infoArgs =
171204 Info
172− <$> strOption (long "name" <> short 'n' <> help "deployment name")
205+ <$> strOption (long "name" <> short 'n' <> help "deployment name")
173206
174207-- | Parses arguments of 'cleanup' subcommand.
175208cleanupArgs :: Parser Args
176209cleanupArgs =
177210 Cleanup
178− <$> strOption (long "name" <> short 'n' <> help "deployment name")
211+ <$> strOption (long "name" <> short 'n' <> help "deployment name")
179212
180213-- | Parses arguments of 'restore' subcommand.
181214restoreArgs :: Parser Args
182215restoreArgs =
183216 Restore
184− <$> strOption (long "name" <> short 'n' <> help "deployment name")
217+ <$> strOption (long "name" <> short 'n' <> help "deployment name")
185218
186219-- | Parses arguments of 'clean-archive' subcommand.
187220cleanupArchiveArgs :: Parser Args
@@ -189,11 +222,14 @@ cleanupArchiveArgs =
189222 pure CleanArchive
190223
191224actionLogsArgs :: Parser Args
192−actionLogsArgs = GetActionLogs
193− <$> (ActionId <$> option auto (long "action" <> short 'a' <> help "action id"))
194− <*> option (maybeReader readLogOutput)
195− ( long "log-type"
196− <> short 'l'
197− <> help "log types to print. Values are: stdout, stderr, all"
198− <> value ErrOut
199− <> completeWith ["stdout", "stderr", "all"])
225+actionLogsArgs =
226+ GetActionLogs
227+ <$> (ActionId <$> option auto (long "action" <> short 'a' <> help "action id"))
228+ <*> option
229+ (maybeReader readLogOutput)
230+ ( long "log-type"
231+ <> short 'l'
232+ <> help "log types to print. Values are: stdout, stderr, all"
233+ <> value ErrOut
234+ <> completeWith ["stdout", "stderr", "all"]
235+ )
octo-cli/src/Octopod/CLI/TLS.hsdeleted−39
@@ -1,39 +0,0 @@
1−module Octopod.CLI.TLS
2− ( makeClientManager
3− ) where
4−
5−import Common.Types
6−import qualified Data.ByteString.Char8 as BSC
7−import Data.Coerce
8−import Data.Default.Class (def)
9−import Network.Connection
10−import Network.HTTP.Client
11− (Manager, managerResponseTimeout, newManager, responseTimeoutMicro)
12−import Network.HTTP.Client.TLS
13−import Network.TLS
14− (ClientHooks(..), clientHooks, clientSupported, credentialLoadX509,
15− defaultParamsClient, supportedCiphers)
16−import Network.TLS.Extra.Cipher
17−
18−-- | Initializes the HTTP client using TLS
19−makeClientManager :: String -> TLSCertPath -> TLSKeyPath -> IO Manager
20−makeClientManager octopodHostName cert key = do
21− creds <- do
22− let
23− cert' = BSC.unpack . coerce $ cert
24− key' = BSC.unpack . coerce $ key
25− either error Just <$> credentialLoadX509 cert' key'
26− let
27− hooks = def
28− { onCertificateRequest = \_ -> return creds
29− , onServerCertificate = \_ _ _ _ -> return []
30− }
31− clientParams = (defaultParamsClient octopodHostName "")
32− { clientHooks = hooks
33− , clientSupported = def { supportedCiphers = ciphersuite_default }
34− }
35− tlsSettings = TLSSettings clientParams
36− timeout = 20 * 60 * 10 ^ (6 :: Int)
37− tlsManagerSettings' = (mkManagerSettings tlsSettings Nothing)
38− { managerResponseTimeout = responseTimeoutMicro timeout }
39− in newManager tlsManagerSettings'
octo-cli/src/Octopod/PowerAPI/Auth/Client.hsnew file+28
@@ -0,0 +1,28 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Octopod.PowerAPI.Auth.Client
4+ ( IsAuth (..),
5+ AuthContext (..),
6+ )
7+where
8+
9+import Data.Proxy
10+import Octopod.PowerAPI
11+import Servant.API hiding (addHeader)
12+import Servant.Auth
13+import Servant.Client.Core
14+
15+class IsAuth a where
16+ data AuthContext a
17+ applyAuth :: AuthContext a -> Request -> Request
18+
19+instance (IsAuth a, HasClient m api) => HasClient m (Auth '[a] x :> api) where
20+ type Client m (Auth '[a] x :> api) = AuthContext a -> Client m api
21+ clientWithRoute pm Proxy req val =
22+ clientWithRoute pm (Proxy :: Proxy api) (applyAuth val req)
23+
24+ hoistClientMonad pm _ f cl = hoistClientMonad pm (Proxy :: Proxy api) f . cl
25+
26+instance IsAuth AuthHeaderAuth where
27+ data AuthContext AuthHeaderAuth = AuthHeaderAuthCtx String
28+ applyAuth (AuthHeaderAuthCtx h) req = addHeader "Authorization" h req
octopod-api/octopod-api.cabal+13−2
@@ -35,6 +35,7 @@ library
3535 base,
3636 servant,
3737 octopod-common,
38+ servant-auth,
3839 if !impl(ghcjs)
3940 build-depends:
4041 servant-websockets,
@@ -42,5 +43,15 @@ library
4243 hs-source-dirs: src
4344 default-language: Haskell2010
4445 ghc-options:
45− -Wall
46− -Werror
46+ -Weverything
47+ -Wno-implicit-prelude
48+ -Wno-missing-safe-haskell-mode
49+ -Wno-safe
50+ -Wno-prepositive-qualified-module
51+ -Wno-missing-import-lists
52+ -Wno-all-missed-specialisations
53+ -Wno-missing-local-signatures
54+ -Wno-partial-fields
55+ -Wno-unsafe
56+ -Wno-monomorphism-restriction
57+ -Wno-missed-specialisations
octopod-api/src/Octopod/API/WebSocket.hs+7−3
@@ -1,6 +1,12 @@
11{-# LANGUAGE CPP #-}
22
3−module Octopod.API.WebSocket where
3+module Octopod.API.WebSocket
4+ ( ApiWSPath,
5+#ifndef ghcjs_HOST_OS
6+ WebSocketAPI,
7+#endif
8+ )
9+where
410
511#ifndef ghcjs_HOST_OS
612
@@ -10,10 +16,8 @@ import Servant.API.WebSocketConduit
1016
1117#endif
1218
13−
1419type ApiWSPath = "event"
1520
16−
1721#ifndef ghcjs_HOST_OS
1822
1923-- | WS API
octopod-api/src/Octopod/PowerAPI.hs+51−42
@@ -1,34 +1,41 @@
1−{-|
2−Module : Octopod.PowerAPI
3−Description : Backend endpoints.
4−
5−This module contains backend endpoints.
6−-}
7−
8−
1+-- |
2+-- Module : Octopod.PowerAPI
3+-- Description : Backend endpoints.
4+--
5+-- This module contains backend endpoints.
96module Octopod.PowerAPI
10− ( module Octopod.API.WebSocket
11− , PowerAPI
12− ) where
13−
14−import Servant.API
7+ ( module Octopod.API.WebSocket,
8+ PowerAPI,
9+ AuthHeaderAuth,
10+ )
11+where
1512
16−import Common.Types
17−import Octopod.API.WebSocket
13+import Common.Types
14+import Octopod.API.WebSocket
15+import Servant.API
16+import Servant.Auth
1817
1918type CaptureName = Capture "name" DeploymentName
2019
2120type ListEndpoint = Get '[JSON] [DeploymentFullInfo]
21+
2222type CreateEndpoint =
2323 ReqBody '[JSON] Deployment :> Post '[JSON] CommandResponse
24+
2425type ArchiveEndpoint c = c :> Delete '[JSON] CommandResponse
26+
2527type UpdateEndpoint c =
2628 c :> ReqBody '[JSON] DeploymentUpdate :> Put '[JSON] CommandResponse
29+
2730type InfoEndpoint c = c :> "info" :> Get '[JSON] [DeploymentInfo]
31+
2832type FullInfoEndpoint c = c :> "full_info" :> Get '[JSON] DeploymentFullInfo
33+
2934type StatusEndpoint c = c :> "status" :> Get '[JSON] CurrentDeploymentStatus
35+
3036type CleanupEndpoint c =
3137 c :> "cleanup" :> Delete '[JSON] CommandResponse
38+
3239type RestoreEndpoint c =
3340 c :> "restore" :> Patch '[JSON] CommandResponse
3441
@@ -39,33 +46,35 @@ type CleanArchiveEndpoint =
3946 "clean_archive" :> Delete '[JSON] CommandResponse
4047
4148type DeploymentAPI' c =
42− "api" :> "v1" :>
43− ( "deployments" :>
44− ( ListEndpoint
45− -- endpoint to get deployment list
46− :<|> CreateEndpoint
47− -- endpoint to create a new deployment
48− :<|> ArchiveEndpoint c
49− -- endpoint to archive an existing deployment
50− :<|> UpdateEndpoint c
51− -- endpoint to update an existing deployment
52− :<|> InfoEndpoint c
53− -- endpoint to get deployment info
54− :<|> FullInfoEndpoint c
55− -- endpoint to get full deployment info
56− :<|> StatusEndpoint c
57− -- endpoint to get deployment status
58− :<|> CleanupEndpoint c
59− -- endpoint to clean up resources of an archived deployment
60− :<|> RestoreEndpoint c
61− -- endpoint to restore an archived deployment
62− )
63− :<|> GetActionInfoEndpoint
64− -- endpoint to get action logs
65− :<|> CleanArchiveEndpoint
66− -- endpoint to clean up resources of all archived deployments
67− -- according to the archive retention policy
68− )
49+ Auth '[AuthHeaderAuth] () :> "api" :> "v1"
50+ :> ( "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
75+ )
76+
77+data AuthHeaderAuth
6978
7079-- | API for the octo CLI
7180type PowerAPI = DeploymentAPI' CaptureName
octopod-backend/app/Main.hs+2−2
@@ -1,6 +1,6 @@
1−module Main where
1+module Main (main) where
22
3−import Octopod.Server
3+import Octopod.Server
44
55main :: IO ()
66main = runOctopodServer
octopod-backend/octopod-backend.cabal+32−103
@@ -18,8 +18,19 @@ source-repository head
1818 location: https://github.com/typeable/octopod
1919
2020library
21− ghc-options: -Wall
22− -Werror
21+ ghc-options:
22+ -Weverything
23+ -Wno-implicit-prelude
24+ -Wno-missing-safe-haskell-mode
25+ -Wno-safe
26+ -Wno-prepositive-qualified-module
27+ -Wno-missing-import-lists
28+ -Wno-all-missed-specialisations
29+ -Wno-missing-local-signatures
30+ -Wno-partial-fields
31+ -Wno-unsafe
32+ -Wno-monomorphism-restriction
33+ -Wno-missed-specialisations
2334 default-extensions: BlockArguments
2435 , ConstraintKinds
2536 , DataKinds
@@ -42,6 +53,7 @@ library
4253 , RankNTypes
4354 , TypeFamilies
4455 , QuantifiedConstraints
56+ , DeriveAnyClass
4557 exposed-modules:
4658 Octopod.Server
4759 Octopod.Server.Args
@@ -49,10 +61,10 @@ library
4961 Octopod.Server.Logger
5062 Octopod.Server.Posix
5163 Orphans
52− TLS
5364 Types
5465 Database.PostgreSQL.Simple.Instances
5566 Control.Octopod.DeploymentLock
67+ Octopod.PowerAPI.Auth.Server
5668 hs-source-dirs:
5769 src
5870 build-depends:
@@ -62,17 +74,11 @@ library
6274 , bytestring
6375 , chronos
6476 , conduit
65− , connection
66− , data-default-class
6777 , deriving-aeson
68− , directory
6978 , octopod-common
7079 , fast-logger
71− , filepath
7280 , generic-lens
7381 , http-api-data
74− , http-client
75− , http-client-tls
7682 , lens
7783 , mtl
7884 , network-uri
@@ -83,120 +89,43 @@ library
8389 , postgresql-simple
8490 , resource-pool
8591 , servant
86− , servant-client
87− , servant-client-core
8892 , servant-server
8993 , servant-websockets
9094 , stm
91− , temporary
9295 , text
93− , tls
9496 , transformers
9597 , transformers-base
9698 , typed-process
9799 , unix
98100 , warp
99− , warp-tls
100101 , x509
101− , x509-store
102− , x509-validation
103102 , containers
104103 , monad-control
105104 , lifted-base
106105 , containers
106+ , servant-auth-server
107+ , wai
108+ , jose
107109 default-language: Haskell2010
108110
109111executable octopod-exe
110112 main-is: Main.hs
111− ghc-options: -Wall
112− -Werror
113+ ghc-options:
114+ -Weverything
115+ -Wno-implicit-prelude
116+ -Wno-missing-safe-haskell-mode
117+ -Wno-safe
118+ -Wno-prepositive-qualified-module
119+ -Wno-missing-import-lists
120+ -Wno-all-missed-specialisations
121+ -Wno-missing-local-signatures
122+ -Wno-partial-fields
123+ -Wno-unsafe
124+ -Wno-monomorphism-restriction
125+ -Wno-missed-specialisations
113126 hs-source-dirs: app
114127 ghc-options: -threaded -rtsopts -with-rtsopts=-N
115128 build-depends:
116− aeson
117− , async
118− , base >=4.7 && <5
119− , bytestring
120− , chronos
121− , conduit
122− , connection
123− , data-default-class
124− , directory
125− , octopod-backend
126− , fast-logger
127− , http-client
128− , http-client-tls
129− , mtl
130− , network-uri
131− , optparse-applicative
132− , optparse-generic
133− , postgresql-error-codes
134− , postgresql-simple
135− , resource-pool
136− , servant
137− , servant-client
138− , servant-client-core
139− , servant-server
140− , servant-websockets
141− , stm
142− , temporary
143− , text
144− , tls
145− , transformers
146− , transformers-base
147− , typed-process
148− , unix
149− , warp
150− , warp-tls
151− , x509
152− , x509-store
153− , x509-validation
154− default-language: Haskell2010
155−
156−test-suite octopod-test
157− type: exitcode-stdio-1.0
158− main-is: Spec.hs
159− hs-source-dirs:
160− test
161− ghc-options: -threaded -rtsopts -with-rtsopts=-N
162− build-depends:
163− aeson
164− , async
165− , base >=4.7 && <5
166− , bytestring
167− , chronos
168− , conduit
169− , connection
170− , data-default-class
171− , directory
129+ base >=4.7 && <5
172130 , octopod-backend
173− , fast-logger
174− , filepath
175− , http-client
176− , http-client-tls
177− , mtl
178− , network-uri
179− , optparse-applicative
180− , optparse-generic
181− , postgresql-error-codes
182− , postgresql-simple
183− , resource-pool
184− , servant
185− , servant-client
186− , servant-client-core
187− , servant-server
188− , servant-websockets
189− , stm
190− , temporary
191− , text
192− , tls
193− , transformers
194− , transformers-base
195− , typed-process
196− , unix
197− , warp
198− , warp-tls
199− , x509
200− , x509-store
201− , x509-validation
202131 default-language: Haskell2010
octopod-backend/src/Octopod/PowerAPI/Auth/Server.hsnew file+23
@@ -0,0 +1,23 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Octopod.PowerAPI.Auth.Server
4+ ( AuthHeader (..),
5+ )
6+where
7+
8+import Data.ByteString (ByteString)
9+import Network.Wai
10+import Octopod.PowerAPI
11+import Servant.Auth.Server
12+import Servant.Auth.Server.Internal.Class
13+
14+newtype AuthHeader = AuthHeader ByteString
15+
16+instance IsAuth AuthHeaderAuth () where
17+ type AuthArgs AuthHeaderAuth = '[AuthHeader]
18+ runAuth _ _ (AuthHeader h) = AuthCheck $ \req ->
19+ pure $ case lookup "Authorization" $ requestHeaders req of
20+ Just v | v == h -> Authenticated ()
21+ _ -> Indefinite
22+
23+deriving anyclass instance ToJWT ()
octopod-backend/src/Octopod/Server.hs+769−672
@@ -1,118 +1,119 @@
11module Octopod.Server (runOctopodServer) where
22
3−
4−import Chronos (Time, getTime, now)
5−import Control.Applicative
6−import Control.Concurrent (threadDelay)
7−import Control.Concurrent.Async (race_)
3+import Chronos (Time, getTime, now)
4+import Common.Validation (isNameValid)
5+import Control.Applicative
6+import Control.Concurrent (threadDelay)
7+import Control.Concurrent.Async (race_)
88import qualified Control.Concurrent.Lifted as L
9+import Control.Concurrent.MVar
10+import Control.Concurrent.STM
11+import Control.Exception (Exception, throwIO, try)
912import qualified Control.Exception.Lifted as L
10−import Control.Concurrent.MVar
11−import Control.Concurrent.STM
12−import Control.Exception (Exception, throwIO, try)
13−import Control.Lens hiding (pre)
14−import Control.Monad
15−import Control.Monad.Except
16−import Control.Monad.Reader
17−import Data.Aeson (Value(..), encode, toJSON)
18−import Data.ByteString (ByteString)
13+import Control.Lens hiding (Context, pre)
14+import Control.Lens.Extras
15+import Control.Monad
16+import Control.Monad.Base
17+import Control.Monad.Except
18+import Control.Monad.Reader
19+import Control.Monad.Trans.Control
20+import Control.Octopod.DeploymentLock
21+import Crypto.JOSE hiding (Context)
22+import Data.Aeson (Value (..), encode, toJSON)
23+import Data.ByteString (ByteString)
24+import qualified Data.ByteString.Char8 as BSC
1925import qualified Data.ByteString.Lazy as BSL
20−import Data.Coerce
21−import Data.Conduit (ConduitT, yield)
22−import Data.Foldable (foldrM)
23−import Data.Functor
24−import Data.Int (Int64)
25−import Data.IORef
26−import Data.Maybe
27−import Data.Pool
28−import Data.Text (lines, pack, unpack, unwords)
29−import Data.Traversable
30−import Database.PostgreSQL.Simple
31−import Database.PostgreSQL.Simple.Instances ()
32−import Network.Wai.Handler.Warp
33−import Network.Wai.Handler.WarpTLS
34−import Options.Generic
35−import PostgreSQL.ErrorCodes (unique_violation)
36−import Prelude hiding (lines, log, unlines, unwords)
37−import Servant
38−import System.Environment (lookupEnv)
39−import System.Exit
40−import System.Log.FastLogger
41−import System.Posix.Signals (sigTERM)
42−
43−
44−import Common.Validation (isNameValid)
45−import Control.Monad.Base
46−import Control.Monad.Trans.Control
47−import Control.Octopod.DeploymentLock
4826import qualified Data.ByteString.Lazy.Char8 as BSLC
49−import Data.Generics.Labels ()
27+import Data.Coerce
28+import Data.Conduit (ConduitT, yield)
29+import Data.Foldable (foldrM)
30+import Data.Functor
31+import Data.Generics.Labels ()
32+import Data.IORef
33+import Data.Int (Int64)
34+import Data.Maybe
35+import Data.Pool
36+import Data.Text (lines, pack, unpack, unwords)
5037import qualified Data.Text.Encoding as T
51−import Database.PostgreSQL.Simple.Transaction
52−import Octopod.API
53−import Octopod.PowerAPI
54−import Octopod.Server.Args
55−import Octopod.Server.ControlScriptUtils
56−import Octopod.Server.Logger
57−import Octopod.Server.Posix
58−import Orphans ()
59−import TLS (createTLSOpts)
60−import Types
61−import Control.Lens.Extras
38+import Data.Traversable
39+import Database.PostgreSQL.Simple hiding ((:.))
40+import Database.PostgreSQL.Simple.Instances ()
41+import Database.PostgreSQL.Simple.Transaction
42+import Network.Wai.Handler.Warp
43+import Octopod.API
44+import Octopod.PowerAPI
45+import Octopod.PowerAPI.Auth.Server
46+import Octopod.Server.Args
47+import Octopod.Server.ControlScriptUtils
48+import Octopod.Server.Logger
49+import Octopod.Server.Posix
50+import Options.Generic
51+import Orphans ()
52+import PostgreSQL.ErrorCodes (unique_violation)
53+import Servant
54+import Servant.Auth.Server
55+import System.Environment (lookupEnv)
56+import System.Exit
57+import System.Log.FastLogger
58+import System.Posix.Signals (sigTERM)
59+import Types
60+import Prelude hiding (lines, log, unlines, unwords)
6261
6362type PgPool = Pool Connection
64−type AppM = ReaderT AppState Handler
63+
64+type AppM = ReaderT AppState Handler
6565
6666-- | Octopod Server state definition.
6767data AppState = AppState
68− { pool :: PgPool
69− -- ^ postgres pool
70− , logger :: TimedFastLogger
71− -- ^ logger
72− , eventSink :: TChan WSEvent
73− -- ^ channel for WS events for the frontend
74− , bgWorkersCounter :: IORef Int
75− -- ^ background workers counter
76− , gracefulShutdownActivated :: IORef Bool
77− -- ^ flag of activating graceful shutdown
78− , shutdownSem :: MVar ()
79− -- ^ semaphore for graceful shutdown
80− , projectName :: ProjectName
81− -- ^ project name
82− , baseDomain :: Domain
83− -- ^ base domain
84− , namespace :: Namespace
85− -- ^ namespace
86− , archiveRetention :: ArchiveRetention
87− -- ^ archive retention
88− , statusUpdateTimeout :: Timeout
89− -- ^ status update timeout
90− , creationCommand :: Command
91− -- ^ creation command path
92− , updateCommand :: Command
93− -- ^ update command path
94− , archiveCommand :: Command
95− -- ^ deletion command path
96− , checkingCommand :: Command
97− -- ^ checking command path
98− , cleanupCommand :: Command
99− -- ^ cleanup command path
100− , archiveCheckingCommand :: Command
101− -- ^ archive checking command path
102− , tagCheckingCommand :: Command
103− -- ^ tag checking command path
68+ { -- | postgres pool
69+ pool :: PgPool
70+ , -- | logger
71+ logger :: TimedFastLogger
72+ , -- | channel for WS events for the frontend
73+ eventSink :: TChan WSEvent
74+ , -- | background workers counter
75+ bgWorkersCounter :: IORef Int
76+ , -- | flag of activating graceful shutdown
77+ gracefulShutdownActivated :: IORef Bool
78+ , -- | semaphore for graceful shutdown
79+ shutdownSem :: MVar ()
80+ , -- | project name
81+ projectName :: ProjectName
82+ , -- | base domain
83+ baseDomain :: Domain
84+ , -- | namespace
85+ namespace :: Namespace
86+ , -- | archive retention
87+ archiveRetention :: ArchiveRetention
88+ , -- | status update timeout
89+ statusUpdateTimeout :: Timeout
90+ , -- | creation command path
91+ creationCommand :: Command
92+ , -- | update command path
93+ updateCommand :: Command
94+ , -- | deletion command path
95+ archiveCommand :: Command
96+ , -- | checking command path
97+ checkingCommand :: Command
98+ , -- | cleanup command path
99+ cleanupCommand :: Command
100+ , -- | archive checking command path
101+ archiveCheckingCommand :: Command
102+ , -- | tag checking command path
103+ tagCheckingCommand :: Command
104104 , infoCommand :: Command
105105 , notificationCommand :: Maybe Command
106− , lockedDeployments :: LockedDeployments
107− -- ^ Deployments currently being processed which has not yet been
108− -- recorded in the database.
109− } deriving Generic
106+ , -- | Deployments currently being processed which has not yet been
107+ -- recorded in the database.
108+ lockedDeployments :: LockedDeployments
109+ }
110+ deriving stock (Generic)
110111
111112-- | Deployment exception definition.
112113data DeploymentException
113114 = DeploymentFailed Int
114115 | ThereIsActiveDeployment
115− deriving (Show)
116+ deriving stock (Show)
116117
117118instance Exception DeploymentException
118119
@@ -121,13 +122,13 @@ data DeploymentListType
121122 = AllDeployments
122123 | ArchivedOnlyDeployments
123124 | ActiveOnlyDeployments
124− deriving (Show)
125+ deriving stock (Show)
125126
126127-- | Full info list type definition.
127128data FullInfoListType
128129 = FullInfoForAll
129130 | FullInfoOnlyForOne DeploymentName
130− deriving (Show)
131+ deriving stock (Show)
131132
132133runOctopodServer :: IO ()
133134runOctopodServer = do
@@ -137,13 +138,11 @@ runOctopodServer = do
137138 gracefulShutdownAct <- newIORef False
138139 shutdownS <- newEmptyMVar
139140 void $ do
140− let
141− termHandler = terminationHandler bgWorkersC gracefulShutdownAct shutdownS
141+ let termHandler = terminationHandler bgWorkersC gracefulShutdownAct shutdownS
142142 installShutdownHandler logger' [sigTERM] termHandler
143143 opts <- parseArgs
144− let
145− a ?! e = a >>= maybe (die e) pure
146− getEnvOrDie eName = lookupEnv eName ?! (eName <> " is not set")
144+ let a ?! e = a >>= maybe (die e) pure
145+ getEnvOrDie eName = lookupEnv eName ?! (eName <> " is not set")
147146 projName <- coerce . pack <$> getEnvOrDie "PROJECT_NAME"
148147 domain <- coerce . pack <$> getEnvOrDie "BASE_DOMAIN"
149148 ns <- coerce . pack <$> getEnvOrDie "NAMESPACE"
@@ -157,55 +156,53 @@ runOctopodServer = do
157156 archiveCheckingCmd <- coerce . pack <$> getEnvOrDie "ARCHIVE_CHECKING_COMMAND"
158157 tagCheckingCmd <- coerce . pack <$> getEnvOrDie "TAG_CHECKING_COMMAND"
159158 infoCmd <- coerce . pack <$> getEnvOrDie "INFO_COMMAND"
160− notificationCmd <- (fmap . fmap) (Command . pack)
161− $ lookupEnv "NOTIFICATION_COMMAND" <&> \case
162− Just "" -> Nothing
163− x -> x
164− pgPool <- initConnectionPool
165− (unDBConnectionString $ octopodDB opts)
166− (unDBPoolSize $ octopodDBPoolSize opts)
159+ powerAuthorizationHeader <- AuthHeader . BSC.pack <$> getEnvOrDie "POWER_AUTHORIZATION_HEADER"
160+ notificationCmd <-
161+ (fmap . fmap) (Command . pack) $
162+ lookupEnv "NOTIFICATION_COMMAND" <&> \case
163+ Just "" -> Nothing
164+ x -> x
165+ pgPool <-
166+ initConnectionPool
167+ (unDBConnectionString $ octopodDB opts)
168+ (unDBPoolSize $ octopodDBPoolSize opts)
167169 channel <- liftIO . atomically $ newBroadcastTChan
168170 lockedDs <- initLockedDeployments
169− let
170− appSt =
171− AppState
172− pgPool
173− logger'
174− channel
175− bgWorkersC
176− gracefulShutdownAct
177− shutdownS
178− projName
179− domain
180− ns
181− archRetention
182− stUpdateTimeout
183− creationCmd
184− updateCmd
185− archiveCmd
186− checkingCmd
187− cleanupCmd
188− archiveCheckingCmd
189− tagCheckingCmd
190− infoCmd
191− notificationCmd
192− lockedDs
193− app' = app appSt
194− powerApp' = powerApp appSt
195− wsApp' = wsApp channel
196− serverPort = octopodPort opts
197− uiServerPort = unServerPort $ octopodUIPort opts
198− wsServerPort = unServerPort $ octopodWSPort opts
199− tlsOpts =
200− createTLSOpts (octopodTLSCertPath opts) (octopodTLSKeyPath opts)
201− (octopodTLSStorePath opts) serverPort
202− warpOpts = setPort (unServerPort serverPort) defaultSettings
203− in
204− (run uiServerPort app')
205− `race_` (runTLS tlsOpts warpOpts powerApp')
206− `race_` (run wsServerPort wsApp')
207− `race_` (runStatusUpdater appSt)
208− `race_` (runShutdownHandler appSt)
171+ let appSt =
172+ AppState
173+ pgPool
174+ logger'
175+ channel
176+ bgWorkersC
177+ gracefulShutdownAct
178+ shutdownS
179+ projName
180+ domain
181+ ns
182+ archRetention
183+ stUpdateTimeout
184+ creationCmd
185+ updateCmd
186+ archiveCmd
187+ checkingCmd
188+ cleanupCmd
189+ archiveCheckingCmd
190+ tagCheckingCmd
191+ infoCmd
192+ notificationCmd
193+ lockedDs
194+ app' = app appSt
195+ wsApp' = wsApp channel
196+ serverPort = octopodPort opts
197+ uiServerPort = unServerPort $ octopodUIPort opts
198+ powerServerPort = unServerPort serverPort
199+ wsServerPort = unServerPort $ octopodWSPort opts
200+ powerApp' <- powerApp powerAuthorizationHeader appSt
201+ (run uiServerPort app')
202+ `race_` (run powerServerPort powerApp')
203+ `race_` (run wsServerPort wsApp')
204+ `race_` (runStatusUpdater appSt)
205+ `race_` (runShutdownHandler appSt)
209206
210207-- | Initializes the connection pool.
211208initConnectionPool :: ByteString -> Int -> IO PgPool
@@ -226,21 +223,46 @@ app s = serve api $ hoistServer api (nt s) server
226223server :: ServerT API AppM
227224server =
228225 ( listH :<|> createH :<|> archiveH :<|> updateH
229− :<|> infoH :<|> fullInfoH :<|> statusH :<|> restoreH
230− ) :<|> pingH :<|> projectNameH
226+ :<|> infoH
227+ :<|> fullInfoH
228+ :<|> statusH
229+ :<|> restoreH
230+ )
231+ :<|> pingH
232+ :<|> projectNameH
231233
232234-- | Application with the octo CLI API.
233−powerApp :: AppState -> Application
234−powerApp s = serve api $ hoistServer api (nt s) powerServer
235+powerApp :: AuthHeader -> AppState -> IO Application
236+powerApp h s = do
237+ jwk' <- liftIO $ genJWK (RSAGenParam (4096 `div` 8))
238+ let ctx :: Context Ctx
239+ ctx =
240+ h
241+ :. (defaultJWTSettings jwk' :: JWTSettings)
242+ :. (defaultCookieSettings :: CookieSettings)
243+ :. EmptyContext
244+
245+ return $
246+ serveWithContext api ctx $
247+ hoistServerWithContext api (Proxy @Ctx) (nt s) powerServer
235248 where
236249 api = Proxy @PowerAPI
237250
251+type Ctx = '[AuthHeader, JWTSettings, CookieSettings]
252+
238253-- | Request handlers of application with the octo CLI API.
239254powerServer :: ServerT PowerAPI AppM
240−powerServer =
255+powerServer (Authenticated ()) =
241256 ( powerListH :<|> createH :<|> archiveH :<|> updateH
242− :<|> powerInfoH :<|> powerFullInfoH :<|> statusH :<|> cleanupH :<|> restoreH
243− ) :<|> getActionInfoH :<|> cleanArchiveH
257+ :<|> powerInfoH
258+ :<|> powerFullInfoH
259+ :<|> statusH
260+ :<|> cleanupH
261+ :<|> restoreH
262+ )
263+ :<|> getActionInfoH
264+ :<|> cleanArchiveH
265+powerServer _ = throwAll err401
244266
245267-- | Application with the WS API.
246268wsApp :: TChan WSEvent -> Application
@@ -269,16 +291,21 @@ powerListH :: AppM [DeploymentFullInfo]
269291powerListH = getFullInfo FullInfoForAll
270292
271293-- | Handles the 'full_info' request of the Web UI API.
272−fullInfoH
273− :: (MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m)
274− => DeploymentName -> m DeploymentFullInfo
294+fullInfoH ::
295+ (MonadReader AppState m, MonadBaseControl IO m, MonadError ServerError m) =>
296+ DeploymentName ->
297+ m DeploymentFullInfo
275298fullInfoH dName = do
276− fullInfoList <- hidePrivateOverridesInFullInfos <$>
277− getFullInfo (FullInfoOnlyForOne dName)
299+ fullInfoList <-
300+ hidePrivateOverridesInFullInfos
301+ <$> getFullInfo (FullInfoOnlyForOne dName)
278302 case fullInfoList of
279303 fullInfo : _ -> pure fullInfo
280− [] -> throwError err404
281− { errBody = validationError ["Name not found"] [] }
304+ [] ->
305+ throwError
306+ err404
307+ { errBody = validationError ["Name not found"] []
308+ }
282309
283310-- | Handles the 'full_info' request of the octo CLI API.
284311powerFullInfoH :: DeploymentName -> AppM DeploymentFullInfo
@@ -286,64 +313,66 @@ powerFullInfoH dName = do
286313 fullInfoList <- getFullInfo $ FullInfoOnlyForOne dName
287314 case fullInfoList of
288315 fullInfo : _ -> pure fullInfo
289− [] -> throwError err404
290− { errBody = validationError ["Name not found"] [] }
316+ [] ->
317+ throwError
318+ err404
319+ { errBody = validationError ["Name not found"] []
320+ }
291321
292322-- | Hides private overrides in 'full_info' response.
293323hidePrivateOverridesInFullInfos :: [DeploymentFullInfo] -> [DeploymentFullInfo]
294−hidePrivateOverridesInFullInfos dFullInfos = do
324+hidePrivateOverridesInFullInfos dFullInfos = do
295325 dFullInfos <&> \(DeploymentFullInfo dep s ct ut u) ->
296− let
297− hidePrivate (Deployment n t appOvs depOvs) =
298− Deployment n t (hideP appOvs) (hideP depOvs)
299− hideP o =
300− coerce o <&> \(Override k v vis) ->
301− let
302− v' = case vis of
303− Private -> "*"
304− Public -> v
305− in coerce $ Override k v' vis
306− in DeploymentFullInfo (hidePrivate dep) s ct ut u
307−
308−getFullInfo
309− :: (MonadReader AppState m, MonadBaseControl IO m)
310− => FullInfoListType
311− -> m [DeploymentFullInfo]
326+ let hidePrivate (Deployment n t appOvs depOvs) =
327+ Deployment n t (hideP appOvs) (hideP depOvs)
328+ hideP o =
329+ coerce o <&> \(Override k v vis) ->
330+ let v' = case vis of
331+ Private -> "*"
332+ Public -> v
333+ in coerce $ Override k v' vis
334+ in DeploymentFullInfo (hidePrivate dep) s ct ut u
335+
336+getFullInfo ::
337+ (MonadReader AppState m, MonadBaseControl IO m) =>
338+ FullInfoListType ->
339+ m [DeploymentFullInfo]
312340getFullInfo lType = do
313341 p <- asks pool
314342 liftBaseOp (withResource p) $ \conn -> getFullInfo' conn lType
315343
316344-- | Helper to get full_info from the database.
317−getFullInfo'
318− :: (MonadReader AppState m, MonadBase IO m)
319− => Connection
320− -> FullInfoListType
321− -> m [DeploymentFullInfo]
345+getFullInfo' ::
346+ (MonadReader AppState m, MonadBase IO m) =>
347+ Connection ->
348+ FullInfoListType ->
349+ m [DeploymentFullInfo]
322350getFullInfo' conn listType = do
323351 AppState {logger = l} <- ask
324352 deployments <- do
325353 rows <- case listType of
326354 FullInfoForAll -> liftBase $ query_ conn qAll
327− FullInfoOnlyForOne dName -> liftBase $ query conn qOne (Only dName)
355+ FullInfoOnlyForOne dName -> liftBase $ query conn qOne (Only dName)
328356 for rows $ \(n, t, ct, ut, st) -> do
329− (appOvs, depOvs) <- liftBase $ selectOverrides conn n
330− dMeta <- liftBase $ selectDeploymentMetadata conn n
331− st' <- isDeploymentLocked n <&> \case
332− True -> DeploymentPending st
333− False -> DeploymentNotPending st
357+ (appOvs, depOvs) <- liftBase $ selectOverrides conn n
358+ dMeta <- liftBase $ selectDeploymentMetadata conn n
359+ st' <-
360+ isDeploymentLocked n <&> \case
361+ True -> DeploymentPending st
362+ False -> DeploymentNotPending st
334363 pure $ do
335364 let dep = Deployment n t appOvs depOvs
336365 DeploymentFullInfo dep st' dMeta ct ut
337366 liftBase . logInfo l $ "get deployments: " <> (pack . show $ deployments)
338367 return deployments
339368 where
340− qAll =
369+ qAll =
341370 "SELECT name, tag, extract(epoch from created_at)::int, \
342− \extract(epoch from updated_at)::int, status \
371+ \extract(epoch from updated_at)::int, status \
343372 \FROM deployments ORDER BY name"
344− qOne =
373+ qOne =
345374 "SELECT name, tag, extract(epoch from created_at)::int, \
346− \extract(epoch from updated_at)::int, status \
375+ \extract(epoch from updated_at)::int, status \
347376 \FROM deployments \
348377 \WHERE name = ?"
349378
@@ -352,39 +381,46 @@ createH :: Deployment -> AppM CommandResponse
352381createH dep = do
353382 failIfGracefulShutdownActivated
354383 unless (isNameValid $ name dep) $ do
355− let
356− badNameText =
357− "Deployment name length should be longer than 2 characters, \
358− \under 17 characters and begin with a letter."
359− throwError err400
360− { errBody = validationError [badNameText] [] }
384+ let badNameText =
385+ "Deployment name length should be longer than 2 characters, \
386+ \under 17 characters and begin with a letter."
387+ throwError
388+ err400
389+ { errBody = validationError [badNameText] []
390+ }
361391 t1 <- liftIO $ now
362392 st <- ask
363393 let pgPool = pool st
364394 failIfImageNotFound (name dep) (tag dep)
365395 failIfGracefulShutdownActivated
366− runDeploymentBgWorker Nothing (name dep)
396+ runDeploymentBgWorker
397+ Nothing
398+ (name dep)
367399 ( do
368− let
369− q =
370− "INSERT INTO deployments (name, tag, status) \
371− \VALUES (?, ?, ?) RETURNING id"
372− createDep :: PgPool -> Deployment -> IO [Only Int]
373− createDep p Deployment { name = n, tag = t } =
374− withResource p $ \conn ->
375− query conn q (n, t, CreatePending)
376− res :: Either SqlError [Only Int] <- liftIO . try $ createDep pgPool dep
377− case res of
378− Right ((Only depId) : _) ->
379− pure . DeploymentId $ depId
380− Right [] ->
381− throwError err404
382− { errBody = validationError ["Name not found"] [] }
383− Left (SqlError code _ _ _ _) | code == unique_violation ->
384− throwError err400
385− { errBody = validationError ["Deployment already exists"] [] }
386− Left (SqlError _ _ _ _ _) ->
387− throwError err409 { errBody = appError "Some database error" }
400+ let q =
401+ "INSERT INTO deployments (name, tag, status) \
402+ \VALUES (?, ?, ?) RETURNING id"
403+ createDep :: PgPool -> Deployment -> IO [Only Int]
404+ createDep p Deployment {name = n, tag = t} =
405+ withResource p $ \conn ->
406+ query conn q (n, t, CreatePending)
407+ res :: Either SqlError [Only Int] <- liftIO . try $ createDep pgPool dep
408+ case res of
409+ Right ((Only depId) : _) ->
410+ pure . DeploymentId $ depId
411+ Right [] ->
412+ throwError
413+ err404
414+ { errBody = validationError ["Name not found"] []
415+ }
416+ Left (SqlError code _ _ _ _)
417+ | code == unique_violation ->
418+ throwError
419+ err400
420+ { errBody = validationError ["Deployment already exists"] []
421+ }
422+ Left (SqlError _ _ _ _ _) ->
423+ throwError err409 {errBody = appError "Some database error"}
388424 )
389425 $ \dId -> do
390426 liftIO . withResource pgPool $ \conn ->
@@ -393,9 +429,8 @@ createH dep = do
393429 liftBase $ updateDeploymentInfo (name dep) st
394430 (ec, out, err) <- liftBase $ createDeployment dep st
395431 t2 <- liftBase $ now
396− let
397− elTime = elapsedTime t2 t1
398− liftIO . withResource pgPool $ \conn ->
432+ let elTime = elapsedTime t2 t1
433+ withResource pgPool $ \conn ->
399434 -- calling it directly now is fine since there is no previous status.
400435 createDeploymentLog conn dep "create" ec elTime out err
401436 liftBase $ sendReloadEvent st
@@ -405,10 +440,9 @@ createH dep = do
405440-- | Updates deployment info.
406441updateDeploymentInfo :: DeploymentName -> AppState -> IO ()
407442updateDeploymentInfo dName st = do
408− let
409− log = logWarning (logger st)
410− args = infoCommandArgs (projectName st) (baseDomain st) (namespace st) dName
411− cmd = coerce $ infoCommand st
443+ let log = logWarning (logger st)
444+ args = infoCommandArgs (projectName st) (baseDomain st) (namespace st) dName
445+ cmd = coerce $ infoCommand st
412446 liftIO $ do
413447 (ec, out, err) <- runCommand (unpack cmd) (coerce args)
414448 case ec of
@@ -418,23 +452,31 @@ updateDeploymentInfo dName st = do
418452 ExitFailure _ ->
419453 log $
420454 "could not get deployment info, exit code: " <> (pack . show $ ec)
421− <> ", stdout: " <> coerce out <> "stderr: " <> coerce err
455+ <> ", stdout: "
456+ <> coerce out
457+ <> "stderr: "
458+ <> coerce err
422459
423460-- | Helper to create a new deployment.
424461createDeployment :: Deployment -> AppState -> IO (ExitCode, Stdout, Stderr)
425462createDeployment dep st = do
426− let
427− log :: Text -> IO ()
428− log = logInfo (logger st)
429− args =
430− [ "--project-name", coerce $ projectName st
431− , "--base-domain", coerce $ baseDomain st
432− , "--namespace", coerce $ namespace st
433− , "--name", coerce $ name dep
434− , "--tag", coerce $ tag dep
435− ] ++ applicationOverridesToArgs (appOverrides dep)
436− ++ deploymentOverridesToArgs (deploymentOverrides dep)
437− cmd = coerce $ creationCommand st
463+ let log :: Text -> IO ()
464+ log = logInfo (logger st)
465+ args =
466+ [ "--project-name"
467+ , coerce $ projectName st
468+ , "--base-domain"
469+ , coerce $ baseDomain st
470+ , "--namespace"
471+ , coerce $ namespace st
472+ , "--name"
473+ , coerce $ name dep
474+ , "--tag"
475+ , coerce $ tag dep
476+ ]
477+ ++ applicationOverridesToArgs (appOverrides dep)
478+ ++ deploymentOverridesToArgs (deploymentOverrides dep)
479+ cmd = coerce $ creationCommand st
438480
439481 liftIO $ do
440482 log $ "call " <> unwords (cmd : args)
@@ -448,7 +490,7 @@ applicationOverrideToArg o = ["--app-env-override", overrideToArg . coerce $ o]
448490
449491-- | Helper to convert an application-level override to command arguments.
450492applicationOverridesToArgs :: ApplicationOverrides -> [Text]
451−applicationOverridesToArgs ovs = concat [applicationOverrideToArg o | o <- ovs ]
493+applicationOverridesToArgs ovs = concat [applicationOverrideToArg o | o <- ovs]
452494
453495-- | Converts a deployment-level override list to command arguments.
454496deploymentOverrideToArg :: DeploymentOverride -> [Text]
@@ -460,19 +502,18 @@ deploymentOverridesToArgs :: DeploymentOverrides -> [Text]
460502deploymentOverridesToArgs ovs = concat [deploymentOverrideToArg o | o <- ovs]
461503
462504-- | Helper to get deployment logs.
463−selectDeploymentLogs
464− :: PgPool
465− -> DeploymentId
466− -> IO [DeploymentLog]
505+selectDeploymentLogs ::
506+ PgPool ->
507+ DeploymentId ->
508+ IO [DeploymentLog]
467509selectDeploymentLogs pgPool dId = do
468− let
469− q =
470− "SELECT id, action::text, tag, exit_code, \
510+ let q =
511+ "SELECT id, action::text, tag, exit_code, \
471512 \duration, extract(epoch from created_at)::int \
472− \FROM deployment_logs \
473− \WHERE deployment_id = ? \
474− \ORDER BY created_at DESC \
475− \LIMIT 20"
513+ \FROM deployment_logs \
514+ \WHERE deployment_id = ? \
515+ \ORDER BY created_at DESC \
516+ \LIMIT 20"
476517 withResource pgPool $ \conn -> do
477518 rows <- query conn q (Only . unDeploymentId $ dId)
478519 -- FIXME: use FromRow instance instead
@@ -481,9 +522,9 @@ selectDeploymentLogs pgPool dId = do
481522 pure $ DeploymentLog (ActionId ai) a t appOvs depOvs ec (Duration d) ts
482523
483524-- | Helper to get a deployment.
484−selectDeployment
485− :: DeploymentName
486− -> AppM Deployment
525+selectDeployment ::
526+ DeploymentName ->
527+ AppM Deployment
487528selectDeployment dName = do
488529 pgPool <- asks pool
489530 let q = "SELECT name, tag FROM deployments WHERE name = ?"
@@ -501,7 +542,8 @@ data StatusTransitionProcessOutput = StatusTransitionProcessOutput
501542 , duration :: Duration
502543 , stdout :: Stdout
503544 , stderr :: Stderr
504− } deriving (Generic, Show)
545+ }
546+ deriving stock (Generic, Show)
505547
506548data DeploymentStatusTransition
507549 = TransitionArchived
@@ -512,7 +554,7 @@ data DeploymentStatusTransition
512554 | TransitionUpdatePending StatusTransitionProcessOutput
513555 | TransitionCreatePending StatusTransitionProcessOutput
514556 | TransitionFailure FailureType
515− deriving Show
557+ deriving stock (Show)
516558
517559transitionStatus :: DeploymentStatusTransition -> DeploymentStatus
518560transitionStatus TransitionArchived {} = Archived
@@ -534,107 +576,125 @@ processOutput (TransitionUpdatePending x) = Just (x, Action "update")
534576processOutput (TransitionCreatePending x) = Just (x, Action "create")
535577processOutput TransitionFailure {} = Nothing
536578
537−transitionToStatusS
538− :: (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m)
539− => DeploymentName -> DeploymentStatusTransition -> m ()
579+transitionToStatusS ::
580+ (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m, MonadIO m) =>
581+ DeploymentName ->
582+ DeploymentStatusTransition ->
583+ m ()
540584transitionToStatusS dName tran =
541585 runExceptT (transitionToStatus dName tran) >>= transitionErrorToServerError
542586
543587transitionErrorToServerError :: MonadError ServerError m => Either StatusTransitionError a -> m a
544588transitionErrorToServerError = \case
545589 Right x -> return x
546− Left (InvalidStatusTransition dName a b) -> throwError err409
547− { errBody
548− = "Unable to transition deployment "
549− <> (BSL.fromStrict . T.encodeUtf8 . unDeploymentName) dName
550− <> " from " <> show'' a <> " to " <> show'' b <> "."
551− }
552− Left (DeploymentNotFound dName) -> throwError err404
553− { errBody
554− = "Couldn't find deployment "
555− <> (BSL.fromStrict . T.encodeUtf8 . unDeploymentName) dName <> "."
556− }
590+ Left (InvalidStatusTransition dName a b) ->
591+ throwError
592+ err409
593+ { errBody =
594+ "Unable to transition deployment "
595+ <> (BSL.fromStrict . T.encodeUtf8 . unDeploymentName) dName
596+ <> " from "
597+ <> show'' a
598+ <> " to "
599+ <> show'' b
600+ <> "."
601+ }
602+ Left (DeploymentNotFound dName) ->
603+ throwError
604+ err404
605+ { errBody =
606+ "Couldn't find deployment "
607+ <> (BSL.fromStrict . T.encodeUtf8 . unDeploymentName) dName
608+ <> "."
609+ }
557610 where
558611 show'' = BSLC.pack . show
559612
560−getDeploymentS
561− :: (MonadReader AppState m, MonadBase IO m, MonadError ServerError m)
562− => Connection -> DeploymentName -> m DeploymentFullInfo
563−getDeploymentS conn dName = (getFullInfo' conn (FullInfoOnlyForOne dName)) >>= \case
564− [x] -> return x
565− _ -> throwError err404 {errBody = "Deployment not found."}
613+getDeploymentS ::
614+ (MonadReader AppState m, MonadBase IO m, MonadError ServerError m) =>
615+ Connection ->
616+ DeploymentName ->
617+ m DeploymentFullInfo
618+getDeploymentS conn dName =
619+ (getFullInfo' conn (FullInfoOnlyForOne dName)) >>= \case
620+ [x] -> return x
621+ _ -> throwError err404 {errBody = "Deployment not found."}
566622
567−transitionToStatus
568− :: (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m)
569− => DeploymentName -> DeploymentStatusTransition -> ExceptT StatusTransitionError m ()
623+transitionToStatus ::
624+ (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m, MonadIO m) =>
625+ DeploymentName ->
626+ DeploymentStatusTransition ->
627+ ExceptT StatusTransitionError m ()
570628transitionToStatus dName s = do
571629 p <- asks pool
572630 st <- ask
573631 let log = liftBase . logInfo (logger st)
574632 (oldS, newS, dep :: DeploymentFullInfo) <- withResource p $ \conn -> liftBaseOp_ (withTransaction conn) $ do
575633 dep <- lift $ getDeploymentS conn dName
576− let
577− oldS = recordedStatus $ dep ^. #status
578− newS = transitionStatus s
634+ let oldS = recordedStatus $ dep ^. #status
635+ newS = transitionStatus s
579636 assertStatusTransitionPossible dName oldS newS
580637 log $ "Transitioning deployment " <> (show' . unDeploymentName) dName <> " " <> show' s
581− let
582− q =
583− "UPDATE deployments \
584− \SET status = ?, status_updated_at = now() "
585− <> (if newS == ArchivePending then ", archived_at = now()" else mempty)
586− <> " WHERE name = ?"
638+ let q =
639+ "UPDATE deployments \
640+ \SET status = ?, status_updated_at = now() "
641+ <> (if newS == ArchivePending then ", archived_at = now()" else mempty)
642+ <> " WHERE name = ?"
587643 void . liftBase $ execute conn q (newS, dName)
588− liftBase $ forM_ (processOutput s) $ \(output, act) ->
589− createDeploymentLog
590− conn
591− (dep ^. #deployment)
592− act
593− (output ^. #exitCode)
594− (output ^. #duration)
595− (output ^. #stdout)
596− (output ^. #stderr)
644+ lift $
645+ forM_ (processOutput s) $ \(output, act) ->
646+ createDeploymentLog
647+ conn
648+ (dep ^. #deployment)
649+ act
650+ (output ^. #exitCode)
651+ (output ^. #duration)
652+ (output ^. #stdout)
653+ (output ^. #stderr)
597654 return (oldS, newS, dep)
598655 notificationCmd <- asks notificationCommand
599656 forM_ notificationCmd $ \nCmd ->
600− runBgWorker . void $ runCommandArgs' nCmd
601− =<< notificationCommandArgs dName (dep ^. #deployment . #tag) oldS newS
657+ runBgWorker . void $
658+ runCommandArgs' nCmd
659+ =<< notificationCommandArgs dName (dep ^. #deployment . #tag) oldS newS
602660 liftBase $ sendReloadEvent st
603661
604−assertStatusTransitionPossible
605− :: MonadError StatusTransitionError m
606− => DeploymentName
607− -> DeploymentStatus
608− -> DeploymentStatus
609− -> m ()
662+assertStatusTransitionPossible ::
663+ MonadError StatusTransitionError m =>
664+ DeploymentName ->
665+ DeploymentStatus ->
666+ DeploymentStatus ->
667+ m ()
610668assertStatusTransitionPossible dName old new =
611669 unless
612670 (possibleTransitions old new)
613671 (throwError $ InvalidStatusTransition dName old new)
614672
615−assertDeploymentTransitionPossible
616− :: (MonadError StatusTransitionError m, MonadReader AppState m, MonadBaseControl IO m)
617− => DeploymentName
618− -> DeploymentStatus
619− -> m ()
673+assertDeploymentTransitionPossible ::
674+ (MonadError StatusTransitionError m, MonadReader AppState m, MonadBaseControl IO m) =>
675+ DeploymentName ->
676+ DeploymentStatus ->
677+ m ()
620678assertDeploymentTransitionPossible dName new = do
621679 p <- asks pool
622− dep <- withResource p $ \conn -> (getFullInfo' conn (FullInfoOnlyForOne dName)) >>= \case
623− [x] -> return x
624− _ -> throwError $ DeploymentNotFound dName
680+ dep <- withResource p $ \conn ->
681+ (getFullInfo' conn (FullInfoOnlyForOne dName)) >>= \case
682+ [x] -> return x
683+ _ -> throwError $ DeploymentNotFound dName
625684 assertStatusTransitionPossible dName (recordedStatus $ dep ^. #status) new
626685
627−assertDeploymentTransitionPossibleS
628− :: (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m)
629− => DeploymentName
630− -> DeploymentStatus
631− -> m ()
686+assertDeploymentTransitionPossibleS ::
687+ (MonadError ServerError m, MonadReader AppState m, MonadBaseControl IO m) =>
688+ DeploymentName ->
689+ DeploymentStatus ->
690+ m ()
632691assertDeploymentTransitionPossibleS dName new =
633692 runExceptT (assertDeploymentTransitionPossible dName new) >>= transitionErrorToServerError
693+
634694data StatusTransitionError
635695 = InvalidStatusTransition DeploymentName DeploymentStatus DeploymentStatus
636696 | DeploymentNotFound DeploymentName
637− deriving Show
697+ deriving stock (Show)
638698
639699possibleTransitions :: DeploymentStatus -> DeploymentStatus -> Bool
640700possibleTransitions CreatePending = anyPred [is #_Running, is #_Failure]
@@ -653,26 +713,31 @@ archiveH dName = do
653713 failIfGracefulShutdownActivated
654714 t1 <- liftBase now
655715 st <- ask
656− let
657− log = liftBase . logInfo (logger st)
658− args =
659− [ "--project-name", coerce $ projectName st
660− , "--base-domain", coerce $ baseDomain st
661− , "--namespace", coerce $ namespace st
662− , "--name", coerce dName
663− ]
664− cmd = coerce $ archiveCommand st
716+ let log = liftBase . logInfo (logger st)
717+ args =
718+ [ "--project-name"
719+ , coerce $ projectName st
720+ , "--base-domain"
721+ , coerce $ baseDomain st
722+ , "--namespace"
723+ , coerce $ namespace st
724+ , "--name"
725+ , coerce dName
726+ ]
727+ cmd = coerce $ archiveCommand st
665728 runDeploymentBgWorker (Just ArchivePending) dName (pure ()) $ \() -> do
666729 log $ "call " <> unwords (cmd : args)
667730 (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args)
668731 t2 <- liftBase now
669732 let elTime = elapsedTime t2 t1
670− transitionToStatusS dName $ TransitionArchivePending StatusTransitionProcessOutput
671− { exitCode = ec
672− , duration = elTime
673− , stdout = out
674− , stderr = err
675− }
733+ transitionToStatusS dName $
734+ TransitionArchivePending
735+ StatusTransitionProcessOutput
736+ { exitCode = ec
737+ , duration = elTime
738+ , stdout = out
739+ , stderr = err
740+ }
676741 handleExitCode ec
677742 pure Success
678743
@@ -682,16 +747,15 @@ updateH dName dUpdate = do
682747 failIfGracefulShutdownActivated
683748 t1 <- liftIO $ now
684749 st <- ask
685− let
686− DeploymentUpdate
687− { newTag = dTag
688− , newAppOverrides = newAppOvs
689− , oldAppOverrides = oldAppOvs
690− , newDeploymentOverrides = newDepOvs
691− , oldDeploymentOverrides = oldDepOvs
692− } = dUpdate
693− pgPool = pool st
694− log = logInfo (logger st)
750+ let DeploymentUpdate
751+ { newTag = dTag
752+ , newAppOverrides = newAppOvs
753+ , oldAppOverrides = oldAppOvs
754+ , newDeploymentOverrides = newDepOvs
755+ , oldDeploymentOverrides = oldDepOvs
756+ } = dUpdate
757+ pgPool = pool st
758+ log = logInfo (logger st)
695759 dId <- selectDeploymentId pgPool dName
696760 failIfImageNotFound dName dTag
697761 failIfGracefulShutdownActivated
@@ -704,157 +768,169 @@ updateH dName dUpdate = do
704768 selectOverrides conn dName
705769 liftBase $ updateDeploymentInfo dName st
706770 liftBase $ sendReloadEvent st
707− let
708− args =
709− [ "--project-name", coerce $ projectName st
710− , "--base-domain", coerce $ baseDomain st
711− , "--namespace", coerce $ namespace st
712− , "--name", coerce $ dName
713− , "--tag", coerce $ dTag
714− ] ++ applicationOverridesToArgs appOvs
715− ++ deploymentOverridesToArgs depOvs
716− cmd = coerce $ updateCommand st
771+ let args =
772+ [ "--project-name"
773+ , coerce $ projectName st
774+ , "--base-domain"
775+ , coerce $ baseDomain st
776+ , "--namespace"
777+ , coerce $ namespace st
778+ , "--name"
779+ , coerce $ dName
780+ , "--tag"
781+ , coerce $ dTag
782+ ]
783+ ++ applicationOverridesToArgs appOvs
784+ ++ deploymentOverridesToArgs depOvs
785+ cmd = coerce $ updateCommand st
717786 liftBase . log $ "call " <> unwords (cmd : args)
718787 (ec, out, err) <- liftBase $ runCommand (unpack cmd) (unpack <$> args)
719− liftBase . log $ "deployment updated, name: "
720− <> coerce dName <> ", tag: " <> coerce dTag
788+ liftBase . log $
789+ "deployment updated, name: "
790+ <> coerce dName
791+ <> ", tag: "
792+ <> coerce dTag
721793 t2 <- liftBase now
722794 let elTime = elapsedTime t2 t1
723− transitionToStatusS dName $ TransitionUpdatePending StatusTransitionProcessOutput
724− { exitCode = ec
725− , duration = elTime
726− , stdout = out
727− , stderr = err
728− }
795+ transitionToStatusS dName $
796+ TransitionUpdatePending
797+ StatusTransitionProcessOutput
798+ { exitCode = ec
799+ , duration = elTime
800+ , stdout = out
801+ , stderr = err
802+ }
729803 handleExitCode ec
730804 return Success
731805
732806-- | Helper to get overrides from the database.
733−selectOverrides
734− :: Connection
735− -> DeploymentName
736− -> IO (ApplicationOverrides, DeploymentOverrides)
807+selectOverrides ::
808+ Connection ->
809+ DeploymentName ->
810+ IO (ApplicationOverrides, DeploymentOverrides)
737811selectOverrides conn dName = do
738− let
739− q =
740− "SELECT key, value, scope::text, visibility::text \
741− \FROM deployment_overrides \
742− \WHERE deployment_id = ( \
812+ let q =
813+ "SELECT key, value, scope::text, visibility::text \
814+ \FROM deployment_overrides \
815+ \WHERE deployment_id = ( \
743816 \SELECT id FROM deployments WHERE name = ? \
744− \)"
745− parseVis :: Text -> OverrideVisibility
746− parseVis = read . unpack
747− parseScope :: Text -> OverrideScope
748− parseScope = read . unpack
749− toOverrides (k, v, s, vis) (appOvs, depOvs) =
750− pure $ case parseScope s of
751− ApplicationScope ->
752− (ApplicationOverride (Override k v $ parseVis vis) : appOvs, depOvs)
753− DeploymentScope ->
754− (appOvs, DeploymentOverride (Override k v $ parseVis vis) : depOvs)
817+ \)"
818+ parseVis :: Text -> OverrideVisibility
819+ parseVis = read . unpack
820+ parseScope :: Text -> OverrideScope
821+ parseScope = read . unpack
822+ toOverrides (k, v, s, vis) (appOvs, depOvs) =
823+ pure $ case parseScope s of
824+ ApplicationScope ->
825+ (ApplicationOverride (Override k v $ parseVis vis) : appOvs, depOvs)
826+ DeploymentScope ->
827+ (appOvs, DeploymentOverride (Override k v $ parseVis vis) : depOvs)
755828 rows <- query conn q (Only dName)
756829 foldrM toOverrides ([], []) rows
757830
758831-- | Helper to get override logs from the database.
759−selectLogOverrides
760− :: Connection
761− -> ActionId
762− -> IO (ApplicationOverrides, DeploymentOverrides)
832+selectLogOverrides ::
833+ Connection ->
834+ ActionId ->
835+ IO (ApplicationOverrides, DeploymentOverrides)
763836selectLogOverrides conn aId = do
764− let
765− q =
766− "SELECT key, value, scope::text, visibility::text \
767− \FROM deployment_log_overrides \
768− \WHERE deployment_log_id = ?"
769− parseVis :: Text -> OverrideVisibility
770− parseVis = read . unpack
771− parseScope :: Text -> OverrideScope
772− parseScope = read . unpack
773− toOverrides (k, v, s, vis) (appOvs, depOvs) =
774− pure $ case parseScope s of
775− ApplicationScope ->
776− (ApplicationOverride (Override k v $ parseVis vis) : appOvs, depOvs)
777− DeploymentScope ->
778− (appOvs, DeploymentOverride (Override k v $ parseVis vis) : depOvs)
837+ let q =
838+ "SELECT key, value, scope::text, visibility::text \
839+ \FROM deployment_log_overrides \
840+ \WHERE deployment_log_id = ?"
841+ parseVis :: Text -> OverrideVisibility
842+ parseVis = read . unpack
843+ parseScope :: Text -> OverrideScope
844+ parseScope = read . unpack
845+ toOverrides (k, v, s, vis) (appOvs, depOvs) =
846+ pure $ case parseScope s of
847+ ApplicationScope ->
848+ (ApplicationOverride (Override k v $ parseVis vis) : appOvs, depOvs)
849+ DeploymentScope ->
850+ (appOvs, DeploymentOverride (Override k v $ parseVis vis) : depOvs)
779851 rows <- query conn q (Only . unActionId $ aId)
780852 foldrM toOverrides ([], []) rows
781853
782854-- | Helper to delete overrides from the database.
783−deleteOldOverrides
784− :: Connection
785− -> DeploymentId
786− -> ApplicationOverrides
787− -> DeploymentOverrides
788− -> IO ()
855+deleteOldOverrides ::
856+ Connection ->
857+ DeploymentId ->
858+ ApplicationOverrides ->
859+ DeploymentOverrides ->
860+ IO ()
789861deleteOldOverrides conn dId appOvs depOvs = do
790− let
791− q =
792− "DELETE FROM deployment_overrides \
793− \WHERE deployment_id = ? AND key = ? AND scope = ?"
794− dId' = unDeploymentId dId
795− void $ for appOvs $ \o -> do
796− let
797− oKey = overrideKey . unApplicationOverride $ o
798− oScope = show ApplicationScope
799− execute conn q (dId', oKey, oScope)
800− void $ for depOvs $ \o -> do
801− let
802− oKey = overrideKey . unDeploymentOverride $ o
803− oScope = show DeploymentScope
804− execute conn q (dId', oKey, oScope)
862+ let q =
863+ "DELETE FROM deployment_overrides \
864+ \WHERE deployment_id = ? AND key = ? AND scope = ?"
865+ dId' = unDeploymentId dId
866+ void $
867+ for appOvs $ \o -> do
868+ let oKey = overrideKey . unApplicationOverride $ o
869+ oScope = show ApplicationScope
870+ execute conn q (dId', oKey, oScope)
871+ void $
872+ for depOvs $ \o -> do
873+ let oKey = overrideKey . unDeploymentOverride $ o
874+ oScope = show DeploymentScope
875+ execute conn q (dId', oKey, oScope)
805876
806877-- | Helper to get the deployment id from the database.
807878selectDeploymentId :: PgPool -> DeploymentName -> AppM DeploymentId
808879selectDeploymentId pgPool dName = do
809− dIds :: [(Only Int)] <- liftIO $ withResource pgPool $ \conn ->
810− query conn "SELECT id FROM deployments WHERE name = ?" (Only dName)
880+ dIds :: [(Only Int)] <- liftIO $
881+ withResource pgPool $ \conn ->
882+ query conn "SELECT id FROM deployments WHERE name = ?" (Only dName)
811883 case dIds of
812884 [(Only dId)] -> pure . DeploymentId $ dId
813− [] -> throwError err404
814− { errBody = validationError ["Name not found"] [] }
815− _ -> throwError err406
816− { errBody = validationError ["More than one name found"] [] }
885+ [] ->
886+ throwError
887+ err404
888+ { errBody = validationError ["Name not found"] []
889+ }
890+ _ ->
891+ throwError
892+ err406
893+ { errBody = validationError ["More than one name found"] []
894+ }
817895
818896-- | Helper to insert or update overrides.
819−upsertNewOverrides
820− :: Connection
821− -> DeploymentId
822− -> ApplicationOverrides
823− -> DeploymentOverrides
824− -> IO ()
897+upsertNewOverrides ::
898+ Connection ->
899+ DeploymentId ->
900+ ApplicationOverrides ->
901+ DeploymentOverrides ->
902+ IO ()
825903upsertNewOverrides conn dId appOvs depOvs = do
826− let
827− q =
828− "INSERT INTO deployment_overrides \
829− \(key, value, deployment_id, scope, visibility) \
830− \VALUES (?, ?, ?, ?, ?) \
831− \ON CONFLICT (key, deployment_id, scope) \
832− \DO \
904+ let q =
905+ "INSERT INTO deployment_overrides \
906+ \(key, value, deployment_id, scope, visibility) \
907+ \VALUES (?, ?, ?, ?, ?) \
908+ \ON CONFLICT (key, deployment_id, scope) \
909+ \DO \
833910 \UPDATE SET value = ?, visibility = ?, updated_at = now()"
834− dId' = unDeploymentId dId
835− void $ for appOvs $ \o -> do
836− let
837− oKey = overrideKey . unApplicationOverride $ o
838− oValue = overrideValue . unApplicationOverride $ o
839− oScope = show ApplicationScope
840− oVis = show . overrideVisibility . unApplicationOverride $ o
841− execute conn q (oKey, oValue, dId', oScope, oVis, oValue, oVis)
842− void $ for depOvs $ \o -> do
843− let
844− oKey = overrideKey . unDeploymentOverride $ o
845− oValue = overrideValue . unDeploymentOverride $ o
846− oScope = show DeploymentScope
847− oVis = show . overrideVisibility . unDeploymentOverride $ o
848− execute conn q (oKey, oValue, dId', oScope, oVis, oValue, oVis)
849−
850−updateTag
851− :: Connection
852− -> DeploymentId
853− -> DeploymentTag
854− -> IO ()
911+ dId' = unDeploymentId dId
912+ void $
913+ for appOvs $ \o -> do
914+ let oKey = overrideKey . unApplicationOverride $ o
915+ oValue = overrideValue . unApplicationOverride $ o
916+ oScope = show ApplicationScope
917+ oVis = show . overrideVisibility . unApplicationOverride $ o
918+ execute conn q (oKey, oValue, dId', oScope, oVis, oValue, oVis)
919+ void $
920+ for depOvs $ \o -> do
921+ let oKey = overrideKey . unDeploymentOverride $ o
922+ oValue = overrideValue . unDeploymentOverride $ o
923+ oScope = show DeploymentScope
924+ oVis = show . overrideVisibility . unDeploymentOverride $ o
925+ execute conn q (oKey, oValue, dId', oScope, oVis, oValue, oVis)
926+
927+updateTag ::
928+ Connection ->
929+ DeploymentId ->
930+ DeploymentTag ->
931+ IO ()
855932updateTag conn (DeploymentId dId) (DeploymentTag dTag) = do
856− let
857− q = "UPDATE deployments SET tag=? WHERE id=?;"
933+ let q = "UPDATE deployments SET tag=? WHERE id=?;"
858934 void $ execute conn q (dTag, dId)
859935
860936-- | Handles the 'info' request of the Web UI API.
@@ -878,19 +954,19 @@ powerInfoH dName = do
878954-- | Hides private overrides of 'info' response.
879955hidePrivateOverridesInInfo :: DeploymentInfo -> DeploymentInfo
880956hidePrivateOverridesInInfo (DeploymentInfo dep dMeta dLogs) =
881− let
882− dep' =
883− let (Deployment n t ao so) = dep
884− in Deployment n t (hideP ao) (hideP so)
885− dLogs' = dLogs <&> \(DeploymentLog ai a t ao so ec d ct) ->
886− DeploymentLog ai a t (hideP ao) (hideP so) ec d ct
887− hideP o = coerce o <&> \(Override k v vis) ->
888− let
889− v' = case vis of
890− Private -> "*"
891− Public -> v
892− in coerce $ Override k v' vis
893− in DeploymentInfo dep' dMeta dLogs'
957+ let dep' =
958+ let (Deployment n t ao so) = dep
959+ in Deployment n t (hideP ao) (hideP so)
960+ dLogs' =
961+ dLogs <&> \(DeploymentLog ai a t ao so ec d ct) ->
962+ DeploymentLog ai a t (hideP ao) (hideP so) ec d ct
963+ hideP o =
964+ coerce o <&> \(Override k v vis) ->
965+ let v' = case vis of
966+ Private -> "*"
967+ Public -> v
968+ in coerce $ Override k v' vis
969+ in DeploymentInfo dep' dMeta dLogs'
894970
895971-- | Helper to get deployment info from the database.
896972getInfo :: DeploymentName -> AppM DeploymentInfo
@@ -910,8 +986,9 @@ getInfo dName = do
910986pingH :: AppM NoContent
911987pingH = do
912988 pgPool <- pool <$> ask
913− _ :: [Only Int] <- liftIO $ withResource pgPool $ \conn ->
914− query_ conn "SELECT 1"
989+ _ :: [Only Int] <- liftIO $
990+ withResource pgPool $ \conn ->
991+ query_ conn "SELECT 1"
915992 pure NoContent
916993
917994-- | Handles the 'project_name' request.
@@ -934,22 +1011,25 @@ cleanupH :: DeploymentName -> AppM CommandResponse
9341011cleanupH dName = do
9351012 failIfGracefulShutdownActivated
9361013 st <- ask
937− runDeploymentBgWorker Nothing dName (pure ()) $ \ () -> liftBase $ cleanupDeployment dName st
1014+ runDeploymentBgWorker Nothing dName (pure ()) $ \() -> liftBase $ cleanupDeployment dName st
9381015 pure Success
9391016
9401017-- | Helper to cleanup deployment.
9411018cleanupDeployment :: DeploymentName -> AppState -> IO ()
9421019cleanupDeployment dName st = do
943− let
944− log = logInfo (logger st)
945− pgPool = pool st
946− args =
947− [ "--project-name", coerce $ projectName st
948− , "--base-domain", coerce $ baseDomain st
949− , "--namespace", coerce $ namespace st
950− , "--name", coerce dName
951− ]
952− cmd = coerce $ cleanupCommand st
1020+ let log = logInfo (logger st)
1021+ pgPool = pool st
1022+ args =
1023+ [ "--project-name"
1024+ , coerce $ projectName st
1025+ , "--base-domain"
1026+ , coerce $ baseDomain st
1027+ , "--namespace"
1028+ , coerce $ namespace st
1029+ , "--name"
1030+ , coerce dName
1031+ ]
1032+ cmd = coerce $ cleanupCommand st
9531033 log $ "call " <> unwords (cmd : args)
9541034 (ec, out, err) <- runCommand (unpack cmd) (unpack <$> args)
9551035 print out >> print err
@@ -964,30 +1044,33 @@ cleanupDeployment dName st = do
9641044
9651045-- | Helper to delete deployment log overrides.
9661046deleteDeploymentLogOverrides :: PgPool -> DeploymentName -> IO Int64
967−deleteDeploymentLogOverrides p n = withResource p $ \conn -> execute
968− conn
969− "DELETE FROM deployment_log_overrides WHERE deployment_log_id in ( \
1047+deleteDeploymentLogOverrides p n = withResource p $ \conn ->
1048+ execute
1049+ conn
1050+ "DELETE FROM deployment_log_overrides WHERE deployment_log_id in ( \
9701051 \SELECT id FROM deployment_logs WHERE deployment_id in ( \
971− \SELECT id FROM deployments where name = ? \
1052+ \SELECT id FROM deployments where name = ? \
9721053 \) \
973− \)"
974− (Only n)
1054+ \)"
1055+ (Only n)
9751056
9761057-- | Helper to delete deployment logs.
9771058deleteDeploymentLogs :: PgPool -> DeploymentName -> IO Int64
978−deleteDeploymentLogs p n = withResource p $ \conn -> execute
979− conn
980− "DELETE FROM deployment_logs WHERE deployment_id in\
981− \ (SELECT id FROM deployments where name = ?)"
982− (Only n)
1059+deleteDeploymentLogs p n = withResource p $ \conn ->
1060+ execute
1061+ conn
1062+ "DELETE FROM deployment_logs WHERE deployment_id in\
1063+ \ (SELECT id FROM deployments where name = ?)"
1064+ (Only n)
9831065
9841066-- | Helper to delete deployment overrides.
9851067deleteDeploymentOverrides :: PgPool -> DeploymentName -> IO Int64
986−deleteDeploymentOverrides p n = withResource p $ \conn -> execute
987− conn
988− "DELETE FROM deployment_overrides WHERE deployment_id in\
989− \ (SELECT id FROM deployments where name = ?)"
990− (Only n)
1068+deleteDeploymentOverrides p n = withResource p $ \conn ->
1069+ execute
1070+ conn
1071+ "DELETE FROM deployment_overrides WHERE deployment_id in\
1072+ \ (SELECT id FROM deployments where name = ?)"
1073+ (Only n)
9911074
9921075-- | Helper to delete a deployment.
9931076deleteDeployment :: PgPool -> DeploymentName -> IO Int64
@@ -999,12 +1082,11 @@ cleanArchiveH :: AppM CommandResponse
9991082cleanArchiveH = do
10001083 failIfGracefulShutdownActivated
10011084 st <- ask
1002− let
1003− pgPool = pool st
1004− archRetention = unArchiveRetention . archiveRetention $ st
1005− q =
1006− "SELECT name FROM deployments \
1007− \WHERE status in ? AND archived_at + interval '?' second < now()"
1085+ let pgPool = pool st
1086+ archRetention = unArchiveRetention . archiveRetention $ st
1087+ q =
1088+ "SELECT name FROM deployments \
1089+ \WHERE status in ? AND archived_at + interval '?' second < now()"
10081090 retrieved :: [Only DeploymentName] <- liftIO $
10091091 withResource pgPool $ \conn -> query conn q (In archivedStatuses, archRetention)
10101092 runBgWorker . void $
@@ -1028,12 +1110,14 @@ restoreH dName = do
10281110 (ec, out, err) <- liftBase $ createDeployment dep' st
10291111 t2 <- liftBase now
10301112 let elTime = elapsedTime t2 t1
1031− transitionToStatusS dName $ TransitionCreatePending StatusTransitionProcessOutput
1032− { exitCode = ec
1033− , duration = elTime
1034− , stdout = out
1035− , stderr = err
1036− }
1113+ transitionToStatusS dName $
1114+ TransitionCreatePending
1115+ StatusTransitionProcessOutput
1116+ { exitCode = ec
1117+ , duration = elTime
1118+ , stdout = out
1119+ , stderr = err
1120+ }
10371121 handleExitCode ec
10381122 pure Success
10391123
@@ -1041,16 +1125,15 @@ restoreH dName = do
10411125getActionInfoH :: ActionId -> AppM ActionInfo
10421126getActionInfoH aId = do
10431127 st <- ask
1044− let
1045− pgPool = pool st
1046− aId' = Only . unActionId $ aId
1047− q = "SELECT stdout, stderr FROM deployment_logs WHERE id = ?"
1128+ let pgPool = pool st
1129+ aId' = Only . unActionId $ aId
1130+ q = "SELECT stdout, stderr FROM deployment_logs WHERE id = ?"
10481131 rows :: [(Text, Text)] <- liftIO $
10491132 withResource pgPool $ \conn -> query conn q aId'
10501133 case rows of
10511134 (out, err) : _ -> pure $ ActionInfo out err
10521135 _ ->
1053− throwError err400 { errBody = appError "Action not found" }
1136+ throwError err400 {errBody = appError "Action not found"}
10541137
10551138-- | Helper to handle exit code.
10561139handleExitCode :: MonadBaseControl IO m => ExitCode -> m ()
@@ -1058,96 +1141,103 @@ handleExitCode ExitSuccess = return ()
10581141handleExitCode (ExitFailure c) = liftBase . throwIO $ DeploymentFailed c
10591142
10601143-- | Helper to log a deployment action.
1061−createDeploymentLog
1062− :: Connection
1063− -> Deployment
1064− -> Action
1065− -> ExitCode
1066− -> Duration
1067− -> Stdout
1068− -> Stderr
1069− -> IO ()
1144+createDeploymentLog ::
1145+ (MonadError ServerError m, MonadIO m) =>
1146+ Connection ->
1147+ Deployment ->
1148+ Action ->
1149+ ExitCode ->
1150+ Duration ->
1151+ Stdout ->
1152+ Stderr ->
1153+ m ()
10701154createDeploymentLog conn dep act ec dur out err = do
1071− let
1072− (Deployment dName dTag appOvs depOvs) = dep
1073− exitCode' =
1074− case ec of
1075− ExitSuccess -> 0
1076− ExitFailure errCode -> errCode
1077− dur' = unDuration dur
1078− out' = unStdout out
1079− err' = unStderr err
1080− qInsertLog =
1081− "INSERT INTO deployment_logs \
1082− \(deployment_id, action, tag, exit_code, \
1083− \duration, stdout, stderr) \
1084− \(\
1155+ let (Deployment dName dTag appOvs depOvs) = dep
1156+ exitCode' =
1157+ case ec of
1158+ ExitSuccess -> 0
1159+ ExitFailure errCode -> errCode
1160+ dur' = unDuration dur
1161+ out' = unStdout out
1162+ err' = unStderr err
1163+ qInsertLog =
1164+ "INSERT INTO deployment_logs \
1165+ \(deployment_id, action, tag, exit_code, \
1166+ \duration, stdout, stderr) \
1167+ \(\
10851168 \SELECT id, ?, ?, ?, ?, ?, ? \
10861169 \FROM deployments \
10871170 \WHERE name = ? \
1088− \) RETURNING id"
1089− qInsertLogOverride =
1090− "INSERT INTO deployment_log_overrides \
1091− \(key, value, deployment_log_id, scope, visibility) \
1092− \VALUES (?, ?, ?, ?, ?)"
1093− aIds :: [Only Int] <- query conn qInsertLog
1094− (act, dTag, exitCode', dur', out', err', dName)
1095− void $ for appOvs $ \o -> do
1096− let
1097− [Only aId] = aIds
1098− oKey = overrideKey . unApplicationOverride $ o
1099− oValue = overrideValue . unApplicationOverride $ o
1100− oScope = show ApplicationScope
1101− oVis = show . overrideVisibility . unApplicationOverride $ o
1102− execute conn qInsertLogOverride (oKey, oValue, aId, oScope, oVis)
1103− void $ for depOvs $ \o -> do
1104− let
1105− [Only aId] = aIds
1106− oKey = overrideKey . unDeploymentOverride $ o
1107− oValue = overrideValue . unDeploymentOverride $ o
1108− oScope = show DeploymentScope
1109− oVis = show . overrideVisibility . unDeploymentOverride $ o
1110− execute conn qInsertLogOverride (oKey, oValue, aId, oScope, oVis)
1171+ \) RETURNING id"
1172+ qInsertLogOverride =
1173+ "INSERT INTO deployment_log_overrides \
1174+ \(key, value, deployment_log_id, scope, visibility) \
1175+ \VALUES (?, ?, ?, ?, ?)"
1176+ (Only aId) :: Only Int <-
1177+ liftIO
1178+ ( query
1179+ conn
1180+ qInsertLog
1181+ (act, dTag, exitCode', dur', out', err', dName)
1182+ )
1183+ >>= ensureOne
1184+ void $
1185+ for appOvs $ \o -> do
1186+ let oKey = overrideKey . unApplicationOverride $ o
1187+ oValue = overrideValue . unApplicationOverride $ o
1188+ oScope = show ApplicationScope
1189+ oVis = show . overrideVisibility . unApplicationOverride $ o
1190+ liftIO $ execute conn qInsertLogOverride (oKey, oValue, aId, oScope, oVis)
1191+ void $
1192+ for depOvs $ \o -> do
1193+ let oKey = overrideKey . unDeploymentOverride $ o
1194+ oValue = overrideValue . unDeploymentOverride $ o
1195+ oScope = show DeploymentScope
1196+ oVis = show . overrideVisibility . unDeploymentOverride $ o
1197+ liftIO $ execute conn qInsertLogOverride (oKey, oValue, aId, oScope, oVis)
1198+
1199+ensureOne :: MonadError ServerError m => [a] -> m a
1200+ensureOne [x] = return x
1201+ensureOne _ = throwError err500
11111202
11121203-- | Helper to get deployment metadata from the database.
1113−selectDeploymentMetadata
1114− :: Connection
1115− -> DeploymentName
1116− -> IO [DeploymentMetadata]
1204+selectDeploymentMetadata ::
1205+ Connection ->
1206+ DeploymentName ->
1207+ IO [DeploymentMetadata]
11171208selectDeploymentMetadata conn dName = do
1118− let
1119− q =
1120− "SELECT key, value FROM deployment_metadata \
1121− \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?) \
1122− \ORDER BY id ASC"
1209+ let q =
1210+ "SELECT key, value FROM deployment_metadata \
1211+ \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?) \
1212+ \ORDER BY id ASC"
11231213 rows <- query conn q (Only dName)
11241214 for rows $ \(k, v) -> pure $ DeploymentMetadata k v
11251215
11261216-- | Helper to delete deployment metadata.
1127−deleteDeploymentMetadata
1128− :: PgPool
1129− -> DeploymentName
1130− -> IO ()
1217+deleteDeploymentMetadata ::
1218+ PgPool ->
1219+ DeploymentName ->
1220+ IO ()
11311221deleteDeploymentMetadata pgPool dName = do
1132− let
1133− q =
1134− "DELETE FROM deployment_metadata \
1135− \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)"
1222+ let q =
1223+ "DELETE FROM deployment_metadata \
1224+ \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)"
11361225 void $ withResource pgPool $ \conn -> execute conn q (Only dName)
11371226
11381227-- | Helper to insert or update deployment metadata.
1139−upsertDeploymentMetadata
1140− :: PgPool
1141− -> DeploymentName
1142− -> [DeploymentMetadata]
1143− -> IO ()
1228+upsertDeploymentMetadata ::
1229+ PgPool ->
1230+ DeploymentName ->
1231+ [DeploymentMetadata] ->
1232+ IO ()
11441233upsertDeploymentMetadata pgPool dName dMetadatas = do
11451234 withResource pgPool $ \conn -> withTransactionSerializable conn $ do
1146− void $ execute
1147− conn
1148− "DELETE FROM deployment_metadata \
1149− \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)"
1150− (Only dName)
1235+ void $
1236+ execute
1237+ conn
1238+ "DELETE FROM deployment_metadata \
1239+ \WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)"
1240+ (Only dName)
11511241 forM_ dMetadatas $ \dMeta ->
11521242 execute
11531243 conn
@@ -1161,17 +1251,21 @@ upsertDeploymentMetadata pgPool dName dMetadatas = do
11611251failIfImageNotFound :: DeploymentName -> DeploymentTag -> AppM ()
11621252failIfImageNotFound dName dTag = do
11631253 st <- ask
1164− let
1165− log :: Text -> IO ()
1166− log = logInfo (logger st)
1167− args =
1168− [ "--project-name", coerce $ projectName st
1169− , "--base-domain", coerce $ baseDomain st
1170− , "--namespace", coerce $ namespace st
1171− , "--name", coerce $ dName
1172− , "--tag", coerce $ dTag
1173− ]
1174− cmd = coerce $ tagCheckingCommand st
1254+ let log :: Text -> IO ()
1255+ log = logInfo (logger st)
1256+ args =
1257+ [ "--project-name"
1258+ , coerce $ projectName st
1259+ , "--base-domain"
1260+ , coerce $ baseDomain st
1261+ , "--namespace"
1262+ , coerce $ namespace st
1263+ , "--name"
1264+ , coerce $ dName
1265+ , "--tag"
1266+ , coerce $ dTag
1267+ ]
1268+ cmd = coerce $ tagCheckingCommand st
11751269
11761270 ec <- liftIO $ do
11771271 log $ "call " <> unwords (cmd : args)
@@ -1179,7 +1273,7 @@ failIfImageNotFound dName dTag = do
11791273 case ec of
11801274 ExitSuccess -> pure ()
11811275 ExitFailure _ ->
1182− throwError err400 { errBody = validationError [] ["Tag not found"] }
1276+ throwError err400 {errBody = validationError [] ["Tag not found"]}
11831277
11841278-- | Helper to create an application-level error.
11851279appError :: Text -> BSL.ByteString
@@ -1203,27 +1297,25 @@ elapsedTime t1 t2 =
12031297-- | Runs the status updater.
12041298runStatusUpdater :: AppState -> IO ()
12051299runStatusUpdater state = do
1206− let
1207− pgPool = pool state
1208− interval = 30 :: Int
1209− selectDeps =
1210− "SELECT name, status::text, \
1211− \extract(epoch from now())::int - \
1300+ let pgPool = pool state
1301+ interval = 30 :: Int
1302+ selectDeps =
1303+ "SELECT name, status::text, \
1304+ \extract(epoch from now())::int - \
12121305 \extract(epoch from status_updated_at)::int \
1213− \, tag \
1214− \FROM deployments \
1215− \WHERE checked_at < now() - interval '?' second AND status != 'Archived'"
1216− updateCheckedAt =
1217− "UPDATE deployments SET checked_at = now() WHERE name = ? AND status = ?"
1218− logErr :: Text -> IO ()
1219− logErr = logWarning (logger state)
1306+ \, tag \
1307+ \FROM deployments \
1308+ \WHERE checked_at < now() - interval '?' second AND status != 'Archived'"
1309+ updateCheckedAt =
1310+ "UPDATE deployments SET checked_at = now() WHERE name = ? AND status = ?"
1311+ logErr :: Text -> IO ()
1312+ logErr = logWarning (logger state)
12201313
12211314 forever $ do
12221315 rows :: [(DeploymentName, DeploymentStatus, Int, DeploymentTag)] <- liftIO $
12231316 withResource pgPool $ \conn -> query conn selectDeps (Only interval)
1224− let
1225− checkList :: [(DeploymentName, DeploymentStatus, Timestamp, DeploymentTag)] =
1226− (\(n, s, t, dTag) -> (n, s, coerce t, dTag)) <$> rows
1317+ let checkList :: [(DeploymentName, DeploymentStatus, Timestamp, DeploymentTag)] =
1318+ (\(n, s, t, dTag) -> (n, s, coerce t, dTag)) <$> rows
12271319 checkResult <- for checkList $ \(dName, dStatus, ts, dTag) -> do
12281320 let timeout = statusUpdateTimeout state
12291321 (ec, _, _) <- flip runReaderT state case dStatus of
@@ -1236,28 +1328,31 @@ runStatusUpdater state = do
12361328 case transitionM of
12371329 Nothing -> execute conn updateCheckedAt (dName, dStatus) $> False
12381330 Just transition ->
1239− ($> True) $ (flip runReaderT state . runExceptT . runExceptT) (transitionToStatus dName transition) >>= \case
1240− Right (Right ()) -> void $ execute conn updateCheckedAt (dName, transitionStatus transition)
1241− Left e -> logErr $ show' e
1242− Right (Left e) -> logErr $ show' e
1331+ ($> True) $
1332+ (flip runReaderT state . runExceptT . runExceptT) (transitionToStatus dName transition) >>= \case
1333+ Right (Right ()) -> void $ execute conn updateCheckedAt (dName, transitionStatus transition)
1334+ Left e -> logErr $ show' e
1335+ Right (Left e) -> logErr $ show' e
12431336 when (or updated) $ sendReloadEvent state
12441337 threadDelay 5000000
12451338
12461339-- | Returns the new deployment status.
1247−statusTransition
1248− :: ExitCode
1249− -> DeploymentStatus
1250− -> Timestamp
1251− -> Timeout
1252− -> Maybe DeploymentStatusTransition
1253−statusTransition ExitSuccess ArchivePending _ _ = Just TransitionArchived
1254−statusTransition ExitSuccess Running _ _ = Nothing
1255−statusTransition ExitSuccess _ _ _ = Just TransitionCreate
1256−statusTransition (ExitFailure n) Running _ _ = Just . TransitionFailure $ failureStatusType n
1257−statusTransition (ExitFailure n) CreatePending ts timeout | ts > coerce timeout =
1258− Just . TransitionFailure $ failureStatusType n
1259−statusTransition (ExitFailure n) UpdatePending ts timeout | ts > coerce timeout =
1260− Just . TransitionFailure $ failureStatusType n
1340+statusTransition ::
1341+ ExitCode ->
1342+ DeploymentStatus ->
1343+ Timestamp ->
1344+ Timeout ->
1345+ Maybe DeploymentStatusTransition
1346+statusTransition ExitSuccess ArchivePending _ _ = Just TransitionArchived
1347+statusTransition ExitSuccess Running _ _ = Nothing
1348+statusTransition ExitSuccess _ _ _ = Just TransitionCreate
1349+statusTransition (ExitFailure n) Running _ _ = Just . TransitionFailure $ failureStatusType n
1350+statusTransition (ExitFailure n) CreatePending ts timeout
1351+ | ts > coerce timeout =
1352+ Just . TransitionFailure $ failureStatusType n
1353+statusTransition (ExitFailure n) UpdatePending ts timeout
1354+ | ts > coerce timeout =
1355+ Just . TransitionFailure $ failureStatusType n
12611356statusTransition (ExitFailure _) _ _ _ = Nothing
12621357
12631358failureStatusType :: Int -> FailureType
@@ -1273,7 +1368,7 @@ failIfGracefulShutdownActivated = do
12731368 gracefulShutdownAct <- gracefulShutdownActivated <$> ask
12741369 gracefulShutdown <- liftIO . readIORef $ gracefulShutdownAct
12751370 if gracefulShutdown
1276− then throwError err405 { errBody = appError "Graceful shutdown activated" }
1371+ then throwError err405 {errBody = appError "Graceful shutdown activated"}
12771372 else pure ()
12781373
12791374-- | Handles the graceful shutdown signal.
@@ -1314,36 +1409,38 @@ runBgWorker act = void $ L.forkFinally act' cleanup
13141409 else pure ()
13151410
13161411-- | Same as 'runBgWorker' but also locks the specified deployment.
1317−runDeploymentBgWorker
1318− :: forall m a. (MonadBaseControl IO m, MonadReader AppState m, MonadError ServerError m)
1319− => Maybe DeploymentStatus
1320− -> DeploymentName
1321− -> m a
1322− -- ^ Think of this as running synchronously.
1412+runDeploymentBgWorker ::
1413+ forall m a.
1414+ (MonadBaseControl IO m, MonadReader AppState m, MonadError ServerError m) =>
1415+ Maybe DeploymentStatus ->
1416+ DeploymentName ->
1417+ -- | Think of this as running synchronously.
13231418 -- The resulting monad state will be forwarded to the calling context.
1324− -> (a -> m ())
1325− -- ^ The computation to run asynchronously.
1326− -> m ()
1419+ m a ->
1420+ -- | The computation to run asynchronously.
1421+ (a -> m ()) ->
1422+ m ()
13271423runDeploymentBgWorker newS dName pre post = do
13281424 st <- ask
13291425 mainThread <- L.myThreadId
13301426 (err :: MVar (Either ServerError (StM m a))) <- L.newEmptyMVar
1331− runBgWorker $ withLockedDeployment
1332− dName
1333− (L.putMVar err $ Left err409 {errBody = "The deployment is currently being processed."})
1334− ( do
1335− L.try (liftBaseWith \runInBase -> runInBase pre) >>= \case
1336− Left (e :: L.SomeException) -> L.throwTo mainThread e
1337− Right x -> do
1338− proceed <- flip L.finally (L.tryPutMVar err (Left err500)) $ case newS of
1339− Just newS' -> do
1340− runExceptT (assertDeploymentTransitionPossibleS dName newS') >>= \case
1427+ runBgWorker $
1428+ withLockedDeployment
1429+ dName
1430+ (L.putMVar err $ Left err409 {errBody = "The deployment is currently being processed."})
1431+ ( do
1432+ L.try (liftBaseWith \runInBase -> runInBase pre) >>= \case
1433+ Left (e :: L.SomeException) -> L.throwTo mainThread e
1434+ Right x -> do
1435+ proceed <- flip L.finally (L.tryPutMVar err (Left err500)) $ case newS of
1436+ Just newS' -> do
1437+ runExceptT (assertDeploymentTransitionPossibleS dName newS') >>= \case
13411438 Left e -> L.putMVar err (Left e) $> False
13421439 Right () -> L.putMVar err (Right x) $> True
1343− Nothing -> L.putMVar err (Right x) $> True
1344− liftBase (sendReloadEvent st)
1345− when proceed $ restoreM x >>= post
1346− )
1440+ Nothing -> L.putMVar err (Right x) $> True
1441+ liftBase (sendReloadEvent st)
1442+ when proceed $ restoreM x >>= post
1443+ )
13471444 L.readMVar err >>= \case
13481445 Left e -> throwError e
13491446 Right (stm :: StM m a) -> do
octopod-backend/src/Octopod/Server/Args.hs+46−58
@@ -1,72 +1,60 @@
1−{-|
2−Module : Octopod.Server.Args
3−Description : Octopod Server arguments parser utils.
4−-}
5−
6−module Octopod.Server.Args where
7−
8−
9−import Data.ByteString (ByteString)
10−import Data.Coerce
11−import Options.Generic
12−
13−
14−import Types
15−
1+-- |
2+--Module : Octopod.Server.Args
3+--Description : Octopod Server arguments parser utils.
4+module Octopod.Server.Args
5+ ( Args (..),
6+ OctopodOpts (..),
7+ parseArgs,
8+ )
9+where
10+
11+import Data.ByteString (ByteString)
12+import Data.Coerce
13+import Options.Generic
14+
15+import Types
1616
1717-- | Octopod Server arguments definition.
1818data Args = Args
19− { port :: Int
20− -- ^ port for octo CLI
21− , uiPort :: Int
22− -- ^ port for Web UI (HTTP)
23− , wsPort :: Int
24− -- ^ port for Web UI (WS)
25− , db :: ByteString
26− -- ^ database connection string
27− , dbPoolSize :: Int
28− -- ^ database pool size
29− , tlsCertPath :: ByteString
30− -- ^ TLS certificate file path
31− , tlsKeyPath :: ByteString
32− -- ^ TLS key file path
33− , tlsStorePath :: ByteString
34− -- ^ TLS store path
35− } deriving (Generic, Show)
19+ { -- | port for octo CLI
20+ port :: Int
21+ , -- | port for Web UI (HTTP)
22+ uiPort :: Int
23+ , -- | port for Web UI (WS)
24+ wsPort :: Int
25+ , -- | database connection string
26+ db :: ByteString
27+ , -- | database pool size
28+ dbPoolSize :: Int
29+ }
30+ deriving stock (Generic, Show)
3631
3732instance ParseRecord Args where
3833 parseRecord = parseRecordWithModifiers lispCaseModifiers
3934
4035-- | Parsed Octopod Server arguments definition.
4136data OctopodOpts = OctopodOpts
42− { octopodPort :: ServerPort
43− -- ^ port for Octopod Server
44− , octopodUIPort :: ServerPort
45− -- ^ port for UI (HTTP)
46− , octopodWSPort :: ServerPort
47− -- ^ port for UI (WS)
48− , octopodDB :: DBConnectionString
49− -- ^ database connection string
50− , octopodDBPoolSize :: DBPoolSize
51− -- ^ database pool size
52− , octopodTLSCertPath :: TLSCertPath
53− -- ^ TLS certificate file path
54− , octopodTLSKeyPath :: TLSKeyPath
55− -- ^ TLS key file path
56− , octopodTLSStorePath :: TLSStorePath
57− -- ^ TLS store path
58− } deriving (Show)
37+ { -- | port for Octopod Server
38+ octopodPort :: ServerPort
39+ , -- | port for UI (HTTP)
40+ octopodUIPort :: ServerPort
41+ , -- | port for UI (WS)
42+ octopodWSPort :: ServerPort
43+ , -- | database connection string
44+ octopodDB :: DBConnectionString
45+ , -- | database pool size
46+ octopodDBPoolSize :: DBPoolSize
47+ }
48+ deriving stock (Show)
5949
6050-- | Parses Octopod Server arguments.
6151parseArgs :: IO OctopodOpts
6252parseArgs = do
6353 args <- getRecord "Octopod.Server"
64− pure $ OctopodOpts
65− (coerce $ port args)
66− (coerce $ uiPort args)
67− (coerce $ wsPort args)
68− (coerce $ db args)
69− (coerce $ dbPoolSize args)
70− (coerce $ tlsCertPath args)
71− (coerce $ tlsKeyPath args)
72− (coerce $ tlsStorePath args)
54+ pure $
55+ OctopodOpts
56+ (coerce $ port args)
57+ (coerce $ uiPort args)
58+ (coerce $ wsPort args)
59+ (coerce $ db args)
60+ (coerce $ dbPoolSize args)
octopod-backend/src/Octopod/Server/Logger.hs+16−12
@@ -1,15 +1,19 @@
1−{-|
2−Module : Octopod.Server.Logger
3−Description : Octopod Server logger.
4−-}
5−
6−
7−module Octopod.Server.Logger where
8−
9−import Data.ByteString
10−import Data.Text
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
1115import qualified Data.Text as T
12−import System.Log.FastLogger
16+import System.Log.FastLogger
1317
1418-- | Creates new logger.
1519newLogger :: IO TimedFastLogger
@@ -32,7 +36,7 @@ logWithSeverity l severity msg = l $ \ft -> metadata ft <> message
3236 where
3337 metadata :: ByteString -> LogStr
3438 metadata ft = foldMap toLogStr ["[", ft, " ", severity, "] "]
35− message = toLogStr msg <> toLogStr ("\n" :: ByteString)
39+ message = toLogStr msg <> toLogStr ("\n" :: ByteString)
3640
3741show' :: Show a => a -> Text
3842show' = T.pack . show
octopod-backend/src/Orphans.hs+13−10
@@ -1,16 +1,19 @@
11{-# OPTIONS_GHC -fno-warn-orphans #-}
2−module Orphans where
32
4−import Database.PostgreSQL.Simple.FromField (FromField)
5−import Database.PostgreSQL.Simple.ToField (ToField)
3+module Orphans () where
64
7−import Common.Types
5+import Common.Types
6+import Database.PostgreSQL.Simple.FromField (FromField)
7+import Database.PostgreSQL.Simple.ToField (ToField)
88
9−deriving instance FromField Action
10−deriving instance ToField Action
9+deriving newtype instance FromField Action
1110
12−deriving instance FromField DeploymentTag
13−deriving instance ToField DeploymentTag
11+deriving newtype instance ToField Action
1412
15−deriving instance FromField DeploymentName
16−deriving instance ToField DeploymentName
13+deriving newtype instance FromField DeploymentTag
14+
15+deriving newtype instance ToField DeploymentTag
16+
17+deriving newtype instance FromField DeploymentName
18+
19+deriving newtype instance ToField DeploymentName
octopod-backend/src/TLS.hsdeleted−68
@@ -1,68 +0,0 @@
1−{-|
2−Module : TLS
3−Description : TLS utils.
4−
5−This module contains TLS utils.
6−-}
7−
8−
9−module TLS where
10−
11−
12−import qualified Data.ByteString.Char8 as BSC
13−import Data.Default.Class (def)
14−import Data.X509 (ExtKeyUsagePurpose(..), HashALG(HashSHA256))
15−import Data.X509.CertificateStore (readCertificateStore)
16−import Data.X509.Validation
17− (ValidationChecks(..), ValidationHooks(..), checkLeafV3, validate)
18−import Network.TLS
19− (CertificateRejectReason(..), CertificateUsage(..), onClientCertificate)
20−import Network.Wai.Handler.WarpTLS hiding (TLSSettings, tlsSettings)
21−import qualified Network.Wai.Handler.WarpTLS as WTLS
22−
23−
24−import Types
25−
26−
27−-- | Creates TLS options for the HTTP server
28−createTLSOpts
29− :: TLSCertPath
30− -> TLSKeyPath
31− -> TLSStorePath
32− -> ServerPort
33− -> WTLS.TLSSettings
34−createTLSOpts tlsCert tlsKey tlsStore serverPort =
35− initTLSSettings
36− { tlsWantClientCert = True
37− , onInsecure = DenyInsecure
38− "This server only accepts secure HTTPS connections."
39− , tlsServerHooks = def { onClientCertificate =
40− fmap certificateUsageFromValidations . validateCertificate }
41− }
42− where
43− tlsCert' = unTLSCertPath tlsCert
44− tlsKey' = unTLSKeyPath tlsKey
45− tlsStore' = BSC.unpack . unTLSStorePath $ tlsStore
46− serverPort' = unServerPort serverPort
47− initTLSSettings = WTLS.tlsSettings
48− (BSC.unpack tlsCert') (BSC.unpack tlsKey')
49− certificateUsageFromValidations = maybe CertificateUsageAccept
50− (CertificateUsageReject . CertificateRejectOther)
51− serviceID = ("localhost", BSC.pack $ show serverPort')
52− validationHooks = def
53− { hookValidateName = \_ _ -> [] }
54− validationChecks = def
55− { checkStrictOrdering = True
56− , checkLeafKeyPurpose = [KeyUsagePurpose_ClientAuth]
57− , checkLeafV3 = False
58− }
59− validateCertificate cert = do
60− mstore <- readCertificateStore tlsStore'
61− maybe
62− (pure . Just $ "Cannot init a store")
63− (fmap fromX509FailedReasons . (\store -> validate HashSHA256
64− validationHooks validationChecks store def serviceID cert))
65− mstore
66− fromX509FailedReasons reasons = case reasons of
67− [] -> Nothing
68− _ -> Just (show reasons)
octopod-backend/src/Types.hs+49−55
@@ -1,32 +1,30 @@
1−{-|
2−Module : Types
3−Description : Backend types.
4−
5−This module contains backend types.
6−-}
7−
8−
1+-- |
2+--Module : Types
3+--Description : Backend types.
4+--
5+--This module contains backend types.
96module Types
10− ( module Common.Types
11− , parseDeploymentMetadata
12− , ServerPort(..)
13− , DBPoolSize(..)
14− , DBConnectionString(..)
15− , TLSStorePath(..)
16− , ProjectName(..)
17− , Domain(..)
18− , Namespace(..)
19− , Command(..)
20− , ArchiveRetention(..)
21− , Timeout(..)
22− , ControlScriptArgs(..)) where
23−
24−import Data.Bifunctor
25−import Data.ByteString (ByteString)
26−import Data.Text as T
27−import Data.Traversable
28−
29−import Common.Types
7+ ( module Common.Types,
8+ parseDeploymentMetadata,
9+ ServerPort (..),
10+ DBPoolSize (..),
11+ DBConnectionString (..),
12+ ProjectName (..),
13+ Domain (..),
14+ Namespace (..),
15+ Command (..),
16+ ArchiveRetention (..),
17+ Timeout (..),
18+ ControlScriptArgs (..),
19+ )
20+where
21+
22+import Data.Bifunctor
23+import Data.ByteString (ByteString)
24+import Data.Text as T
25+import Data.Traversable
26+
27+import Common.Types
3028
3129-- | Parses deployment metadata.
3230parseDeploymentMetadata :: [Text] -> IO [DeploymentMetadata]
@@ -34,50 +32,46 @@ parseDeploymentMetadata texts =
3432 for texts $ \t ->
3533 case T.findIndex (== ',') t of
3634 Just i -> do
37− let
38− (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t
35+ let (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t
3936 pure $ DeploymentMetadata key value
40− Nothing -> error $
41− "Malformed metadata key-value pair " <> T.unpack t <>
42− ", should be similar to foo,bar"
37+ Nothing ->
38+ error $
39+ "Malformed metadata key-value pair " <> T.unpack t
40+ <> ", should be similar to foo,bar"
4341
4442-- | Server port.
45−newtype ServerPort = ServerPort { unServerPort :: Int }
46− deriving (Show)
43+newtype ServerPort = ServerPort {unServerPort :: Int}
44+ deriving stock (Show)
4745
4846-- | Database pool size.
49−newtype DBPoolSize = DBPoolSize { unDBPoolSize :: Int }
50− deriving (Show)
47+newtype DBPoolSize = DBPoolSize {unDBPoolSize :: Int}
48+ deriving stock (Show)
5149
5250-- | Database connection string.
5351newtype DBConnectionString = DbConnectionString
54− { unDBConnectionString :: ByteString }
55− deriving (Show)
56−
57−-- | TLS store path.
58−newtype TLSStorePath = TLSStore { unTLSStorePath :: ByteString }
59− deriving (Show)
52+ {unDBConnectionString :: ByteString}
53+ deriving stock (Show)
6054
61−newtype Domain = Domain { unDomain :: Text }
62− deriving (Show)
55+newtype Domain = Domain {unDomain :: Text}
56+ deriving stock (Show)
6357
6458-- | A Kubernetes namespace.
65−newtype Namespace = Namespace { unNamespace :: Text }
66− deriving (Show)
59+newtype Namespace = Namespace {unNamespace :: Text}
60+ deriving stock (Show)
6761
6862-- | Archive retention.
69−newtype ArchiveRetention = ArchiveRetention { unArchiveRetention :: Int }
70− deriving (Show)
63+newtype ArchiveRetention = ArchiveRetention {unArchiveRetention :: Int}
64+ deriving stock (Show)
7165
7266-- | Timeout.
73−newtype Timeout = Timeout { unTimeout :: Int }
74− deriving (Show)
67+newtype Timeout = Timeout {unTimeout :: Int}
68+ deriving stock (Show)
7569
7670-- | Path to a deployment control script.
77−newtype Command = Command { unCommand :: Text }
78− deriving (Show)
71+newtype Command = Command {unCommand :: Text}
72+ deriving stock (Show)
7973
8074-- | Control script arguments.
8175newtype ControlScriptArgs = ControlScriptArgs
82− { unControlScriptArgs :: [String] }
83− deriving (Show)
76+ {unControlScriptArgs :: [String]}
77+ deriving stock (Show)
octopod-backend/test/Spec.hsdeleted−2
@@ -1,2 +0,0 @@
1−main :: IO ()
2−main = putStrLn "Test suite not yet implemented"
octopod-common/octopod-common.cabal+11−1
@@ -42,7 +42,17 @@ library
4242 , http-api-data
4343 , lens
4444 , regex-tdfa
45− , servant
4645 , text
4746 hs-source-dirs: src
4847 default-language: Haskell2010
48+ ghc-options:
49+ -Weverything
50+ -Wno-implicit-prelude
51+ -Wno-missing-safe-haskell-mode
52+ -Wno-safe
53+ -Wno-prepositive-qualified-module
54+ -Wno-missing-import-lists
55+ -Wno-all-missed-specialisations
56+ -Wno-missing-local-signatures
57+ -Wno-partial-fields
58+ -Wno-unsafe
octopod-common/src/Common/Types.hs+90−101
@@ -1,22 +1,19 @@
1−{-|
2−Module : Common.Types
3−Description : Common types for backend and frontend.
4−
5−This module contains common types between the backend and the frontend.
6−-}
7−
1+{-# OPTIONS_GHC -Wno-missing-export-lists #-}
82
3+-- |
4+-- Module : Common.Types
5+-- Description : Common types for backend and frontend.
6+--
7+-- This module contains common types between the backend and the frontend.
98module Common.Types where
109
11−import Data.Bifunctor
12−import Data.ByteString (ByteString)
13−import Data.Coerce
14−import Data.String
15−import Data.Text as T
16−import Data.Traversable
17−import Deriving.Aeson.Stock
18−import GHC.Records
19−import Web.HttpApiData
10+import Data.Bifunctor
11+import Data.Coerce
12+import Data.String
13+import Data.Text as T
14+import Data.Traversable
15+import Deriving.Aeson.Stock
16+import Web.HttpApiData
2017
2118-- | Deployment override.
2219data Override = Override
@@ -24,7 +21,7 @@ data Override = Override
2421 , overrideValue :: Text
2522 , overrideVisibility :: OverrideVisibility
2623 }
27− deriving (Generic, Show, Eq)
24+ deriving stock (Generic, Show, Eq)
2825 deriving (FromJSON, ToJSON) via Snake Override
2926
3027type Overrides = [Override]
@@ -33,57 +30,56 @@ type Overrides = [Override]
3330data OverrideScope
3431 = ApplicationScope
3532 | DeploymentScope
36− deriving (Generic, Show, Read, Eq)
33+ deriving stock (Generic, Show, Read, Eq)
3734 deriving (FromJSON, ToJSON) via Snake OverrideScope
3835
3936-- | Deployment override visibility.
4037data OverrideVisibility
4138 = Private
4239 | Public
43− deriving (Generic, Show, Read, Eq)
40+ deriving stock (Generic, Show, Read, Eq)
4441 deriving (FromJSON, ToJSON) via Snake OverrideVisibility
4542
4643-- | Deployment application-level override.
47−newtype ApplicationOverride =
48− ApplicationOverride { unApplicationOverride :: Override }
49− deriving (Show, Eq, FromJSON, ToJSON)
44+newtype ApplicationOverride = ApplicationOverride {unApplicationOverride :: Override}
45+ deriving newtype (Show, Eq, FromJSON, ToJSON)
5046
5147-- | Deployment application-level overrides.
5248type ApplicationOverrides = [ApplicationOverride]
5349
5450-- | Deployment-level override.
5551newtype DeploymentOverride = DeploymentOverride
56− { unDeploymentOverride :: Override }
57− deriving (Show, Eq, FromJSON, ToJSON)
52+ {unDeploymentOverride :: Override}
53+ deriving newtype (Show, Eq, FromJSON, ToJSON)
5854
5955-- | Deployment-level overrides.
6056type DeploymentOverrides = [DeploymentOverride]
6157
62−newtype DeploymentId = DeploymentId { unDeploymentId :: Int }
63− deriving (Show)
58+newtype DeploymentId = DeploymentId {unDeploymentId :: Int}
59+ deriving stock (Show)
6460
65−newtype DeploymentName = DeploymentName { unDeploymentName :: Text }
66− deriving
61+newtype DeploymentName = DeploymentName {unDeploymentName :: Text}
62+ deriving newtype
6763 (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq, Ord)
6864
69−newtype DeploymentTag = DeploymentTag { unDeploymentTag :: Text }
70− deriving
65+newtype DeploymentTag = DeploymentTag {unDeploymentTag :: Text}
66+ deriving newtype
7167 (Show, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq)
7268
73−newtype Action = Action { unAction :: Text }
74− deriving (Show, FromJSON, ToJSON, IsString)
69+newtype Action = Action {unAction :: Text}
70+ deriving newtype (Show, FromJSON, ToJSON, IsString)
7571
76−newtype ArchivedFlag = ArchivedFlag { unArchivedFlag :: Bool }
77− deriving (Show, FromJSON, ToJSON)
72+newtype ArchivedFlag = ArchivedFlag {unArchivedFlag :: Bool}
73+ deriving newtype (Show, FromJSON, ToJSON)
7874
79−newtype Duration = Duration { unDuration :: Int }
80− deriving (Show, FromJSON, ToJSON)
75+newtype Duration = Duration {unDuration :: Int}
76+ deriving newtype (Show, FromJSON, ToJSON)
8177
82−newtype Timestamp = Timestamp { unTimestamp :: Int }
83− deriving (Show, Eq, Ord, FromJSON, ToJSON)
78+newtype Timestamp = Timestamp {unTimestamp :: Int}
79+ deriving newtype (Show, Eq, Ord, FromJSON, ToJSON)
8480
85−newtype ProjectName = ProjectName { uProjectName :: Text }
86− deriving (Show, FromJSON, ToJSON)
81+newtype ProjectName = ProjectName {uProjectName :: Text}
82+ deriving newtype (Show, FromJSON, ToJSON)
8783
8884deploymentStatusText :: DeploymentStatus -> Text
8985deploymentStatusText Running = "Running"
@@ -102,21 +98,21 @@ data DeploymentStatus
10298 | UpdatePending
10399 | ArchivePending
104100 | Archived
105− deriving (Generic, Read, Show, Eq)
101+ deriving stock (Generic, Read, Show, Eq)
106102 deriving (FromJSON, ToJSON) via Snake DeploymentStatus
107103
108104data FailureType
109105 = GenericFailure
110106 | TagMismatch
111107 | PartialAvailability
112− deriving (Generic, Read, Show, Eq)
108+ deriving stock (Generic, Read, Show, Eq)
113109 deriving (FromJSON, ToJSON) via Snake FailureType
114110
115111data PreciseDeploymentStatus
116− = DeploymentPending { recordedStatus :: DeploymentStatus }
117− -- ^ The deployment is currently being processed by the server
118− | DeploymentNotPending { recordedStatus :: DeploymentStatus }
119− deriving (Generic, Read, Show, Eq)
112+ = -- | The deployment is currently being processed by the server
113+ DeploymentPending {recordedStatus :: DeploymentStatus}
114+ | DeploymentNotPending {recordedStatus :: DeploymentStatus}
115+ deriving stock (Generic, Read, Show, Eq)
120116 deriving (FromJSON, ToJSON) via Snake PreciseDeploymentStatus
121117
122118archivedStatuses :: [DeploymentStatus]
@@ -131,7 +127,7 @@ data Deployment = Deployment
131127 , appOverrides :: ApplicationOverrides
132128 , deploymentOverrides :: DeploymentOverrides
133129 }
134− deriving (Generic, Show, Eq)
130+ deriving stock (Generic, Show, Eq)
135131 deriving (FromJSON, ToJSON) via Snake Deployment
136132
137133data DeploymentLog = DeploymentLog
@@ -144,14 +140,16 @@ data DeploymentLog = DeploymentLog
144140 , duration :: Duration
145141 , createdAt :: Int
146142 }
147− deriving (Generic, Show)
143+ deriving stock (Generic, Show)
148144 deriving (FromJSON, ToJSON) via Snake DeploymentLog
149145
150146data DeploymentMetadata = DeploymentMetadata
151− { deploymentMetadataKey :: Text -- ^ The name of the link
152− , deploymentMetadataValue :: Text -- ^ The URL
147+ { -- | The name of the link
148+ deploymentMetadataKey :: Text
149+ , -- | The URL
150+ deploymentMetadataValue :: Text
153151 }
154− deriving (Generic, Show, Eq)
152+ deriving stock (Generic, Show, Eq)
155153 deriving (FromJSON, ToJSON) via Snake DeploymentMetadata
156154
157155data DeploymentInfo = DeploymentInfo
@@ -159,7 +157,7 @@ data DeploymentInfo = DeploymentInfo
159157 , metadata :: [DeploymentMetadata]
160158 , logs :: [DeploymentLog]
161159 }
162− deriving (Generic, Show)
160+ deriving stock (Generic, Show)
163161 deriving (FromJSON, ToJSON) via Snake DeploymentInfo
164162
165163data DeploymentFullInfo = DeploymentFullInfo
@@ -169,12 +167,12 @@ data DeploymentFullInfo = DeploymentFullInfo
169167 , createdAt :: Int
170168 , updatedAt :: Int
171169 }
172− deriving (Generic, Show, Eq)
170+ deriving stock (Generic, Show, Eq)
173171 deriving (FromJSON, ToJSON) via Snake DeploymentFullInfo
174172
175173isDeploymentArchived :: DeploymentFullInfo -> Bool
176174isDeploymentArchived DeploymentFullInfo {status = s} = case s of
177− DeploymentNotPending s -> isArchivedStatus s
175+ DeploymentNotPending s' -> isArchivedStatus s'
178176 -- if the deployment is currently undergoing some process,
179177 -- then it is not considered archived
180178 DeploymentPending _ -> False
@@ -186,75 +184,66 @@ data DeploymentUpdate = DeploymentUpdate
186184 , newDeploymentOverrides :: DeploymentOverrides
187185 , oldDeploymentOverrides :: DeploymentOverrides
188186 }
189− deriving (Generic, Show)
187+ deriving stock (Generic, Show)
190188 deriving (FromJSON, ToJSON) via Snake DeploymentUpdate
191189
192190data CurrentStatus
193191 = Ok
194192 | Error
195− deriving (Generic, Show, Eq)
193+ deriving stock (Generic, Show, Eq)
196194 deriving (FromJSON, ToJSON) via Snake CurrentStatus
197195
198−newtype CurrentDeploymentStatus =
199− CurrentDeploymentStatus { status :: CurrentStatus }
200− deriving (Generic, Show, Eq)
196+newtype CurrentDeploymentStatus = CurrentDeploymentStatus {status :: CurrentStatus}
197+ deriving stock (Generic, Show, Eq)
201198 deriving (FromJSON, ToJSON) via Snake CurrentDeploymentStatus
202199
203200data CommandResponse
204201 = Success
205202 | ValidationError
206− { nameField :: [Text]
207− , tagField :: [Text]
208− }
203+ { nameField :: [Text]
204+ , tagField :: [Text]
205+ }
209206 | AppError
210− { errorMessage :: Text }
211− deriving (Generic, Show, Eq)
207+ {errorMessage :: Text}
208+ deriving stock (Generic, Show, Eq)
212209 deriving (FromJSON, ToJSON) via Snake CommandResponse
213210
214211data WSEvent = FrontendPleaseUpdateEverything
215− deriving (Generic, Show)
212+ deriving stock (Generic, Show)
216213 deriving (FromJSON, ToJSON) via Snake WSEvent
217214
218−newtype ActionId = ActionId { unActionId :: Int }
219− deriving
215+newtype ActionId = ActionId {unActionId :: Int}
216+ deriving newtype
220217 (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq)
221218
222−newtype Stdout = Stdout { unStdout :: Text }
223− deriving (Generic, Show)
219+newtype Stdout = Stdout {unStdout :: Text}
220+ deriving stock (Generic, Show)
224221 deriving (FromJSON, ToJSON) via Snake Stdout
225222
226−newtype Stderr = Stderr { unStderr :: Text }
227− deriving (Generic, Show)
223+newtype Stderr = Stderr {unStderr :: Text}
224+ deriving stock (Generic, Show)
228225 deriving (FromJSON, ToJSON) via Snake Stderr
229226
230227data ActionInfo = ActionInfo
231228 { stdout :: Text
232229 , stderr :: Text
233230 }
234− deriving (Generic, Show)
231+ deriving stock (Generic, Show)
235232 deriving (FromJSON, ToJSON) via Snake ActionInfo
236233
237−-- | TLS certificate path.
238−newtype TLSCertPath = TLSCertPath { unTLSCertPath :: ByteString }
239− deriving (Show)
240−
241−-- | TLS key path.
242−newtype TLSKeyPath = TLSKeyPath { unTLSKeyPath :: ByteString }
243− deriving (Show)
244−
245234-- | Parses setting application-level overrides.
246−parseSetApplicationOverrides
247− :: OverrideVisibility
248− -> [Text]
249− -> IO [ApplicationOverride]
235+parseSetApplicationOverrides ::
236+ OverrideVisibility ->
237+ [Text] ->
238+ IO [ApplicationOverride]
250239parseSetApplicationOverrides visibility texts =
251240 coerce <$> parseSetOverrides visibility texts
252241
253242-- | Parses setting deployment-level overrides.
254−parseSetDeploymentOverrides
255− :: OverrideVisibility
256− -> [Text]
257− -> IO [DeploymentOverride]
243+parseSetDeploymentOverrides ::
244+ OverrideVisibility ->
245+ [Text] ->
246+ IO [DeploymentOverride]
258247parseSetDeploymentOverrides visibility texts =
259248 coerce <$> parseSetOverrides visibility texts
260249
@@ -264,26 +253,26 @@ parseSetOverrides visibility texts =
264253 for texts $ \t ->
265254 case T.findIndex (== '=') t of
266255 Just i -> do
267− let
268− (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t
256+ let (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t
269257 pure $ Override key value visibility
270− Nothing -> error $
271− "Malformed override key-value pair " <> T.unpack t <>
272− ", should be similar to FOO=bar"
258+ Nothing ->
259+ error $
260+ "Malformed override key-value pair " <> T.unpack t
261+ <> ", should be similar to FOO=bar"
273262
274263-- | Parses unsetting application-level overrides.
275−parseUnsetApplicationOverrides
276− :: OverrideVisibility
277− -> [Text]
278− -> IO [ApplicationOverride]
264+parseUnsetApplicationOverrides ::
265+ OverrideVisibility ->
266+ [Text] ->
267+ IO [ApplicationOverride]
279268parseUnsetApplicationOverrides visibility texts =
280269 coerce <$> parseUnsetOverrides visibility texts
281270
282271-- | Parses unsetting deployment-level overrides.
283−parseUnsetDeploymentOverrides
284− :: OverrideVisibility
285− -> [Text]
286− -> IO [DeploymentOverride]
272+parseUnsetDeploymentOverrides ::
273+ OverrideVisibility ->
274+ [Text] ->
275+ IO [DeploymentOverride]
287276parseUnsetDeploymentOverrides visibility texts =
288277 coerce <$> parseUnsetOverrides visibility texts
289278
octopod-common/src/Common/Utils.hs+24−16
@@ -1,18 +1,22 @@
1−{-|
2−Module : Common.Utils
3−Description : Common utils for backend and frontend.
4−
5−This module contains common utils between the backend and the frontend.
6−-}
7−
8−
9−module Common.Utils where
10−
11−import Control.Lens
12−import Data.Generics.Product
13−import Data.Monoid
14−
15−import Common.Types
1+-- |
2+--Module : Common.Utils
3+--Description : Common utils for backend and frontend.
4+--
5+--This module contains common utils between the backend and the frontend.
6+module Common.Utils
7+ ( (<^.>),
8+ (<^?>),
9+ (<^..>),
10+ dfiName,
11+ isPending,
12+ )
13+where
14+
15+import Control.Lens
16+import Data.Generics.Product
17+import Data.Monoid
18+
19+import Common.Types
1620
1721-- Lens convenience helpers
1822(<^.>) :: Functor f => f a -> Getting b a b -> f b
@@ -27,7 +31,11 @@ import Common.Types
2731infixl 8 <^.>, <^..>, <^?>
2832
2933-- | Gets name from deployment full info.
30−dfiName :: Getter DeploymentFullInfo DeploymentName
34+dfiName ::
35+ Functor f =>
36+ (DeploymentName -> f DeploymentName) ->
37+ DeploymentFullInfo ->
38+ f DeploymentFullInfo
3139dfiName = field @"deployment" . field @"name"
3240
3341-- | Checks that deployment status is pending.
octopod-common/src/Common/Validation.hs+15−15
@@ -1,20 +1,20 @@
1−{-|
2−Module : Common.Validation
3−Description : Common validations for backend and frontend.
1+-- |
2+--Module : Common.Validation
3+--Description : Common validations for backend and frontend.
4+--
5+--This module contains common validations between the backend and the frontend.
6+module Common.Validation
7+ ( isNameValid,
8+ )
9+where
410
5−This module contains common validations between the backend and the frontend.
6−-}
11+import Data.ByteString (ByteString)
12+import Data.Text.Encoding as T (encodeUtf8)
13+import Text.Regex.TDFA
714
8−
9−module Common.Validation where
10−
11−import Data.ByteString (ByteString)
12−import Data.Text.Encoding as T (encodeUtf8)
13−import Text.Regex.TDFA
14−
15−import Common.Types
15+import Common.Types
1616
1717-- | Validates a deployment name.
1818isNameValid :: DeploymentName -> Bool
19−isNameValid (DeploymentName name) =
20− (T.encodeUtf8 name =~ ("^[a-z][a-z0-9\\-]{1,16}$" :: ByteString))
19+isNameValid (DeploymentName n) =
20+ T.encodeUtf8 n =~ ("^[a-z][a-z0-9\\-]{1,16}$" :: ByteString)
octopod-frontend/octopod-frontend.cabal+16−8
@@ -37,11 +37,22 @@ executable frontend
3737 , Page.Elements.Links
3838 , Reflex.MultiEventWriter.Class
3939 , Data.Text.Search
40− ghc-options: -Wall
41− -Werror
42− -Wno-missing-home-modules
43− -Wno-error=missing-home-modules
44− -threaded
40+ ghc-options:
41+ -Weverything
42+ -Wno-implicit-prelude
43+ -Wno-missing-safe-haskell-mode
44+ -Wno-safe
45+ -Wno-prepositive-qualified-module
46+ -Wno-missing-import-lists
47+ -Wno-all-missed-specialisations
48+ -Wno-missing-local-signatures
49+ -Wno-partial-fields
50+ -Wno-unsafe
51+ -Wno-monomorphism-restriction
52+ -Wno-missed-specialisations
53+ -- Because reflex-dom is a false positive.
54+ -Wno-unused-packages
55+ -threaded
4556 default-extensions: BlockArguments
4657 , ConstraintKinds
4758 , DataKinds
@@ -70,19 +81,16 @@ executable frontend
7081 , FunctionalDependencies
7182 , AllowAmbiguousTypes
7283 , OverloadedLabels
73− -- other-extensions:
7484 build-depends: aeson
7585 , base
7686 , bytestring
7787 , containers
7888 , deriving-aeson
79− , dependent-sum
8089 , octopod-common
8190 , octopod-api
8291 , exceptions
8392 , generic-lens
8493 , ghcjs-dom
85− , jsaddle
8694 , lens
8795 , obelisk-route
8896 , reflex-dom
octopod-frontend/src/Data/Text/Search.hs+16−15
@@ -1,14 +1,15 @@
11module Data.Text.Search
2− ( fuzzySearch
3− , FuzzySearchStringChunk(..)
4− ) where
2+ ( fuzzySearch,
3+ FuzzySearchStringChunk (..),
4+ )
5+where
56
6−import Control.Applicative
7−import Data.Bifunctor
8−import Data.Char
9−import Data.Function
10−import Data.List
11−import Data.Text (Text)
7+import Control.Applicative
8+import Data.Bifunctor
9+import Data.Char
10+import Data.Function
11+import qualified Data.List as L
12+import Data.Text (Text)
1213import qualified Data.Text as T
1314
1415type Needle = Text
@@ -20,10 +21,10 @@ fuzzySearch' :: Needle -> Haystack -> Running -> Penalty -> [([FuzzySearchString
2021fuzzySearch' needle haystack i p = case (T.uncons needle, T.uncons haystack) of
2122 (Just _, Nothing) -> []
2223 (Just (n, eedle), Just (h, aystack)) ->
23− bimap (prependNotMatched h) (+ (i - p)) <$> fuzzySearch' needle aystack 0 p
24− <|> if toLower n == toLower h
25− then first (prependMatched h) <$> fuzzySearch' eedle aystack (i * 2 + 1) 1
26− else empty
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
2728 (Nothing, Nothing) -> [([], i)]
2829 (Nothing, Just _) -> [([NotMatched $ T.unpack haystack], i)]
2930
@@ -41,7 +42,7 @@ fuzzySearch :: Needle -> Haystack -> Maybe ([FuzzySearchStringChunk Text], Int)
4142fuzzySearch "" h = Just ([NotMatched h], 0)
4243fuzzySearch n h = case fuzzySearch' n h 0 0 of
4344 [] -> Nothing
44− xs@(_:_) -> Just . (first . fmap . fmap) T.pack $ maximumBy (compare `on` snd) xs
45+ xs@(_ : _) -> Just . (first . fmap . fmap) T.pack $ L.maximumBy (compare `on` snd) xs
4546
4647data FuzzySearchStringChunk a = NotMatched !a | Matched !a
47− deriving (Show, Eq, Functor)
48+ deriving stock (Show, Eq, Functor)
octopod-frontend/src/Frontend/API.hs+123−105
@@ -1,37 +1,50 @@
1−{-|
2−Module : Frontend.API
3−Description : API requests.
4−
5−This module contains client functions that perform API requests (these functions
6−are generated from the servant API) and functions that work with request
7−results.
8−-}
9−
10−{-# LANGUAGE CPP #-}
111{-# LANGUAGE NoMonomorphismRestriction #-}
12−{-# LANGUAGE PartialTypeSignatures #-}
132
14−module Frontend.API where
3+-- |
4+--Module : Frontend.API
5+--Description : API requests.
6+--
7+--This module contains client functions that perform API requests (these functions
8+--are generated from the servant API) and functions that work with request
9+--results.
10+module Frontend.API
11+ ( apiClients,
12+ listEndpoint,
13+ createEndpoint,
14+ archiveEndpoint,
15+ updateEndpoint,
16+ infoEndpoint,
17+ fullInfoEndpoint,
18+ statusEndpoint,
19+ restoreEndpoint,
20+ pingEndpoint,
21+ projectName,
22+ commandResponse,
23+ wsPath,
24+ processResp,
25+ )
26+where
1527
16−import Control.Lens
17−import Data.Aeson
18−import Data.Proxy
19−import Data.Text as T
20−import GHC.TypeLits
21−import Reflex.Dom as R
22−import Servant.API as S
23−import Servant.Reflex as SR
24−import Servant.Reflex.Extra
28+import Control.Lens
29+import Data.Aeson
30+import Data.Proxy
31+import Data.Text as T
32+import GHC.TypeLits
33+import Reflex.Dom as R
34+import Servant.API as S
35+import Servant.Reflex as SR
36+import Servant.Reflex.Extra
2537
26−import Common.Types
27−import Frontend.GHCJS
28−import Octopod.API
38+import Common.Types
39+import Frontend.GHCJS
40+import Octopod.API
2941
3042-- | 'apiClients' generates API requests. Before each request the API host and
3143-- API authentication token are read from session storage.
32−apiClients
33− :: forall t m. MonadWidget t m
34− => SR.Client t m API ()
44+apiClients ::
45+ forall t m.
46+ MonadWidget t m =>
47+ SR.Client t m API ()
3548apiClients =
3649 clientWithOpts
3750 (Proxy @API)
@@ -43,83 +56,89 @@ apiClients =
4356 tweakRequest = ClientOptions $ \req -> do
4457 url <- getAppUrl
4558 auth <- getAppAuth
46− pure $ req
47− & xhrRequest_config
48− . xhrRequestConfig_headers
49− . at "Authorization" ?~ auth
50− & xhrRequest_url %~ (T.append url)
59+ pure $
60+ req
61+ & xhrRequest_config
62+ . xhrRequestConfig_headers
63+ . at "Authorization"
64+ ?~ auth
65+ & xhrRequest_url %~ (T.append url)
5166 host = SR.BasePath "/"
5267
53−
54−listEndpoint
55− :: MonadWidget t m
56− => Event t ()
57− -> m (Event t (ReqResult () [DeploymentFullInfo]))
58−createEndpoint
59− :: MonadWidget t m
60− => Dynamic t (Either Text Deployment)
61− -> Event t ()
62− -> m (Event t (ReqResult () CommandResponse))
63−archiveEndpoint
64− :: MonadWidget t m
65− => Dynamic t (Either Text DeploymentName)
66− -> Event t ()
67− -> m (Event t (ReqResult () CommandResponse))
68−updateEndpoint
69− :: MonadWidget t m
70− => Dynamic t (Either Text DeploymentName)
71− -> Dynamic t (Either Text DeploymentUpdate)
72− -> Event t ()
73− -> m (Event t (ReqResult () CommandResponse))
74−infoEndpoint
75− :: MonadWidget t m
76− => Dynamic t (Either Text DeploymentName)
77− -> Event t ()
78− -> m (Event t (ReqResult () [DeploymentInfo]))
79−fullInfoEndpoint
80− :: MonadWidget t m
81− => Dynamic t (Either Text DeploymentName)
82− -> Event t ()
83− -> m (Event t (ReqResult () DeploymentFullInfo))
84−statusEndpoint
85− :: MonadWidget t m
86− => Dynamic t (Either Text DeploymentName)
87− -> Event t ()
88− -> m (Event t (ReqResult () CurrentDeploymentStatus))
89−restoreEndpoint
90− :: MonadWidget t m
91− => Dynamic t (Either Text DeploymentName)
92− -> Event t ()
93− -> m (Event t (ReqResult () CommandResponse))
94−pingEndpoint
95− :: MonadWidget t m
96− => Event t ()
97− -> m (Event t (ReqResult () NoContent))
98−projectName
99− :: MonadWidget t m
100− => Event t ()
101− -> m (Event t (ReqResult () ProjectName))
102−(listEndpoint
103− :<|> createEndpoint
104− :<|> archiveEndpoint
105− :<|> updateEndpoint
106− :<|> infoEndpoint
107− :<|> fullInfoEndpoint
108− :<|> statusEndpoint
109− :<|> restoreEndpoint)
68+listEndpoint ::
69+ MonadWidget t m =>
70+ Event t () ->
71+ m (Event t (ReqResult () [DeploymentFullInfo]))
72+createEndpoint ::
73+ MonadWidget t m =>
74+ Dynamic t (Either Text Deployment) ->
75+ Event t () ->
76+ m (Event t (ReqResult () CommandResponse))
77+archiveEndpoint ::
78+ MonadWidget t m =>
79+ Dynamic t (Either Text DeploymentName) ->
80+ Event t () ->
81+ m (Event t (ReqResult () CommandResponse))
82+updateEndpoint ::
83+ MonadWidget t m =>
84+ Dynamic t (Either Text DeploymentName) ->
85+ Dynamic t (Either Text DeploymentUpdate) ->
86+ Event t () ->
87+ m (Event t (ReqResult () CommandResponse))
88+infoEndpoint ::
89+ MonadWidget t m =>
90+ Dynamic t (Either Text DeploymentName) ->
91+ Event t () ->
92+ m (Event t (ReqResult () [DeploymentInfo]))
93+fullInfoEndpoint ::
94+ MonadWidget t m =>
95+ Dynamic t (Either Text DeploymentName) ->
96+ Event t () ->
97+ m (Event t (ReqResult () DeploymentFullInfo))
98+statusEndpoint ::
99+ MonadWidget t m =>
100+ Dynamic t (Either Text DeploymentName) ->
101+ Event t () ->
102+ m (Event t (ReqResult () CurrentDeploymentStatus))
103+restoreEndpoint ::
104+ MonadWidget t m =>
105+ Dynamic t (Either Text DeploymentName) ->
106+ Event t () ->
107+ m (Event t (ReqResult () CommandResponse))
108+pingEndpoint ::
109+ MonadWidget t m =>
110+ Event t () ->
111+ m (Event t (ReqResult () NoContent))
112+projectName ::
113+ MonadWidget t m =>
114+ Event t () ->
115+ m (Event t (ReqResult () ProjectName))
116+( listEndpoint
117+ :<|> createEndpoint
118+ :<|> archiveEndpoint
119+ :<|> updateEndpoint
120+ :<|> infoEndpoint
121+ :<|> fullInfoEndpoint
122+ :<|> statusEndpoint
123+ :<|> restoreEndpoint
124+ )
110125 :<|> pingEndpoint
111126 :<|> projectName = apiClients
112127
113128-- | Parser of kubectl command responses.
114−commandResponse
115− :: ReqResult tag CommandResponse -- ^ Response of a kubectl request.
116− -> Maybe CommandResponse
129+commandResponse ::
130+ -- | Response of a kubectl request.
131+ ReqResult tag CommandResponse ->
132+ Maybe CommandResponse
117133commandResponse = \case
118134 ResponseSuccess _ a _ -> Just a
119135 ResponseFailure _ _ xhr ->
120− xhr ^? xhrResponse_response . _Just
121− . to getArrayBufferBody . _Just
122− . to decodeStrict . _Just
136+ xhr
137+ ^? xhrResponse_response . _Just
138+ . to getArrayBufferBody
139+ . _Just
140+ . to decodeStrict
141+ . _Just
123142 RequestFailure _ _ -> Nothing
124143 where
125144 getArrayBufferBody = \case
@@ -131,14 +150,13 @@ wsPath :: Text
131150wsPath = T.pack $ symbolVal (Proxy @ApiWSPath)
132151
133152-- | Helper for event with request result.
134−processResp
135− :: Reflex t
136− => Event t (ReqResult tag a)
137− -- ^ Event with a request result.
138− -> (Event t a, Event t ())
139− -- ^ (Success event with obtained data, failure event).
153+processResp ::
154+ Reflex t =>
155+ -- | Event with a request result.
156+ Event t (ReqResult tag a) ->
157+ -- | (Success event with obtained data, failure event).
158+ (Event t a, Event t ())
140159processResp respEv =
141− let
142− respOkEv = fmapMaybe reqSuccess respEv
143− errEv = fmapMaybe reqErrorBody respEv
144− in (respOkEv, () <$ errEv)
160+ let respOkEv = fmapMaybe reqSuccess respEv
161+ errEv = fmapMaybe reqErrorBody respEv
162+ in (respOkEv, () <$ errEv)
octopod-frontend/src/Frontend/GHCJS.hs+41−28
@@ -1,22 +1,30 @@
1−{-|
2−Module : Frontend.GHCJS
3−Description : GHCJS functions.
4−
5−This module contains all functions that require GHCJS imports.
6−-}
7−
8−module Frontend.GHCJS where
9−
10−import Control.Monad.Catch
11−import Data.Aeson as A (Result(..), Value, fromJSON)
12−import Data.Text as T (Text, pack)
13−import Deriving.Aeson.Stock
14−import GHCJS.DOM
15−import GHCJS.DOM.Response
16−import GHCJS.DOM.Storage
17−import GHCJS.DOM.Types
18−import GHCJS.DOM.Window
1+-- |
2+--Module : Frontend.GHCJS
3+--Description : GHCJS functions.
4+--
5+--This module contains all functions that require GHCJS imports.
6+module Frontend.GHCJS
7+ ( appUrlKey,
8+ wsUrlKey,
9+ appAuthKey,
10+ ProjectConfig (..),
11+ initConfig,
12+ getVar,
13+ getAppUrl,
14+ getAppAuth,
15+ getWsUrl,
16+ )
17+where
1918
19+import Control.Monad.Catch
20+import Data.Aeson as A (Result (..), Value, fromJSON)
21+import Data.Text as T (Text, pack)
22+import Deriving.Aeson.Stock
23+import GHCJS.DOM
24+import GHCJS.DOM.Response
25+import GHCJS.DOM.Storage
26+import GHCJS.DOM.Types
27+import GHCJS.DOM.Window
2028
2129appUrlKey, wsUrlKey, appAuthKey :: JSString
2230appUrlKey = "app"
@@ -29,13 +37,16 @@ appAuthKey = "auth"
2937-- | There are project settings that contain the host for API requests,
3038-- websocket host and API authentication token.
3139data ProjectConfig = ProjectConfig
32− { appUrl :: T.Text -- ^ API host.
33− , wsUrl :: T.Text -- ^ WS host.
34− , appAuth :: T.Text -- ^ API authentication token.
35− , kubernetesDashboardUrlTemplate :: Maybe T.Text
36− -- ^ A k8s dashboard url template to which the name of the deployment is appended.
40+ { -- | API host.
41+ appUrl :: T.Text
42+ , -- | WS host.
43+ wsUrl :: T.Text
44+ , -- | API authentication token.
45+ appAuth :: T.Text
46+ , -- | A k8s dashboard url template to which the name of the deployment is appended.
47+ kubernetesDashboardUrlTemplate :: Maybe T.Text
3748 }
38− deriving (Generic, Show, Eq)
49+ deriving stock (Generic, Show, Eq)
3950 deriving (FromJSON, ToJSON) via Snake ProjectConfig
4051
4152-- | Reads settings from the config file and puts them into session storage.
@@ -63,10 +74,12 @@ initConfig = liftJSM $ catchAll initConfig' (const $ pure Nothing)
6374
6475-- | Gets a value from session storage for the given key.
6576-- Throws an error if key doesn't exist.
66−getVar
67− :: MonadJSM m
68− => JSString -- ^ Key in session storage.
69− -> m T.Text -- ^ Value from session storage.
77+getVar ::
78+ MonadJSM m =>
79+ -- | Key in session storage.
80+ JSString ->
81+ -- | Value from session storage.
82+ m T.Text
7083getVar k = do
7184 w <- currentWindowUnchecked
7285 stor <- getSessionStorage w
octopod-frontend/src/Frontend/Route.hs+37−37
@@ -1,48 +1,48 @@
1−{-|
2−Module : Frontend.Route
3−Description : Router setup.
4−
5−This module contains the type-level routing scheme and the router encoder.
6−Also defines a 'Wrapped' instance for 'DeploymentName'. This helps
7−simplify the router encoder.
8−Routing is provided by the 'obelisk-router' package.
9−-}
10−
11−{-# OPTIONS_GHC -fno-warn-orphans #-}
12−{-# LANGUAGE FlexibleInstances #-}
13−{-# LANGUAGE GADTs #-}
14−{-# LANGUAGE MultiParamTypeClasses #-}
15−{-# LANGUAGE RankNTypes #-}
161{-# LANGUAGE TemplateHaskell #-}
17−{-# LANGUAGE TypeFamilies #-}
18−module Frontend.Route where
19−
20−import Control.Category ((.))
21−import Control.Lens (makeWrapped)
22−import Data.Functor.Identity
23−import Data.Text (Text)
24−import Obelisk.Route
25−import Obelisk.Route.TH
26−import Prelude hiding ((.))
27−
28−import Common.Types
2+{-# OPTIONS_GHC -fno-warn-orphans #-}
293
30−makeWrapped ''DeploymentName
4+-- |
5+--Module : Frontend.Route
6+--Description : Router setup.
7+--
8+--This module contains the type-level routing scheme and the router encoder.
9+--Also defines a 'Wrapped' instance for 'DeploymentName'. This helps
10+--simplify the router encoder.
11+--Routing is provided by the 'obelisk-router' package.
12+module Frontend.Route
13+ ( routeEncoder,
14+ Routes (..),
15+ )
16+where
17+
18+import Control.Category ((.))
19+import Control.Lens (makeWrapped)
20+import Data.Functor.Identity
21+import Data.Text (Text)
22+import Obelisk.Route
23+import Obelisk.Route.TH
24+import Prelude hiding ((.))
25+
26+import Common.Types
27+import Data.Kind
3128
3229-- | Routing scheme definition.
33−data Routes :: * -> * where
30+data Routes :: Type -> Type where
3431 DashboardRoute :: Routes (Maybe DeploymentName)
35−deriving instance Show (Routes a)
3632
37−concat <$> mapM deriveRouteComponent
38− [ ''Routes
33+deriving stock instance Show (Routes a)
34+
35+fmap mconcat . sequence $
36+ [ makeWrapped ''DeploymentName
37+ , deriveRouteComponent ''Routes
3938 ]
4039
4140-- | URL encoder and decoder for 'Routes' scheme.
42−routeEncoder
43− :: Encoder (Either Text) Identity (R Routes) PageName
41+routeEncoder ::
42+ Encoder (Either Text) Identity (R Routes) PageName
4443routeEncoder = handleEncoder (const (DashboardRoute :/ Nothing)) $
4544 pathComponentEncoder $ \case
46− DashboardRoute -> PathSegment "deployments"
47− $ maybeEncoder (unitEncoder mempty)
48− $ singlePathSegmentEncoder . unwrappedEncoder
45+ DashboardRoute ->
46+ PathSegment "deployments" $
47+ maybeEncoder (unitEncoder mempty) $
48+ singlePathSegmentEncoder . unwrappedEncoder
octopod-frontend/src/Frontend/Utils.hs+397−340
@@ -1,114 +1,122 @@
1−{-|
2−Module : Frontend.Utils
3−Description : Client utils and helpers.
4−
5−This module contains common types, functions and operators that are used by
6−frontend modules.
7−-}
1+{-# OPTIONS_GHC -Wno-missing-export-lists #-}
82
3+-- |
4+--Module : Frontend.Utils
5+--Description : Client utils and helpers.
6+--
7+--This module contains common types, functions and operators that are used by
8+--frontend modules.
99module Frontend.Utils where
1010
11−import Control.Lens
12−import Control.Monad
13−import Data.Generics.Labels ()
14−import Data.Map (Map)
15−import Data.Maybe (fromMaybe)
16−import Data.Proxy (Proxy(..))
17−import Data.Text as T (Text, pack)
18−import Data.Time
19−import Data.Time.Clock.POSIX
20−import GHCJS.DOM
21−import GHCJS.DOM.Element as DOM
22−import GHCJS.DOM.EventM (on, target)
23−import GHCJS.DOM.GlobalEventHandlers as Events (click)
24−import GHCJS.DOM.Node as DOM
25−import Reflex.Dom as R
26−
27−import Common.Types as CT
28−import Control.Monad.Reader
29−import Frontend.GHCJS
11+import Control.Lens
12+import Control.Monad
13+import Data.Generics.Labels ()
14+import Data.Map (Map)
15+import Data.Maybe (fromMaybe)
16+import Data.Proxy (Proxy (..))
17+import Data.Text as T (Text, pack)
18+import Data.Time
19+import Data.Time.Clock.POSIX
20+import GHCJS.DOM
21+import GHCJS.DOM.Element as DOM
22+import GHCJS.DOM.EventM (on, target)
23+import GHCJS.DOM.GlobalEventHandlers as Events (click)
24+import GHCJS.DOM.Node as DOM
25+import Reflex.Dom as R
26+
27+import Common.Types as CT
28+import Control.Monad.Reader
29+import Frontend.GHCJS
3030
3131-- | Wrapper for @Maybe DOM.Element@. It's used by 'elementClick'.
32−newtype ClickedElement =
33− ClickedElement { unClickedElement :: Maybe DOM.Element }
32+newtype ClickedElement = ClickedElement {unClickedElement :: Maybe DOM.Element}
3433
3534-- | Returns a click event bound to the current document.
3635-- The event carries 'ClickedElement'.
37−elementClick :: MonadWidget t m => m (Event t ClickedElement)
36+elementClick :: MonadWidget t m => m (Event t ClickedElement)
3837elementClick = do
3938 doc <- currentDocumentUnchecked
4039 wrapDomEvent doc (`on` Events.click) $ ClickedElement <$> target
4140
4241-- | Dropdown widget which binds its own @document click@ event.
43−dropdownWidget
44− :: MonadWidget t m
45− => m ()
46− -- ^ Button widget which opens the dropdown widget.
47− -> m (Event t a)
48− -- ^ Widget with the dropdown list.
42+dropdownWidget ::
43+ MonadWidget t m =>
44+ -- | Button widget which opens the dropdown widget.
45+ m () ->
46+ -- | Widget with the dropdown list.
4947 -- Returns an event carrying the user's selection.
50− -> m (Event t a)
48+ m (Event t a) ->
49+ m (Event t a)
5150dropdownWidget btn body = mdo
5251 clickedEl <- elementClick
5352 dropdownWidget' clickedEl btn body
5453
5554-- | Similar to 'dropdownWidget' but uses @document click@ event that may be
5655-- shared between other widgets.
57−dropdownWidget'
58− :: MonadWidget t m
59− => Event t ClickedElement
60− -- ^ Document click event that may be shared between other widgets.
61− -> m ()
62− -- ^ Button widget which opens the dropdown widget.
63− -> m (Event t a)
64− -- ^ Widget with the dropdown list.
56+dropdownWidget' ::
57+ MonadWidget t m =>
58+ -- | Document click event that may be shared between other widgets.
59+ Event t ClickedElement ->
60+ -- | Button widget which opens the dropdown widget.
61+ m () ->
62+ -- | Widget with the dropdown list.
6563 -- Returns an event carrying the user's selection.
66− -> m (Event t a)
64+ m (Event t a) ->
65+ m (Event t a)
6766dropdownWidget' clickedEl btn body = mdo
68− clickInsideEv <- performEvent $ ffor clickedEl $ \(ClickedElement clicked) ->
69− DOM.contains (_element_raw btnEl) clicked
67+ clickInsideEv <- performEvent $
68+ ffor clickedEl $ \(ClickedElement clicked) ->
69+ DOM.contains (_element_raw btnEl) clicked
7070 openedDyn <- foldDyn switchState False $ clickInsideEv
71− let
72− switchState ev cur = ev && not cur
73− wrapperClassDyn = ffor openedDyn $ \case
74− True -> "class" =: "drop drop--actions drop--expanded"
75− False -> "class" =: "drop drop--actions"
71+ let switchState ev cur = ev && not cur
72+ wrapperClassDyn = ffor openedDyn $ \case
73+ True -> "class" =: "drop drop--actions drop--expanded"
74+ False -> "class" =: "drop drop--actions"
7675 (btnEl, (_, wEv)) <- elDynAttr'
77− "div" wrapperClassDyn $ do
76+ "div"
77+ wrapperClassDyn
78+ $ do
7879 btn
79− elDynAttr' "div"
80− (constDyn $ "class" =: "drop__dropdown") body
80+ elDynAttr'
81+ "div"
82+ (constDyn $ "class" =: "drop__dropdown")
83+ body
8184 pure wEv
8285
8386showT :: Show a => a -> Text
8487showT = pack . show
8588
8689-- | Wrapper for a sidebar that provides opening and closing behavior.
87−sidebar
88− :: MonadWidget t m
89− => Event t d
90− -- ^ Show event with data to be passed to sidebar.
91− -> Event t ()
92− -- ^ Close event.
93− -> (d -> m (Event t a, Event t ()))
94− -- ^ Sidebar body which returns data with first event and close event.
95− -> m (Event t a)
90+sidebar ::
91+ MonadWidget t m =>
92+ -- | Show event with data to be passed to sidebar.
93+ Event t d ->
94+ -- | Close event.
95+ Event t () ->
96+ -- | Sidebar body which returns data with first event and close event.
97+ (d -> m (Event t a, Event t ())) ->
98+ m (Event t a)
9699sidebar showEv closeEv m = mdo
97− let
98− blank' = pure (never, never)
99− selfCloseEv = switchDyn $ snd <$> resultEvDyn
100− closeEv' = leftmost
101− [ closeEv, selfCloseEv ]
102− animationDuration = 0.3
100+ let blank' = pure (never, never)
101+ selfCloseEv = switchDyn $ snd <$> resultEvDyn
102+ closeEv' =
103+ leftmost
104+ [closeEv, selfCloseEv]
105+ animationDuration = 0.3
103106 deferDomClearEv <- delay animationDuration closeEv'
104− popupClassDyn <- holdDyn "popup" $ leftmost
105− [ "popup" <$ closeEv'
106− , "popup popup--visible" <$ showEv ]
107+ popupClassDyn <-
108+ holdDyn "popup" $
109+ leftmost
110+ [ "popup" <$ closeEv'
111+ , "popup popup--visible" <$ showEv
112+ ]
107113 resultEvDyn <- elDynClass "div" popupClassDyn $ do
108114 popupOverlay
109− widgetHold blank' $ leftmost
110− [ m <$> showEv
111− , blank' <$ deferDomClearEv ]
115+ widgetHold blank' $
116+ leftmost
117+ [ m <$> showEv
118+ , blank' <$ deferDomClearEv
119+ ]
112120 pure $ switchDyn $ fst <$> resultEvDyn
113121
114122-- | Dark unclickable background for opened sidebar.
@@ -117,157 +125,172 @@ popupOverlay =
117125 elAttr "div" ("class" =: "popup__overlay" <> "aria-hidden" =: "true") blank
118126
119127-- | Button with customizable classes and label text.
120−buttonClass
121− :: (DomBuilder t m, PostBuild t m)
122− => Text
123− -- ^ Classes.
124− -> Text
125− -- ^ Label text.
126− -> m (Event t ())
128+buttonClass ::
129+ (DomBuilder t m, PostBuild t m) =>
130+ -- | Classes.
131+ Text ->
132+ -- | Label text.
133+ Text ->
134+ m (Event t ())
127135buttonClass cl lbl = do
128− (bEl, _) <- elDynAttr' "button"
129− (constDyn $ "class" =: cl <> "type" =: "button") $ text lbl
136+ (bEl, _) <-
137+ elDynAttr'
138+ "button"
139+ (constDyn $ "class" =: cl <> "type" =: "button")
140+ $ text lbl
130141 return $ domEvent Click bEl
131142
132143-- | Advanced version of 'buttonClass' with dynamic arguments.
133−buttonDynClass
134− :: (DomBuilder t m, PostBuild t m)
135− => Dynamic t Text
136− -- ^ Classes.
137− -> Dynamic t Text
138− -- ^ Label text.
139− -> m (Event t ())
144+buttonDynClass ::
145+ (DomBuilder t m, PostBuild t m) =>
146+ -- | Classes.
147+ Dynamic t Text ->
148+ -- | Label text.
149+ Dynamic t Text ->
150+ m (Event t ())
140151buttonDynClass clDyn lblDyn = do
141152 let attrDyn = ffor clDyn $ \cl -> "class" =: cl <> "type" =: "button"
142− (bEl, _) <- elDynAttr' "button" attrDyn $
143− dynText lblDyn
153+ (bEl, _) <-
154+ elDynAttr' "button" attrDyn $
155+ dynText lblDyn
144156 return $ domEvent Click bEl
145157
146158-- | Advanced version of 'buttonClass' with a disabled state.
147−buttonClassEnabled
148− :: (DomBuilder t m, PostBuild t m)
149− => Text
150− -- ^ Classes.
151− -> Text
152− -- ^ Label text.
153− -> Dynamic t Bool
154− -- ^ Enabled flag.
155− -> m (Event t ())
159+buttonClassEnabled ::
160+ (DomBuilder t m, PostBuild t m) =>
161+ -- | Classes.
162+ Text ->
163+ -- | Label text.
164+ Text ->
165+ -- | Enabled flag.
166+ Dynamic t Bool ->
167+ m (Event t ())
156168buttonClassEnabled cl lbl dDyn = do
157− let
158− attrDyn = ffor dDyn $ \case
159− True -> "class" =: cl <> "type" =: "button"
160− False -> "class" =: (cl <> " button--disabled")
161− <> "type" =: "button" <> "disabled" =: ""
162− (bEl, _) <- elDynAttr' "button" attrDyn $
163− text lbl
169+ let attrDyn = ffor dDyn $ \case
170+ True -> "class" =: cl <> "type" =: "button"
171+ False ->
172+ "class" =: (cl <> " button--disabled")
173+ <> "type" =: "button"
174+ <> "disabled" =: ""
175+ (bEl, _) <-
176+ elDynAttr' "button" attrDyn $
177+ text lbl
164178 return $ domEvent Click bEl
165179
166180-- | Special version of 'buttonClassEnabled' that supports custom classes for
167181-- the disabled state.
168−buttonClassEnabled'
169− :: (DomBuilder t m, PostBuild t m)
170− => Text
171− -- ^ Classes.
172− -> Text
173− -- ^ Label text.
174− -> Dynamic t Bool
175− -- ^ Enabled flag.
176− -> Text
177− -- ^ Custom classes for disabled state.
178− -> m (Event t ())
182+buttonClassEnabled' ::
183+ (DomBuilder t m, PostBuild t m) =>
184+ -- | Classes.
185+ Text ->
186+ -- | Label text.
187+ Text ->
188+ -- | Enabled flag.
189+ Dynamic t Bool ->
190+ -- | Custom classes for disabled state.
191+ Text ->
192+ m (Event t ())
179193buttonClassEnabled' cl lbl dDyn disClass = do
180− let
181− attrDyn = ffor dDyn $ \case
182− True -> "class" =: cl <> "type" =: "button"
183− False -> "class" =: (cl <> " " <> disClass)
184− <> "type" =: "button" <> "disabled" =: ""
185− (bEl, _) <- elDynAttr' "button" attrDyn $
186− text lbl
194+ let attrDyn = ffor dDyn $ \case
195+ True -> "class" =: cl <> "type" =: "button"
196+ False ->
197+ "class" =: (cl <> " " <> disClass)
198+ <> "type" =: "button"
199+ <> "disabled" =: ""
200+ (bEl, _) <-
201+ elDynAttr' "button" attrDyn $
202+ text lbl
187203 return $ domEvent Click bEl
188204
189205-- | Version of 'buttonClass' for links that should look like buttons.
190−aButtonClass
191− :: (DomBuilder t m, PostBuild t m)
192− => Text
193− -- ^ Classes.
194− -> Text
195− -- ^ Label text.
196− -> m (Event t ())
206+aButtonClass ::
207+ (DomBuilder t m, PostBuild t m) =>
208+ -- | Classes.
209+ Text ->
210+ -- | Label text.
211+ Text ->
212+ m (Event t ())
197213aButtonClass cl lbl = do
198− (bEl, _) <- elDynAttr' "a"
199− (constDyn $ "class" =: cl) $ text lbl
214+ (bEl, _) <-
215+ elDynAttr'
216+ "a"
217+ (constDyn $ "class" =: cl)
218+ $ text lbl
200219 return $ domEvent Click bEl
201220
202221-- | Version of 'buttonClass' for links that should look like buttons.
203−aButtonClass'
204− :: (DomBuilder t m, PostBuild t m)
205− => Text
206− -- ^ Classes.
207− -> Text
208− -- ^ Label text.
209− -> Dynamic t (Map Text Text)
210− -- ^ Extra attributes
211− -> m (Event t ())
222+aButtonClass' ::
223+ (DomBuilder t m, PostBuild t m) =>
224+ -- | Classes.
225+ Text ->
226+ -- | Label text.
227+ Text ->
228+ -- | Extra attributes
229+ Dynamic t (Map Text Text) ->
230+ m (Event t ())
212231aButtonClass' cl lbl eAttrs = do
213− (bEl, _) <- elDynAttr' "a"
214− (fmap (<> ("class" =: cl)) eAttrs) $ text lbl
232+ (bEl, _) <-
233+ elDynAttr'
234+ "a"
235+ (fmap (<> ("class" =: cl)) eAttrs)
236+ $ text lbl
215237 return $ domEvent Click bEl
216238
217239-- | Version of 'buttonDynClass' for links that should look like
218240-- buttons.
219−aButtonDynClass
220− :: (DomBuilder t m, PostBuild t m)
221− => Dynamic t Text
222− -- ^ Classes.
223− -> Dynamic t Text
224− -- ^ Label text.
225− -> m (Event t ())
241+aButtonDynClass ::
242+ (DomBuilder t m, PostBuild t m) =>
243+ -- | Classes.
244+ Dynamic t Text ->
245+ -- | Label text.
246+ Dynamic t Text ->
247+ m (Event t ())
226248aButtonDynClass clDyn lblDyn = do
227249 let attrDyn = ffor clDyn $ \cl -> "class" =: cl
228− (bEl, _) <- elDynAttr' "a" attrDyn $
229− dynText lblDyn
250+ (bEl, _) <-
251+ elDynAttr' "a" attrDyn $
252+ dynText lblDyn
230253 return $ domEvent Click bEl
231254
232−
233255-- | Version of 'buttonDynClass' for links that should look like
234256-- buttons.
235−aButtonDynClass'
236− :: (DomBuilder t m, PostBuild t m)
237− => Dynamic t Text
238− -- ^ Classes.
239− -> Dynamic t Text
240− -- ^ Label text.
241− -> Dynamic t (Map Text Text)
242− -- ^ Extra attributes
243− -> m (Event t ())
257+aButtonDynClass' ::
258+ (DomBuilder t m, PostBuild t m) =>
259+ -- | Classes.
260+ Dynamic t Text ->
261+ -- | Label text.
262+ Dynamic t Text ->
263+ -- | Extra attributes
264+ Dynamic t (Map Text Text) ->
265+ m (Event t ())
244266aButtonDynClass' clDyn lblDyn eAttrs = do
245− let
246− attrDyn' = ffor clDyn $ \cl -> "class" =: cl
247− attrDyn = (<>) <$> eAttrs <*> attrDyn'
248− (bEl, _) <- elDynAttr' "a" attrDyn $
249− dynText lblDyn
267+ let attrDyn' = ffor clDyn $ \cl -> "class" =: cl
268+ attrDyn = (<>) <$> eAttrs <*> attrDyn'
269+ (bEl, _) <-
270+ elDynAttr' "a" attrDyn $
271+ dynText lblDyn
250272 return $ domEvent Click bEl
251273
252274-- | Version of 'buttonClassEnabled' for links that should look like
253275-- buttons.
254−aButtonClassEnabled
255− :: (DomBuilder t m, PostBuild t m)
256− => Text
257− -- ^ Classes.
258− -> Text
259− -- ^ Label text.
260− -> Dynamic t Bool
261− -- ^ Enabled flag.
262− -> m (Event t ())
276+aButtonClassEnabled ::
277+ (DomBuilder t m, PostBuild t m) =>
278+ -- | Classes.
279+ Text ->
280+ -- | Label text.
281+ Text ->
282+ -- | Enabled flag.
283+ Dynamic t Bool ->
284+ m (Event t ())
263285aButtonClassEnabled cl lbl dDyn = do
264− let
265− attrDyn = ffor dDyn $ \case
266− True -> "class" =: cl
267− False -> "class" =: (cl <> " button--disabled")
268− <> "disabled" =: ""
269− (bEl, _) <- elDynAttr' "a" attrDyn $
270− text lbl
286+ let attrDyn = ffor dDyn $ \case
287+ True -> "class" =: cl
288+ False ->
289+ "class" =: (cl <> " button--disabled")
290+ <> "disabled" =: ""
291+ (bEl, _) <-
292+ elDynAttr' "a" attrDyn $
293+ text lbl
271294 return $ domEvent Click bEl
272295
273296-- | Converter from posix seconds to `UTCTime`.
@@ -277,42 +300,54 @@ intToUTCTime = posixSecondsToUTCTime . realToFrac
277300-- | Taken from <https://gist.github.com/3noch/134b1ee7fa48c347be9d164c3fac4ef7>
278301--
279302-- Like 'elDynAttr'' but configures "prevent default" on the given event.
280−elDynAttrWithPreventDefaultEvent'
281− :: forall a en m t. (DomBuilder t m, PostBuild t m)
282− => EventName en
283− -- ^ Event on the element to configure with 'preventDefault'
284− -> Text
285− -- ^ Element tag
286− -> Dynamic t (Map Text Text)
287− -- ^ Element attributes
288− -> m a
289− -- ^ Child of element
290− -> m (R.Element EventResult (DomBuilderSpace m) t, a)
291− -- ^ An element and the result of the child
292−elDynAttrWithPreventDefaultEvent' ev = elDynAttrWithModifyConfig'
293− (\elCfg -> elCfg & elementConfig_eventSpec %~
294− addEventSpecFlags
295− (Proxy :: Proxy (DomBuilderSpace m)) ev (const preventDefault))
303+elDynAttrWithPreventDefaultEvent' ::
304+ forall a en m t.
305+ (DomBuilder t m, PostBuild t m) =>
306+ -- | Event on the element to configure with 'preventDefault'
307+ EventName en ->
308+ -- | Element tag
309+ Text ->
310+ -- | Element attributes
311+ Dynamic t (Map Text Text) ->
312+ -- | Child of element
313+ m a ->
314+ -- | An element and the result of the child
315+ m (R.Element EventResult (DomBuilderSpace m) t, a)
316+elDynAttrWithPreventDefaultEvent' ev =
317+ elDynAttrWithModifyConfig'
318+ ( \elCfg ->
319+ elCfg & elementConfig_eventSpec
320+ %~ addEventSpecFlags
321+ (Proxy :: Proxy (DomBuilderSpace m))
322+ ev
323+ (const preventDefault)
324+ )
296325
297326-- | Taken from <https://gist.github.com/3noch/134b1ee7fa48c347be9d164c3fac4ef7>
298327--
299328-- Like 'elDynAttr'' but configures "stop propagation" on the given event.
300−elDynAttrWithStopPropagationEvent'
301− :: forall a en m t. (DomBuilder t m, PostBuild t m)
302− => EventName en
303− -- ^ Event on the element to configure with 'preventDefault'
304− -> Text
305− -- ^ Element tag
306− -> Dynamic t (Map Text Text)
307− -- ^ Element attributes
308− -> m a
309− -- ^ Child of element
310− -> m (R.Element EventResult (DomBuilderSpace m) t, a)
311− -- ^ An element and the result of the child
312−elDynAttrWithStopPropagationEvent' ev = elDynAttrWithModifyConfig'
313− (\elCfg -> elCfg & elementConfig_eventSpec %~
314− addEventSpecFlags
315− (Proxy :: Proxy (DomBuilderSpace m)) ev (const stopPropagation))
329+elDynAttrWithStopPropagationEvent' ::
330+ forall a en m t.
331+ (DomBuilder t m, PostBuild t m) =>
332+ -- | Event on the element to configure with 'preventDefault'
333+ EventName en ->
334+ -- | Element tag
335+ Text ->
336+ -- | Element attributes
337+ Dynamic t (Map Text Text) ->
338+ -- | Child of element
339+ m a ->
340+ -- | An element and the result of the child
341+ m (R.Element EventResult (DomBuilderSpace m) t, a)
342+elDynAttrWithStopPropagationEvent' ev =
343+ elDynAttrWithModifyConfig'
344+ ( \elCfg ->
345+ elCfg & elementConfig_eventSpec
346+ %~ addEventSpecFlags
347+ (Proxy :: Proxy (DomBuilderSpace m))
348+ ev
349+ (const stopPropagation)
350+ )
316351
317352-- | Taken from <https://gist.github.com/3noch/134b1ee7fa48c347be9d164c3fac4ef7>
318353--
@@ -320,19 +355,19 @@ elDynAttrWithStopPropagationEvent' ev = elDynAttrWithModifyConfig'
320355--
321356-- Special thanks to @luigy:
322357-- <https://gist.github.com/luigy/b49ce04de8462e594c9c2b5b455ae5a5#file-foo-hs>
323−elDynAttrWithModifyConfig'
324− :: (DomBuilder t m, PostBuild t m)
325− => (ElementConfig EventResult t (DomBuilderSpace m)
326− -> ElementConfig EventResult t (DomBuilderSpace m))
327− -> Text
328− -> Dynamic t (Map Text Text)
329− -> m a
330− -> m (R.Element EventResult (DomBuilderSpace m) t, a)
358+elDynAttrWithModifyConfig' ::
359+ (DomBuilder t m, PostBuild t m) =>
360+ ( ElementConfig EventResult t (DomBuilderSpace m) ->
361+ ElementConfig EventResult t (DomBuilderSpace m)
362+ ) ->
363+ Text ->
364+ Dynamic t (Map Text Text) ->
365+ m a ->
366+ m (R.Element EventResult (DomBuilderSpace m) t, a)
331367elDynAttrWithModifyConfig' f elementTag attrs child = do
332368 modifyAttrs <- dynamicAttributesToModifyAttributes attrs
333− let
334− cfg =
335− def & modifyAttributes .~ fmapCheap mapKeysToAttributeName modifyAttrs
369+ let cfg =
370+ def & modifyAttributes .~ fmapCheap mapKeysToAttributeName modifyAttrs
336371 result <- R.element elementTag (f cfg) child
337372 postBuild <- getPostBuild
338373 notReadyUntil postBuild
@@ -340,45 +375,47 @@ elDynAttrWithModifyConfig' f elementTag attrs child = do
340375
341376-- | Formats posix seconds to date in iso8601.
342377formatPosixToDate :: Int -> Text
343−formatPosixToDate = pack
344− . formatTime defaultTimeLocale (iso8601DateFormat Nothing)
345− . intToUTCTime
378+formatPosixToDate =
379+ pack
380+ . formatTime defaultTimeLocale (iso8601DateFormat Nothing)
381+ . intToUTCTime
346382
347383-- | Formats posix seconds to date in iso8601 with time.
348384formatPosixToDateTime :: Int -> Text
349−formatPosixToDateTime = pack
350− . formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S"))
351− . intToUTCTime
385+formatPosixToDateTime =
386+ pack
387+ . formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S"))
388+ . intToUTCTime
352389
353390-- | Widget displaying the current deployment status.
354391statusWidget :: MonadWidget t m => Dynamic t PreciseDeploymentStatus -> m ()
355392statusWidget stDyn = do
356393 stDyn' <- holdUniqDyn stDyn
357− let
358− loadingWidget = divClass "loading loading--status-alike"
359− dyn_ $ stDyn' <&> \case
360− DeploymentPending _ -> divClass "status status--pending" $ text "Pending..."
361− DeploymentNotPending Running -> divClass "status status--success" $ text "Running"
362− DeploymentNotPending (Failure _) -> divClass "status status--failure" $ text "Failure"
363− DeploymentNotPending CreatePending -> loadingWidget $ text "Creating..."
364− DeploymentNotPending UpdatePending -> loadingWidget $ text "Updating..."
365− DeploymentNotPending ArchivePending -> loadingWidget $ text "Archiving..."
366− DeploymentNotPending Archived -> divClass "status status--archived" $ text "Archived"
394+ let loadingWidget = divClass "loading loading--status-alike"
395+ dyn_ $
396+ stDyn' <&> \case
397+ DeploymentPending _ -> divClass "status status--pending" $ text "Pending..."
398+ DeploymentNotPending Running -> divClass "status status--success" $ text "Running"
399+ DeploymentNotPending (Failure _) -> divClass "status status--failure" $ text "Failure"
400+ DeploymentNotPending CreatePending -> loadingWidget $ text "Creating..."
401+ DeploymentNotPending UpdatePending -> loadingWidget $ text "Updating..."
402+ DeploymentNotPending ArchivePending -> loadingWidget $ text "Archiving..."
403+ DeploymentNotPending Archived -> divClass "status status--archived" $ text "Archived"
367404
368405-- | Text input field with label.
369−octopodTextInput
370− :: MonadWidget t m
371− => Text
372− -- ^ Input field classes.
373− -> Text
374− -- ^ Label text.
375− -> Text
376− -- ^ Placeholder for input field.
377− -> Maybe Text
378− -- ^ Initial value.
379− -> Event t Text
380− -- ^ Event carrying error message.
381− -> m (Dynamic t Text, Dynamic t Bool)
406+octopodTextInput ::
407+ MonadWidget t m =>
408+ -- | Input field classes.
409+ Text ->
410+ -- | Label text.
411+ Text ->
412+ -- | Placeholder for input field.
413+ Text ->
414+ -- | Initial value.
415+ Maybe Text ->
416+ -- | Event carrying error message.
417+ Event t Text ->
418+ m (Dynamic t Text, Dynamic t Bool)
382419octopodTextInput clss lbl placeholder val errEv =
383420 elClass "section" "deployment__section" $ do
384421 elClass "h3" "deployment__sub-heading" $ text lbl
@@ -387,37 +424,47 @@ octopodTextInput clss lbl placeholder val errEv =
387424
388425-- | The only text input field that is used in project forms. This input
389426-- provides automatic error message hiding after user starts typing.
390−octopodTextInput'
391− :: MonadWidget t m
392− => Text
393− -- ^ Input field classes.
394− -> Text
395− -- ^ Placeholder for input field.
396− -> Maybe Text
397− -- ^ Possible init value.
398− -> Event t Text
399− -- ^ Event carrying the error message.
400− -> m (Dynamic t Text, Dynamic t Bool)
427+octopodTextInput' ::
428+ MonadWidget t m =>
429+ -- | Input field classes.
430+ Text ->
431+ -- | Placeholder for input field.
432+ Text ->
433+ -- | Possible init value.
434+ Maybe Text ->
435+ -- | Event carrying the error message.
436+ Event t Text ->
437+ m (Dynamic t Text, Dynamic t Bool)
401438octopodTextInput' clss placeholder val errEv = mdo
402− let
403− inpClass = " input"
404− inpErrClass = " input input--error"
405− isValid <- holdDyn True $ leftmost
406− [ False <$ errEv
407− , True <$ updated valDyn ]
408− classDyn <- holdDyn (clss <> inpClass) $ leftmost
409− [ (clss <> inpErrClass) <$ errEv
410− , (clss <> inpClass) <$ updated valDyn ]
439+ let inpClass = " input"
440+ inpErrClass = " input input--error"
441+ isValid <-
442+ holdDyn True $
443+ leftmost
444+ [ False <$ errEv
445+ , True <$ updated valDyn
446+ ]
447+ classDyn <-
448+ holdDyn (clss <> inpClass) $
449+ leftmost
450+ [ (clss <> inpErrClass) <$ errEv
451+ , (clss <> inpClass) <$ updated valDyn
452+ ]
411453 valDyn <- elDynClass "div" classDyn $ do
412− inp <- inputElement $ def
413− & initialAttributes .~
414− ( "type" =: "text"
415− <> "class" =: "input__widget"
416− <> "placeholder" =: placeholder )
417− & inputElementConfig_initialValue .~ fromMaybe "" val
418− widgetHold_ blank $ leftmost
419− [ divClass "input__output" . text <$> errEv
420− , blank <$ updated valDyn ]
454+ inp <-
455+ inputElement $
456+ def
457+ & initialAttributes
458+ .~ ( "type" =: "text"
459+ <> "class" =: "input__widget"
460+ <> "placeholder" =: placeholder
461+ )
462+ & inputElementConfig_initialValue .~ fromMaybe "" val
463+ widgetHold_ blank $
464+ leftmost
465+ [ divClass "input__output" . text <$> errEv
466+ , blank <$ updated valDyn
467+ ]
421468 pure $ value inp
422469 pure (valDyn, isValid)
423470
@@ -437,27 +484,30 @@ errorCommonWidget =
437484
438485-- | Widget that can show and hide overrides if there are more than 3. This
439486-- widget is used in the deployments table and the deployment action table.
440−overridesWidget
441− :: MonadWidget t m
442− => Overrides -- ^ List of overrides.
443− -> m ()
487+overridesWidget ::
488+ MonadWidget t m =>
489+ -- | List of overrides.
490+ Overrides ->
491+ m ()
444492overridesWidget envs = divClass "listing listing--for-text" $ do
445− let
446− visible = take 3 envs
447− envLength = length envs
493+ let visible = take 3 envs
494+ envLength = length envs
448495 listing visible
449496 when (envLength > 3) $ mdo
450497 let hidden = drop 3 envs
451498 showDyn <- toggle False toggleEv
452− dyn_ $ showDyn <&> \case
453− True -> listing hidden
454− False -> blank
455− let
456− btnClassDyn = ifThenElseDyn showDyn
457− "listing__more expander expander--open"
458− "listing__more expander"
459− btnTextDyn = ifThenElseDyn showDyn "Hide"
460− $ "Show all (" <> (showT $ envLength) <> ")"
499+ dyn_ $
500+ showDyn <&> \case
501+ True -> listing hidden
502+ False -> blank
503+ let btnClassDyn =
504+ ifThenElseDyn
505+ showDyn
506+ "listing__more expander expander--open"
507+ "listing__more expander"
508+ btnTextDyn =
509+ ifThenElseDyn showDyn "Hide" $
510+ "Show all (" <> (showT $ envLength) <> ")"
461511 toggleEv <- buttonDynClass btnClassDyn btnTextDyn
462512 blank
463513 where
@@ -468,49 +518,56 @@ overridesWidget envs = divClass "listing listing--for-text" $ do
468518 text val
469519
470520-- | @if-then-else@ helper for cases when bool value is wrapped in 'Dynamic'.
471−ifThenElseDyn
472− :: Reflex t
473− => Dynamic t Bool
474− -- ^ Condition wrapped in `Dynamic`.
475− -> b
476− -- ^ `then` branch.
477− -> b
478− -- ^ `else` branch.
479− -> Dynamic t b
480−ifThenElseDyn bDyn t f = bDyn <&> \case
481− True -> t
482− False -> f
521+ifThenElseDyn ::
522+ Reflex t =>
523+ -- | Condition wrapped in `Dynamic`.
524+ Dynamic t Bool ->
525+ -- | `then` branch.
526+ b ->
527+ -- | `else` branch.
528+ b ->
529+ Dynamic t b
530+ifThenElseDyn bDyn t f =
531+ bDyn <&> \case
532+ True -> t
533+ False -> f
483534
484535-- | Type of notification at the top of pages.
485536data DeploymentPageNotification
486− = DPMOk Text -- ^ Message text for a \"success\" events.
487− | DPMError Text -- ^ Message text for an \"error\" events.
488− | DPMClear -- ^ Clear notification widget.
537+ = -- | Message text for a \"success\" events.
538+ DPMOk Text
539+ | -- | Message text for an \"error\" events.
540+ DPMError Text
541+ | -- | Clear notification widget.
542+ DPMClear
489543
490544-- | Notification widget that can show success and failure messages.
491−pageNotification
492− :: MonadWidget t m
493− => Event t DeploymentPageNotification
494− -- ^ Event carrying notifications.
495− -> m ()
545+pageNotification ::
546+ MonadWidget t m =>
547+ -- | Event carrying notifications.
548+ Event t DeploymentPageNotification ->
549+ m ()
496550pageNotification notEv = mdo
497− let
498− messageWidget (DPMOk txt) = messageClassWidget txt "notification--success"
499− messageWidget (DPMError txt) = messageClassWidget txt "notification--danger"
500− messageWidget DPMClear = pure never
501− messageClassWidget txt cl =
502− divClass ("page__output notification " <> cl) $ do
503− text txt
504− buttonClass "notification__close" ""
505− closeEv = switchDyn closeEvDyn
506− closeEvDyn <- widgetHold (pure never) $ leftmost
507− [ messageWidget <$> notEv
508− , pure never <$ closeEv ]
551+ let messageWidget (DPMOk txt) = messageClassWidget txt "notification--success"
552+ messageWidget (DPMError txt) = messageClassWidget txt "notification--danger"
553+ messageWidget DPMClear = pure never
554+ messageClassWidget txt cl =
555+ divClass ("page__output notification " <> cl) $ do
556+ text txt
557+ buttonClass "notification__close" ""
558+ closeEv = switchDyn closeEvDyn
559+ closeEvDyn <-
560+ widgetHold (pure never) $
561+ leftmost
562+ [ messageWidget <$> notEv
563+ , pure never <$ closeEv
564+ ]
509565 blank
510566
511−kubeDashboardUrl
512− :: (MonadReader ProjectConfig m, MonadWidget t m)
513− => Dynamic t DeploymentFullInfo -> m (Dynamic t (Maybe Text))
567+kubeDashboardUrl ::
568+ (MonadReader ProjectConfig m, MonadWidget t m) =>
569+ Dynamic t DeploymentFullInfo ->
570+ m (Dynamic t (Maybe Text))
514571kubeDashboardUrl deploymentInfo = do
515572 template <- asks kubernetesDashboardUrlTemplate
516573 let name = unDeploymentName . view (#deployment . #name) <$> deploymentInfo
octopod-frontend/src/Main.hs+207−135
@@ -1,59 +1,61 @@
1−{-|
2−Module : Page.Deployment
3−Description : Runner.
1+-- |
2+--Module : Page.Deployment
3+--Description : Runner.
4+--
5+--This module contains app routing and main.
6+module Main (main) where
47
5−This module contains app routing and main.
6−-}
8+import Data.ByteString (ByteString)
9+import Obelisk.Route.Frontend
10+import Reflex.Dom
11+import Servant.Reflex
712
8−module Main where
9−
10−import Data.ByteString (ByteString)
11−import Obelisk.Route.Frontend
12−import Reflex.Dom
13−import Servant.Reflex
14−
15−import Common.Types as CT
16−import Control.Monad.Reader
17−import Data.Maybe
13+import Common.Types as CT
14+import Control.Monad.Reader
15+import Data.Maybe
1816import qualified Data.Semigroup as S
19−import Frontend.API
20−import Frontend.GHCJS
21−import Frontend.Route
22−import Frontend.Utils (errorCommonWidget, loadingCommonWidget)
23−import Page.Deployment
24−import Page.Deployments
25−
17+import qualified Data.Text as T
18+import Frontend.API
19+import Frontend.GHCJS
20+import Frontend.Route
21+import Frontend.Utils (errorCommonWidget, loadingCommonWidget)
22+import Page.Deployment
23+import Page.Deployments
2624
2725main :: IO ()
2826main = mdo
29− mainWidgetWithHead' (headWidget, \() -> do
30− ((), projectNameEv) <- runEventWriterT initConfigWidget
31− return $ fmap S.getLast projectNameEv
27+ mainWidgetWithHead'
28+ ( headWidget
29+ , \() -> do
30+ ((), projectNameEv) <- runEventWriterT initConfigWidget
31+ return $ fmap S.getLast projectNameEv
3232 )
3333
3434-- | Receives the config file.
3535-- If request fails then an error message is displayed.
36−initConfigWidget
37− :: (MonadWidget t m, Prerender js t m, EventWriter t (S.Last ProjectName) m)
38− => m ()
36+initConfigWidget ::
37+ (MonadWidget t m, Prerender js t m, EventWriter t (S.Last ProjectName) m) =>
38+ m ()
3939initConfigWidget = do
4040 pb <- getPostBuild
4141 x <- performEvent (initConfig <$ pb)
42− widgetHold_ loadingWidget $ leftmost
43− [ (\cfg -> headerWidget >> routeWidget cfg) <$> fmapMaybe id x
44− , errorWidget <$ ffilter isNothing x ]
42+ widgetHold_ loadingWidget $
43+ leftmost
44+ [ (\cfg -> headerWidget >> routeWidget cfg) <$> fmapMaybe id x
45+ , errorWidget <$ ffilter isNothing x
46+ ]
4547 return ()
4648
4749-- | Sets up websockets. WS url is obtained from session storage.
48−wsUpdate :: forall t m . MonadWidget t m => m (Event t ())
50+wsUpdate :: forall t m. MonadWidget t m => m (Event t ())
4951wsUpdate = do
50− let
51− wsConfig = WebSocketConfig
52− { _webSocketConfig_send = never :: Event t [ByteString]
53− , _webSocketConfig_close = never
54− , _webSocketConfig_reconnect = True
55− , _webSocketConfig_protocols = []
56− }
52+ let wsConfig =
53+ WebSocketConfig
54+ { _webSocketConfig_send = never :: Event t [ByteString]
55+ , _webSocketConfig_close = never
56+ , _webSocketConfig_reconnect = True
57+ , _webSocketConfig_protocols = []
58+ }
5759 wsHost <- getWsUrl
5860 let wsUrl = wsHost <> "/" <> wsPath
5961 ws <- webSocket wsUrl wsConfig
@@ -61,118 +63,188 @@ wsUpdate = do
6163
6264-- | Widget that controls app routing.
6365-- It uses the routing scheme defined in 'Frontend.Route'.
64−routeWidget
65− ::
66− ( MonadWidget t m
67− , Prerender js t m )
68− => ProjectConfig
69− -> m ()
66+routeWidget ::
67+ ( MonadWidget t m
68+ , Prerender js t m
69+ ) =>
70+ ProjectConfig ->
71+ m ()
7072routeWidget cfg = do
71− let Right encoder = checkEncoder routeEncoder
73+ let encoder = case checkEncoder routeEncoder of
74+ Right e -> e
75+ Left err -> error (T.unpack err)
7276 pb <- getPostBuild
7377 updAllEv <- wsUpdate
7478 runRouteViewT encoder pb True $ mdo
7579 subRoute_ $ \case
7680 DashboardRoute -> do
7781 r <- askRoute
78− flip runReaderT cfg $ dyn_ $ ffor r $ \case
79− Nothing -> deploymentsPage updAllEv
80− Just dn -> deploymentPage updAllEv dn
82+ flip runReaderT cfg $
83+ dyn_ $
84+ ffor r $ \case
85+ Nothing -> deploymentsPage updAllEv
86+ Just dn -> deploymentPage updAllEv dn
8187 blank
8288
8389-- | Content of the @head@ DOM element.
84−headWidget
85− :: (DomBuilder t m, MonadHold t m, PostBuild t m)
86− => Event t ProjectName -> m ()
90+headWidget ::
91+ (DomBuilder t m, MonadHold t m, PostBuild t m) =>
92+ Event t ProjectName ->
93+ m ()
8794headWidget projectNameEv = do
88− elAttr "meta" ( "charset" =: "urf8 ") blank
89− elAttr "meta"
90− ( "http-equiv" =: "x-ua-compatible"
91− <> "content" =: "ie=edge" ) blank
92− elAttr "name"
93− ( "name" =: "viewport"
94− <> "content" =: "width=device-width, initial-scale=1" ) blank
95− elAttr "link"
96− ( "href" =: "https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap"
97− <> "rel" =: "stylesheet") blank
98− elAttr "link"
99− ( "href" =: "/static/styles/style.css"
100− <> "rel" =: "stylesheet") blank
101− elAttr "script"
102− ( "src" =: "/static/vendors/outline/outline.js" ) blank
95+ elAttr "meta" ("charset" =: "urf8 ") blank
96+ elAttr
97+ "meta"
98+ ( "http-equiv" =: "x-ua-compatible"
99+ <> "content" =: "ie=edge"
100+ )
101+ blank
102+ elAttr
103+ "name"
104+ ( "name" =: "viewport"
105+ <> "content" =: "width=device-width, initial-scale=1"
106+ )
107+ blank
108+ elAttr
109+ "link"
110+ ( "href" =: "https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700&display=swap"
111+ <> "rel" =: "stylesheet"
112+ )
113+ blank
114+ elAttr
115+ "link"
116+ ( "href" =: "/static/styles/style.css"
117+ <> "rel" =: "stylesheet"
118+ )
119+ blank
120+ elAttr
121+ "script"
122+ ("src" =: "/static/vendors/outline/outline.js")
123+ blank
103124 projectNameDyn <- holdDyn "Octopod" $ ("Octopod – " <>) . uProjectName <$> projectNameEv
104125 el "title" $ dynText projectNameDyn
105126
106127 -- favicon attributes
107128
108− elAttr "link"
109− ( "href" =: "/apple-icon-57x57.png"
110− <> "rel" =: "apple-touch-icon"
111− <> "sizes" =: "57x57") blank
112− elAttr "link"
113− ( "href" =: "/apple-icon-60x60.png"
114− <> "rel" =: "apple-touch-icon"
115− <> "sizes" =: "60x60") blank
116− elAttr "link"
117− ( "href" =: "/apple-icon-72x72.png"
118− <> "rel" =: "apple-touch-icon"
119− <> "sizes" =: "72x72") blank
120− elAttr "link"
121− ( "href" =: "/apple-icon-76x76.png"
122− <> "rel" =: "apple-touch-icon"
123− <> "sizes" =: "76x76") blank
124− elAttr "link"
125− ( "href" =: "/apple-icon-114x114.png"
126− <> "rel" =: "apple-touch-icon"
127− <> "sizes" =: "114x114") blank
128− elAttr "link"
129− ( "href" =: "/apple-icon-120x120.png"
130− <> "rel" =: "apple-touch-icon"
131− <> "sizes" =: "120x120") blank
132− elAttr "link"
133− ( "href" =: "/apple-icon-144x144.png"
134− <> "rel" =: "apple-touch-icon"
135− <> "sizes" =: "144x144") blank
136− elAttr "link"
137− ( "href" =: "/apple-icon-152x152.png"
138− <> "rel" =: "apple-touch-icon"
139− <> "sizes" =: "152x152") blank
140− elAttr "link"
141− ( "href" =: "/apple-icon-180x180.png"
142− <> "rel" =: "apple-touch-icon"
143− <> "sizes" =: "180x180") blank
144− elAttr "link"
145− ( "href" =: "/android-icon-192x192.png"
146− <> "rel" =: "icon"
147− <> "sizes" =: "192x192"
148− <> "type" =: "image/png") blank
149− elAttr "link"
150− ( "href" =: "/favicon-32x32.png"
151− <> "rel" =: "icon"
152− <> "sizes" =: "32x32"
153− <> "type" =: "image/png") blank
154− elAttr "link"
155− ( "href" =: "/favicon-96x96.png"
156− <> "rel" =: "icon"
157− <> "sizes" =: "96x96"
158− <> "type" =: "image/png") blank
159− elAttr "link"
160− ( "href" =: "/favicon-16x16.png"
161− <> "rel" =: "icon"
162− <> "sizes" =: "16x16"
163− <> "type" =: "image/png") blank
164− elAttr "link"
165− ( "href" =: "/manifest.json"
166− <> "rel" =: "manifest") blank
167− elAttr "meta"
168− ( "content" =: "#ffffff"
169− <> "name" =: "msapplication-TileColor") blank
170− elAttr "meta"
171− ( "content" =: "/ms-icon-144x144.png"
172− <> "name" =: "msapplication-TileImage") blank
173− elAttr "meta"
174− ( "content" =: "#ffffff"
175− <> "name" =: "theme-color") blank
129+ elAttr
130+ "link"
131+ ( "href" =: "/apple-icon-57x57.png"
132+ <> "rel" =: "apple-touch-icon"
133+ <> "sizes" =: "57x57"
134+ )
135+ blank
136+ elAttr
137+ "link"
138+ ( "href" =: "/apple-icon-60x60.png"
139+ <> "rel" =: "apple-touch-icon"
140+ <> "sizes" =: "60x60"
141+ )
142+ blank
143+ elAttr
144+ "link"
145+ ( "href" =: "/apple-icon-72x72.png"
146+ <> "rel" =: "apple-touch-icon"
147+ <> "sizes" =: "72x72"
148+ )
149+ blank
150+ elAttr
151+ "link"
152+ ( "href" =: "/apple-icon-76x76.png"
153+ <> "rel" =: "apple-touch-icon"
154+ <> "sizes" =: "76x76"
155+ )
156+ blank
157+ elAttr
158+ "link"
159+ ( "href" =: "/apple-icon-114x114.png"
160+ <> "rel" =: "apple-touch-icon"
161+ <> "sizes" =: "114x114"
162+ )
163+ blank
164+ elAttr
165+ "link"
166+ ( "href" =: "/apple-icon-120x120.png"
167+ <> "rel" =: "apple-touch-icon"
168+ <> "sizes" =: "120x120"
169+ )
170+ blank
171+ elAttr
172+ "link"
173+ ( "href" =: "/apple-icon-144x144.png"
174+ <> "rel" =: "apple-touch-icon"
175+ <> "sizes" =: "144x144"
176+ )
177+ blank
178+ elAttr
179+ "link"
180+ ( "href" =: "/apple-icon-152x152.png"
181+ <> "rel" =: "apple-touch-icon"
182+ <> "sizes" =: "152x152"
183+ )
184+ blank
185+ elAttr
186+ "link"
187+ ( "href" =: "/apple-icon-180x180.png"
188+ <> "rel" =: "apple-touch-icon"
189+ <> "sizes" =: "180x180"
190+ )
191+ blank
192+ elAttr
193+ "link"
194+ ( "href" =: "/android-icon-192x192.png"
195+ <> "rel" =: "icon"
196+ <> "sizes" =: "192x192"
197+ <> "type" =: "image/png"
198+ )
199+ blank
200+ elAttr
201+ "link"
202+ ( "href" =: "/favicon-32x32.png"
203+ <> "rel" =: "icon"
204+ <> "sizes" =: "32x32"
205+ <> "type" =: "image/png"
206+ )
207+ blank
208+ elAttr
209+ "link"
210+ ( "href" =: "/favicon-96x96.png"
211+ <> "rel" =: "icon"
212+ <> "sizes" =: "96x96"
213+ <> "type" =: "image/png"
214+ )
215+ blank
216+ elAttr
217+ "link"
218+ ( "href" =: "/favicon-16x16.png"
219+ <> "rel" =: "icon"
220+ <> "sizes" =: "16x16"
221+ <> "type" =: "image/png"
222+ )
223+ blank
224+ elAttr
225+ "link"
226+ ( "href" =: "/manifest.json"
227+ <> "rel" =: "manifest"
228+ )
229+ blank
230+ elAttr
231+ "meta"
232+ ( "content" =: "#ffffff"
233+ <> "name" =: "msapplication-TileColor"
234+ )
235+ blank
236+ elAttr
237+ "meta"
238+ ( "content" =: "/ms-icon-144x144.png"
239+ <> "name" =: "msapplication-TileImage"
240+ )
241+ blank
242+ elAttr
243+ "meta"
244+ ( "content" =: "#ffffff"
245+ <> "name" =: "theme-color"
246+ )
247+ blank
176248
177249-- | Common headers of all pages. Displays the project name.
178250headerWidget :: (MonadWidget t m, EventWriter t (S.Last ProjectName) m) => m ()
octopod-frontend/src/Page/ClassicPopup.hs+52−46
@@ -1,46 +1,53 @@
1−{-|
2−Module : Page.ClassicPopup
3−Description : Classic Popup.
1+-- |
2+--Module : Page.ClassicPopup
3+--Description : Classic Popup.
4+--
5+--This module contains the definition of classic popups with ok/cancel buttons.
6+module Page.ClassicPopup
7+ ( classicPopup,
8+ popupWidget,
9+ confirmArchivePopup,
10+ )
11+where
412
5−This module contains the definition of classic popups with ok/cancel buttons.
6−-}
7−module Page.ClassicPopup where
13+import Reflex.Dom
814
9−import Reflex.Dom
10−
11−import Frontend.Utils
15+import Frontend.Utils
1216
1317-- | The root function of a popup.
14−classicPopup
15− :: MonadWidget t m
16− => Event t ()
17− -- ^ \"Show\" event.
18− -> m (Event t (), Event t ())
19− -- ^ Body of the popup returning ok/cancel events.
20− -> m (Event t ())
21− -- ^ Returned \"ok\" event.
18+classicPopup ::
19+ MonadWidget t m =>
20+ -- | \"Show\" event.
21+ Event t () ->
22+ -- | Body of the popup returning ok/cancel events.
23+ m (Event t (), Event t ()) ->
24+ -- | Returned \"ok\" event.
25+ m (Event t ())
2226classicPopup showEv m = mdo
23− let
24− closeEv = switchDyn $ snd <$> evsDyn
25− okEv = switchDyn $ fst <$> evsDyn
26− empty = pure (never, never)
27− evsDyn <- widgetHold empty $ leftmost
28− [ empty <$ closeEv
29− , empty <$ okEv
30− , popupWidget m <$ showEv ]
27+ let closeEv = switchDyn $ snd <$> evsDyn
28+ okEv = switchDyn $ fst <$> evsDyn
29+ empty = pure (never, never)
30+ evsDyn <-
31+ widgetHold empty $
32+ leftmost
33+ [ empty <$ closeEv
34+ , empty <$ okEv
35+ , popupWidget m <$ showEv
36+ ]
3137 pure okEv
3238
3339-- | Wrapper for popup widget that darkens the background, makes the background
3440-- unclickable and supports a \"close\" button.
35−popupWidget
36− :: MonadWidget t m
37− => m (Event t (), Event t ())
38− -> m (Event t (), Event t ())
41+popupWidget ::
42+ MonadWidget t m =>
43+ m (Event t (), Event t ()) ->
44+ m (Event t (), Event t ())
3945popupWidget m =
4046 fmap snd $
41− elAttr' "div"
42− ("class" =: "classic-popup" <> "style" =: "display: block;") $
43− divClass "classic-popup__container" $
47+ elAttr'
48+ "div"
49+ ("class" =: "classic-popup" <> "style" =: "display: block;")
50+ $ divClass "classic-popup__container" $
4451 divClass "classic-popup__viewport" $
4552 divClass "classic-popup__slot" $ do
4653 (okEv, cancelEv) <- m
@@ -48,19 +55,18 @@ popupWidget m =
4855 pure $ (okEv, leftmost [cancelEv, closeEv])
4956
5057-- | Popup that requires confirmation of deployment deletion.
51−confirmArchivePopup
52− :: MonadWidget t m
53− => Event t ()
54− -> m ()
55− -> m (Event t ())
58+confirmArchivePopup ::
59+ MonadWidget t m =>
60+ Event t () ->
61+ m () ->
62+ m (Event t ())
5663confirmArchivePopup showEv txt = do
57− let
58− body =
59− divClass "dialog dialog--archive" $ do
60− divClass "dialog__content" txt
61− divClass "dialog__footer" $ do
62− okEv <- buttonClass "dialog__action button" "Archive"
63− cancelEv <-
64− buttonClass "dialog__action button--secondary button" "Cancel"
65− pure (okEv, cancelEv)
64+ let body =
65+ divClass "dialog dialog--archive" $ do
66+ divClass "dialog__content" txt
67+ divClass "dialog__footer" $ do
68+ okEv <- buttonClass "dialog__action button" "Archive"
69+ cancelEv <-
70+ buttonClass "dialog__action button--secondary button" "Cancel"
71+ pure (okEv, cancelEv)
6672 classicPopup showEv body
octopod-frontend/src/Page/Deployments.hs+397−357
@@ -1,90 +1,91 @@
1−{-|
2−Module : Page.Deployments
3−Description : Deployments table page.
4−
5−This module contains the definition of the deployments table page.
6−-}
7−
8−module Page.Deployments
9− ( deploymentsPage ) where
10−
11−import Control.Lens
12−import Control.Lens.Extras
13−import Control.Monad
14−import Control.Monad.IO.Class
15−import Data.Generics.Product (field)
16−import Data.Generics.Sum (_Ctor)
1+-- |
2+--Module : Page.Deployments
3+--Description : Deployments table page.
4+--
5+--This module contains the definition of the deployments table page.
6+module Page.Deployments (deploymentsPage) where
7+
8+import Control.Lens
9+import Control.Lens.Extras
10+import Control.Monad
11+import Control.Monad.IO.Class
12+import Data.Generics.Product (field)
13+import Data.Generics.Sum (_Ctor)
1714import qualified Data.List as L
18−import Data.Text (Text)
19−import Data.Time (diffUTCTime, getCurrentTime)
20−import GHC.Generics (Generic)
21−import Obelisk.Route.Frontend
22−import Reflex.Dom
23−import Servant.Reflex
24−
25−import Common.Types as CT
26−import Common.Utils
27−import Control.Applicative
28−import Control.Monad.Reader
29−import Data.Align
30−import Data.Char
31−import Data.Functor
15+import Data.Text (Text)
16+import Data.Time (diffUTCTime, getCurrentTime)
17+import GHC.Generics (Generic)
18+import Obelisk.Route.Frontend
19+import Reflex.Dom
20+import Servant.Reflex
21+
22+import Common.Types as CT
23+import Common.Utils
24+import Control.Applicative
25+import Control.Monad.Reader
26+import Data.Align
27+import Data.Char
28+import Data.Functor
3229import qualified Data.Semigroup as S
3330import qualified Data.Text as T
34−import Data.Text.Search
35−import Data.These
36−import Frontend.API
37−import Frontend.GHCJS
38−import Frontend.Route
39−import Frontend.Utils
40−import Page.ClassicPopup
41−import Page.Elements.Links
42−import Page.Popup.EditDeployment
43−import Page.Popup.NewDeployment
44−import Reflex.MultiEventWriter.Class
45−import Servant.Reflex.Extra
46−
31+import Data.Text.Search
32+import Data.These
33+import Frontend.API
34+import Frontend.GHCJS
35+import Frontend.Route
36+import Frontend.Utils
37+import Page.ClassicPopup
38+import Page.Elements.Links
39+import Page.Popup.EditDeployment
40+import Page.Popup.NewDeployment
41+import Reflex.MultiEventWriter.Class
42+import Servant.Reflex.Extra
4743
4844-- | The root widget of the deployments list page.
4945-- It requests data of all deployments.
5046-- If a request fails it shows an error, otherwise it calls 'deploymentsWidget',
5147-- passing a received data.
52−deploymentsPage
53− ::
54− ( MonadWidget t m
55− , RouteToUrl (R Routes) m
56− , SetRoute t (R Routes) m
57− , MonadReader ProjectConfig m
58− )
59− => Event t () -- ^ Event notifying about the need to update data.
60− -> m ()
48+deploymentsPage ::
49+ ( MonadWidget t m
50+ , SetRoute t (R Routes) m
51+ , MonadReader ProjectConfig m
52+ ) =>
53+ -- | Event notifying about the need to update data.
54+ Event t () ->
55+ m ()
6156deploymentsPage updAllEv = do
6257 pb <- getPostBuild
6358 respEv <- listEndpoint pb
6459 let (okEv, errEv) = processResp respEv
65− widgetHold_ loadingDeploymentsWidget $ leftmost
66− [ deploymentsWidget updAllEv <$> okEv
67− , errDeploymentsWidget <$ errEv ]
60+ widgetHold_ loadingDeploymentsWidget $
61+ leftmost
62+ [ deploymentsWidget updAllEv <$> okEv
63+ , errDeploymentsWidget <$ errEv
64+ ]
6865
6966-- | Widget that's shown after initial request succeeds.
7067-- It contains the header with an active search field,
7168-- deployments list and sidebars: \"new deployment\" and \"edit deployment\".
72−deploymentsWidget
73− ::
74− ( MonadWidget t m
75− , RouteToUrl (R Routes) m
76− , SetRoute t (R Routes) m
77− , MonadReader ProjectConfig m
78− )
79− => Event t () -- ^ Event notifying about the need to update data.
80− -> [DeploymentFullInfo] -- ^ Initial deployment data.
81− -> m ()
69+deploymentsWidget ::
70+ ( MonadWidget t m
71+ , SetRoute t (R Routes) m
72+ , MonadReader ProjectConfig m
73+ ) =>
74+ -- | Event notifying about the need to update data.
75+ Event t () ->
76+ -- | Initial deployment data.
77+ [DeploymentFullInfo] ->
78+ m ()
8279deploymentsWidget updAllEv dfis = do
8380 (showNewDeploymentEv, editEv) <- deploymentsWidgetWrapper $ mdo
84− pageNotification $ leftmost
85− [ DPMError "Deployment list update failed, deployment list\
86− \ may be slightly outdated." <$ errUpdEv
87− , DPMClear <$ okUpdEv ]
81+ pageNotification $
82+ leftmost
83+ [ DPMError
84+ "Deployment list update failed, deployment list\
85+ \ may be slightly outdated."
86+ <$ errUpdEv
87+ , DPMClear <$ okUpdEv
88+ ]
8889 (showNewDeploymentEv', termDyn) <- deploymentsHeadWidget True okUpdEv
8990 (okUpdEv, errUpdEv, editEv) <- deploymentsListWidget updAllEv termDyn dfis
9091 pure (showNewDeploymentEv', editEv)
@@ -100,90 +101,96 @@ deploymentsWidgetWrapper m =
100101-- | Header of the current page. It contains a timer that shows
101102-- how much time has passed since the last update,
102103-- a search field and z button that opens \"New deployment\" sidebar.
103−deploymentsHeadWidget
104− :: MonadWidget t m
105− => Bool
106− -- ^ Flag showing current state of search field.
107− -> Event t ()
108− -- ^ Event that fires after successful list update.
109− -> m (Event t (), Dynamic t Text)
110− -- ^ Returns an event notifying that the \"new deployment\" sidebar
104+deploymentsHeadWidget ::
105+ MonadWidget t m =>
106+ -- | Flag showing current state of search field.
107+ Bool ->
108+ -- | Event that fires after successful list update.
109+ Event t () ->
110+ -- | Returns an event notifying that the \"new deployment\" sidebar
111111 -- should be open and the search term within it.
112+ m (Event t (), Dynamic t Text)
112113deploymentsHeadWidget enabledSearch okUpdEv =
113114 divClass "page__head" $ do
114115 elClass "h1" "page__heading title" $ text "All deployments"
115116 lastUpdateWidget okUpdEv
116− termDyn <- divClass "page__action input input--search input--has-clear-type\
117− \ page__action--search" $ mdo
118− let
119− enabledSearchAttr = if enabledSearch then mempty else "disabled" =: ""
120− termDyn' <- inputElement $ def
121− & initialAttributes .~
122− ( "type" =: "text"
123− <> "class" =: "input__widget"
124− <> "placeholder" =: "Search for deployments"
125− <> "style" =: "width: 264px;"
126− <> enabledSearchAttr)
127− & inputElementConfig_setValue .~ ("" <$ domEvent Click deleteEl)
128− (deleteEl, _) <- elClass' "button" "input__clear-type spot spot--cancel" $
129− text "Delete"
130− pure termDyn'
131− (nsEl, _) <- elClass' "button"
132− "page__action button button--add popup-handler" $ text "New deployment"
117+ termDyn <- divClass
118+ "page__action input input--search input--has-clear-type\
119+ \ page__action--search"
120+ $ mdo
121+ let enabledSearchAttr = if enabledSearch then mempty else "disabled" =: ""
122+ termDyn' <-
123+ inputElement $
124+ def
125+ & initialAttributes
126+ .~ ( "type" =: "text"
127+ <> "class" =: "input__widget"
128+ <> "placeholder" =: "Search for deployments"
129+ <> "style" =: "width: 264px;"
130+ <> enabledSearchAttr
131+ )
132+ & inputElementConfig_setValue .~ ("" <$ domEvent Click deleteEl)
133+ (deleteEl, _) <-
134+ elClass' "button" "input__clear-type spot spot--cancel" $
135+ text "Delete"
136+ pure termDyn'
137+ (nsEl, _) <-
138+ elClass'
139+ "button"
140+ "page__action button button--add popup-handler"
141+ $ text "New deployment"
133142 pure (domEvent Click nsEl, value termDyn)
134−
135143-- ^ Widget that shows how much time has passed since the last update.
144+
136145lastUpdateWidget :: MonadWidget t m => Event t () -> m ()
137146lastUpdateWidget okUpdEv = do
138147 updTimeEv <- performEvent $ liftIO getCurrentTime <$ okUpdEv
139148 initTime <- liftIO getCurrentTime
140149 updTimeB <- hold initTime updTimeEv
141150 tickEv <- tickLossyFromPostBuildTime 1
142− let
143− diffEv = attachWith calcMins updTimeB tickEv
144− calcMins lastUpd TickInfo{..} = let
145− diffSec = diffUTCTime _tickInfo_lastUTC lastUpd
146− diffMin = diffSec / 60
147− in floor diffMin :: Int
148− mkMsg ms
149− | ms < 1 = "Updated just now"
150− | isSingular ms = "Updated " <> (showT $ ms) <> " minute ago"
151− | ms < 60 = "Updated " <> (showT $ ms) <> " minutes ago"
152− | isSingular (ms `div` 60) =
153− "Updated " <> (showT $ ms `div` 60) <> " hour ago"
154− | otherwise = "Updated " <> (showT $ ms `div` 60) <> " hours ago"
155− isSingular x = x `mod` 100 /= 11 && x `mod` 10 == 1
151+ let diffEv = attachWith calcMins updTimeB tickEv
152+ calcMins lastUpd TickInfo {..} =
153+ let diffSec = diffUTCTime _tickInfo_lastUTC lastUpd
154+ diffMin = diffSec / 60
155+ in floor diffMin :: Int
156+ mkMsg ms
157+ | ms < 1 = "Updated just now"
158+ | isSingular ms = "Updated " <> (showT $ ms) <> " minute ago"
159+ | ms < 60 = "Updated " <> (showT $ ms) <> " minutes ago"
160+ | isSingular (ms `div` 60) =
161+ "Updated " <> (showT $ ms `div` 60) <> " hour ago"
162+ | otherwise = "Updated " <> (showT $ ms `div` 60) <> " hours ago"
163+ isSingular x = x `mod` 100 /= 11 && x `mod` 10 == 1
156164 diffDyn <- holdDyn 0 diffEv
157165 divClass "page__note" $ dynText $ mkMsg <$> diffDyn
158166
159167-- | Widget with all available deployments. It updates
160168-- the deployment information every time when the supplied event fires.
161169-- If an update fails, a notification widget appears at the top of the page.
162−deploymentsListWidget
163− ::
164− ( MonadWidget t m
165− , RouteToUrl (R Routes) m
166− , SetRoute t (R Routes) m
167− , MonadReader ProjectConfig m
168− )
169− => Event t ()
170− -> Dynamic t Text
171− -> [DeploymentFullInfo] -- ^ Initial deployment data
172− -> m (Event t (), Event t (), Event t DeploymentFullInfo)
170+deploymentsListWidget ::
171+ ( MonadWidget t m
172+ , SetRoute t (R Routes) m
173+ , MonadReader ProjectConfig m
174+ ) =>
175+ Event t () ->
176+ Dynamic t Text ->
177+ -- | Initial deployment data
178+ [DeploymentFullInfo] ->
179+ m (Event t (), Event t (), Event t DeploymentFullInfo)
173180deploymentsListWidget updAllEv termDyn ds = dataWidgetWrapper $ mdo
174181 retryEv <- delay 10 errUpdEv
175182 updRespEv <- listEndpoint $ leftmost [updAllEv, () <$ retryEv]
176− let
177− okUpdEv = fmapMaybe reqSuccess updRespEv
178− errUpdEv = fmapMaybe reqErrorBody updRespEv
183+ let okUpdEv = fmapMaybe reqSuccess updRespEv
184+ errUpdEv = fmapMaybe reqErrorBody updRespEv
179185 dsDyn <- holdDyn ds okUpdEv
180− let
181− isArchived = isDeploymentArchived . view #deployment
182− filteredDyn = ffor2 termDyn dsDyn $ \term ds' ->
183− mapMaybe (searchDeployments . T.filter (not . isSpace) $ term) ds'
184− (archivedDsDyn, activeDsDyn) = splitDynPure $ L.partition isArchived
185− <$> filteredDyn
186− searchSorting = termDyn $> Just (SortDesc (view #score))
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))
187194 clickedEv <- elementClick
188195 editEv <- activeDeploymentsWidget searchSorting clickedEv activeDsDyn
189196 archivedDeploymentsWidget searchSorting clickedEv archivedDsDyn
@@ -193,22 +200,26 @@ data SearchedDeploymentFullInfo = SearchedDeploymentFullInfo
193200 { deployment :: DeploymentFullInfo
194201 , searchResult :: DeploymentSearchResult
195202 , score :: Int
196− } deriving stock (Generic, Show, Eq)
203+ }
204+ deriving stock (Generic, Show, Eq)
197205
198206-- | Compares deployment fields (name and tag) with a search term.
199−searchDeployments
200− :: Text -- ^ Search term.
201− -> DeploymentFullInfo -- ^ Deployment data.
202− -> Maybe SearchedDeploymentFullInfo
207+searchDeployments ::
208+ -- | Search term.
209+ Text ->
210+ -- | Deployment data.
211+ DeploymentFullInfo ->
212+ Maybe SearchedDeploymentFullInfo
203213searchDeployments term d = case (tagResult, nameResult) of
204214 (Nothing, Nothing) -> Nothing
205215 (Just (res, score), Nothing) ->
206216 Just $ SearchedDeploymentFullInfo d (DeploymentTagResult res) score
207217 (Nothing, Just (res, score)) ->
208218 Just $ SearchedDeploymentFullInfo d (DeploymentNameResult res) score
209− (Just (tRes, tScore), Just (nRes, nScore)) -> if tScore > nScore
210− then Just $ SearchedDeploymentFullInfo d (DeploymentTagResult tRes) tScore
211− else Just $ SearchedDeploymentFullInfo d (DeploymentNameResult nRes) nScore
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
212223 where
213224 tagResult = fuzzySearch term . unDeploymentTag $ d ^. field @"deployment" . field @"tag"
214225 nameResult = fuzzySearch term . unDeploymentName $ d ^. dfiName
@@ -216,40 +227,39 @@ searchDeployments term d = case (tagResult, nameResult) of
216227data DeploymentSearchResult
217228 = DeploymentNameResult [FuzzySearchStringChunk Text]
218229 | DeploymentTagResult [FuzzySearchStringChunk Text]
219− deriving (Show, Eq)
230+ deriving stock (Show, Eq)
220231
221232-- | Table with active deployments.
222−activeDeploymentsWidget
223− ::
224− ( MonadWidget t m
225− , RouteToUrl (R Routes) m
226− , SetRoute t (R Routes) m
227− , MonadReader ProjectConfig m
228− )
229− => Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo))
230− -> Event t ClickedElement
231− -- ^ Event that carries the clicked DOM element. This event is required by
233+activeDeploymentsWidget ::
234+ ( MonadWidget t m
235+ , SetRoute t (R Routes) m
236+ , MonadReader ProjectConfig m
237+ ) =>
238+ Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo)) ->
239+ -- | Event that carries the clicked DOM element. This event is required by
232240 -- `dropdownWidget'`.
233− -> Dynamic t [SearchedDeploymentFullInfo]
234− -> m (Event t DeploymentFullInfo)
235− -- ^ Returns an event carrying editable deployment
241+ Event t ClickedElement ->
242+ Dynamic t [SearchedDeploymentFullInfo] ->
243+ -- | Returns an event carrying editable deployment
236244 -- to \"edit deployment\" sidebar.
245+ m (Event t DeploymentFullInfo)
237246activeDeploymentsWidget searchSorting clickedEv dsDyn =
238247 divClass "data__primary" $
239248 tableWrapper (updated searchSorting $> SortingChanged) $ \sortDyn -> do
240249 let colSortDyn = fmap (contramap (view #deployment)) <$> sortDyn
241250 sorting <- holdDyn Nothing (mergeWith (<|>) [updated colSortDyn, updated searchSorting])
242− let
243− emptyDyn' = L.null <$> dsDyn
244− dsSortedDyn = zipDynWith sortDeployments dsDyn sorting
251+ let emptyDyn' = L.null <$> dsDyn
252+ dsSortedDyn = zipDynWith sortDeployments dsDyn sorting
245253 emptyDyn <- holdUniqDyn emptyDyn'
246− editEvEv <- dyn $ emptyDyn <&> \case
247− False -> do
248− editEvs <- simpleList dsSortedDyn (activeDeploymentWidget clickedEv)
249− pure $ switchDyn $ leftmost <$> editEvs
250− True -> do
251− emptyTableBody $ noDeploymentsWidget
252− pure never
254+ editEvEv <-
255+ dyn $
256+ emptyDyn <&> \case
257+ False -> do
258+ editEvs <- simpleList dsSortedDyn (activeDeploymentWidget clickedEv)
259+ pure $ switchDyn $ leftmost <$> editEvs
260+ True -> do
261+ emptyTableBody $ noDeploymentsWidget
262+ pure never
253263 switchHold never editEvEv
254264
255265-- | This type helps determine which item was selected
@@ -257,74 +267,81 @@ activeDeploymentsWidget searchSorting clickedEv dsDyn =
257267data DeploymentAction
258268 = ArchiveDeployment
259269 | EditDeployment
260− deriving (Show, Eq, Generic)
270+ deriving stock (Show, Eq, Generic)
261271
262272-- | Row of active deployment.
263−activeDeploymentWidget
264− ::
265− ( MonadWidget t m
266− , RouteToUrl (R Routes) m
267− , SetRoute t (R Routes) m
268− , MonadReader ProjectConfig m
269− )
270− => Event t ClickedElement
271− -- ^ Event that carries the clicked DOM element. This event is required by
273+activeDeploymentWidget ::
274+ ( MonadWidget t m
275+ , SetRoute t (R Routes) m
276+ , MonadReader ProjectConfig m
277+ ) =>
278+ -- | Event that carries the clicked DOM element. This event is required by
272279 -- `dropdownWidget'`.
273− -> Dynamic t SearchedDeploymentFullInfo
274− -> m (Event t DeploymentFullInfo)
275− -- ^ Returns event carrying editable deployment
280+ Event t ClickedElement ->
281+ Dynamic t SearchedDeploymentFullInfo ->
282+ -- | Returns event carrying editable deployment
276283 -- that is required by \"edit deployment\" sidebar.
284+ m (Event t DeploymentFullInfo)
277285activeDeploymentWidget clickedEv dDyn' = do
278286 dDyn <- holdUniqDyn dDyn'
279− editEvEv <- dyn $ ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = d@DeploymentFullInfo{..}} -> do
280− let
281− (name, tag') = displaySearchHighlighting s
282− dName = deployment ^. #name
283− (linkEl, dropdownEv) <- el' "tr" $ do
284− el "td" $ do
285− name
286− statusWidget $ constDyn status
287− el "td" $ divClass "listing" $ forM_ metadata (renderMetadataLink . pure)
288− el "td" tag'
289− el "td" $
290− overridesWidget $ deployment ^. field @"appOverrides" . coerced
291− el "td" $
292− overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
293− el "td" $
294− text $ formatPosixToDate createdAt
295− el "td" $
296− text $ formatPosixToDate updatedAt
297− el "td" $ do
298− let
299− enabled = not . isPending . recordedStatus $ status
300− elId = "deployment_row_" <> unDeploymentName dName
301− btn = elAttr "button"
302− ( "class" =: "drop__handler"
303− <> "type" =: "button"
304− <> "id" =: elId ) $ text "Actions"
305− body = do
306− btnEditEv <- buttonClassEnabled' "action action--edit" "Edit" (pure enabled) "action--disabled"
307− btnArcEv <- buttonClassEnabled' "action action--archive" "Move to archive" (pure enabled) "action--disabled"
308− url' <- kubeDashboardUrl (view #deployment <$> dDyn)
309− void . dyn $ url' <&> maybe blank (\url ->
310− void $ aButtonClass' "action action--logs" "Details"
311− (pure $ "href" =: url <> "target" =: "_blank")
312− )
313− pure $
314− leftmost
315− [ ArchiveDeployment <$ btnArcEv, EditDeployment <$ btnEditEv ]
316− dropdownWidget' clickedEv btn body
317− let
318− archEv = () <$ ffilter (is (_Ctor @"ArchiveDeployment")) dropdownEv
319− editEv = d <$ ffilter (is (_Ctor @"EditDeployment")) dropdownEv
320− delEv <- confirmArchivePopup archEv $ do
321− text "Are you sure you want to archive the"
322− el "br" blank
323− text $ unDeploymentName dName <> " deployment?"
324− void $ archiveEndpoint (constDyn . Right $ dName) delEv
325− let route = DashboardRoute :/ Just dName
326− setRoute $ route <$ domEvent Dblclick linkEl
327− pure editEv
287+ editEvEv <- dyn $
288+ ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = d@DeploymentFullInfo {..}} -> do
289+ let (name, tag') = displaySearchHighlighting s
290+ dName = deployment ^. #name
291+ (linkEl, dropdownEv) <- el' "tr" $ do
292+ el "td" $ do
293+ name
294+ statusWidget $ constDyn status
295+ el "td" $ divClass "listing" $ forM_ metadata (renderMetadataLink . pure)
296+ el "td" tag'
297+ el "td" $
298+ overridesWidget $ deployment ^. field @"appOverrides" . coerced
299+ el "td" $
300+ overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
301+ el "td" $
302+ text $ formatPosixToDate createdAt
303+ el "td" $
304+ text $ formatPosixToDate updatedAt
305+ el "td" $ do
306+ let enabled = not . isPending . recordedStatus $ status
307+ elId = "deployment_row_" <> unDeploymentName dName
308+ btn =
309+ elAttr
310+ "button"
311+ ( "class" =: "drop__handler"
312+ <> "type" =: "button"
313+ <> "id" =: elId
314+ )
315+ $ text "Actions"
316+ body = do
317+ btnEditEv <- buttonClassEnabled' "action action--edit" "Edit" (pure enabled) "action--disabled"
318+ btnArcEv <- buttonClassEnabled' "action action--archive" "Move to archive" (pure enabled) "action--disabled"
319+ url' <- kubeDashboardUrl (view #deployment <$> dDyn)
320+ void . dyn $
321+ url'
322+ <&> maybe
323+ blank
324+ ( \url ->
325+ void $
326+ aButtonClass'
327+ "action action--logs"
328+ "Details"
329+ (pure $ "href" =: url <> "target" =: "_blank")
330+ )
331+ pure $
332+ leftmost
333+ [ArchiveDeployment <$ btnArcEv, EditDeployment <$ btnEditEv]
334+ dropdownWidget' clickedEv btn body
335+ let archEv = () <$ ffilter (is (_Ctor @"ArchiveDeployment")) dropdownEv
336+ editEv = d <$ ffilter (is (_Ctor @"EditDeployment")) dropdownEv
337+ delEv <- confirmArchivePopup archEv $ do
338+ text "Are you sure you want to archive the"
339+ el "br" blank
340+ text $ unDeploymentName dName <> " deployment?"
341+ void $ archiveEndpoint (constDyn . Right $ dName) delEv
342+ let route = DashboardRoute :/ Just dName
343+ setRoute $ route <$ domEvent Dblclick linkEl
344+ pure editEv
328345 switchHold never editEvEv
329346
330347displaySearchHighlighting :: DomBuilder t m => SearchedDeploymentFullInfo -> (m (), m ())
@@ -346,88 +363,94 @@ markMatches (Matched x : xs) =
346363 elAttr "span" ("style" =: "text-decoration: underline;") (text x) >> markMatches xs
347364
348365-- | Table with archived deployments.
349−archivedDeploymentsWidget
350− :: forall t m.
351− ( MonadWidget t m
352− , RouteToUrl (R Routes) m
353− , SetRoute t (R Routes) m )
354− => Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo))
355− -> Event t ClickedElement
356− -- ^ Event that carries the clicked DOM element. This event is required by
366+archivedDeploymentsWidget ::
367+ forall t m.
368+ ( MonadWidget t m
369+ , SetRoute t (R Routes) m
370+ ) =>
371+ Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo)) ->
372+ -- | Event that carries the clicked DOM element. This event is required by
357373 -- `dropdownWidget'`.
358− -> Dynamic t [SearchedDeploymentFullInfo]
359− -> m ()
374+ Event t ClickedElement ->
375+ Dynamic t [SearchedDeploymentFullInfo] ->
376+ m ()
360377archivedDeploymentsWidget searchSorting clickedEv dsDyn = do
361378 showDyn <- toggleButton
362− let
363− classDyn = ffor showDyn $ \case
364− True -> "data__archive data__archive--open"
365− False -> "data__archive"
379+ let classDyn = ffor showDyn $ \case
380+ True -> "data__archive data__archive--open"
381+ False -> "data__archive"
366382 elDynClass "div" classDyn $
367383 tableWrapper (updated searchSorting $> SortingChanged) $ \sortDyn -> do
368384 let colSortDyn = fmap (contramap (view #deployment)) <$> sortDyn
369385 sorting <- holdDyn Nothing (mergeWith (<|>) [updated colSortDyn, updated searchSorting])
370− let
371− emptyDyn' = L.null <$> dsDyn
372− dsSortedDyn = zipDynWith sortDeployments dsDyn sorting
386+ let emptyDyn' = L.null <$> dsDyn
387+ dsSortedDyn = zipDynWith sortDeployments dsDyn sorting
373388 emptyDyn <- holdUniqDyn emptyDyn'
374− dyn_ $ emptyDyn <&> \case
375− False -> void $ simpleList dsSortedDyn
376− (archivedDeploymentWidget clickedEv)
377− True -> emptyTableBody $ noDeploymentsWidget
389+ dyn_ $
390+ emptyDyn <&> \case
391+ False ->
392+ void $
393+ simpleList
394+ dsSortedDyn
395+ (archivedDeploymentWidget clickedEv)
396+ True -> emptyTableBody $ noDeploymentsWidget
378397
379398-- | Row with archived deployment.
380−archivedDeploymentWidget
381− ::
382− ( MonadWidget t m
383− , RouteToUrl (R Routes) m
384− , SetRoute t (R Routes) m )
385− => Event t ClickedElement
386− -> Dynamic t SearchedDeploymentFullInfo
387− -> m ()
399+archivedDeploymentWidget ::
400+ ( MonadWidget t m
401+ , SetRoute t (R Routes) m
402+ ) =>
403+ Event t ClickedElement ->
404+ Dynamic t SearchedDeploymentFullInfo ->
405+ m ()
388406archivedDeploymentWidget clickedEv dDyn' = do
389407 dDyn <- holdUniqDyn dDyn'
390− dyn_ $ ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = DeploymentFullInfo{..}} -> do
391− let
392− (name, tag') = displaySearchHighlighting s
393− dName = deployment ^. #name
394− (linkEl, _) <- el' "tr" $ do
395− el "td" $ do
396− name
397− divClass "status status--archived" $ text "Archived"
398− el "td" $ text "..."
399− el "td" tag'
400− el "td" $
401− overridesWidget $ deployment ^. field @"appOverrides" . coerced
402− el "td" $
403− overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
404− el "td" $
405− text $ formatPosixToDate createdAt
406− el "td" $
407− text $ formatPosixToDate updatedAt
408− el "td" $ do
409− let
410− elId = "deployment_row_" <> (unDeploymentName dName)
411− btn = elAttr "button"
412− ( "class" =: "drop__handler"
413− <> "type" =: "button"
414− <> "id" =: elId) $ text "Actions"
415− body = do
416− btnArcEv <- buttonClass
417− "action action--archive" "Restore from archive"
418− pure btnArcEv
419− btnEv <- dropdownWidget' clickedEv btn body
420− void $ restoreEndpoint (constDyn $ Right $ dName) btnEv
421− let route = DashboardRoute :/ Just dName
422− setRoute $ route <$ domEvent Dblclick linkEl
408+ dyn_ $
409+ ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = DeploymentFullInfo {..}} -> do
410+ let (name, tag') = displaySearchHighlighting s
411+ dName = deployment ^. #name
412+ (linkEl, _) <- el' "tr" $ do
413+ el "td" $ do
414+ name
415+ divClass "status status--archived" $ text "Archived"
416+ el "td" $ text "..."
417+ el "td" tag'
418+ el "td" $
419+ overridesWidget $ deployment ^. field @"appOverrides" . coerced
420+ el "td" $
421+ overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
422+ el "td" $
423+ text $ formatPosixToDate createdAt
424+ el "td" $
425+ text $ formatPosixToDate updatedAt
426+ el "td" $ do
427+ let elId = "deployment_row_" <> (unDeploymentName dName)
428+ btn =
429+ elAttr
430+ "button"
431+ ( "class" =: "drop__handler"
432+ <> "type" =: "button"
433+ <> "id" =: elId
434+ )
435+ $ text "Actions"
436+ body = do
437+ btnArcEv <-
438+ buttonClass
439+ "action action--archive"
440+ "Restore from archive"
441+ pure btnArcEv
442+ btnEv <- dropdownWidget' clickedEv btn body
443+ void $ restoreEndpoint (constDyn $ Right $ dName) btnEv
444+ let route = DashboardRoute :/ Just dName
445+ setRoute $ route <$ domEvent Dblclick linkEl
423446
424447-- | Sort deployments by the supplied condition.
425−sortDeployments
426− :: [SearchedDeploymentFullInfo]
427− -> Maybe (SortDir SearchedDeploymentFullInfo)
428− -- ^ Sorting condition.
429− -> [SearchedDeploymentFullInfo]
430−sortDeployments items s = L.sortBy sortFunc items
448+sortDeployments ::
449+ [SearchedDeploymentFullInfo] ->
450+ -- | Sorting condition.
451+ Maybe (SortDir SearchedDeploymentFullInfo) ->
452+ [SearchedDeploymentFullInfo]
453+sortDeployments items s = L.sortBy sortFunc items
431454 where
432455 sortFunc a b = case s of
433456 Just (SortAsc get) -> compare (get a) (get b)
@@ -437,9 +460,9 @@ sortDeployments items s = L.sortBy sortFunc items
437460-- | Each constructor contains a getter
438461-- that extracts the field that is used for sorting.
439462data SortDir x where
440− SortAsc :: Ord a => (x -> a) -> SortDir x
463+ SortAsc :: Ord a => (x -> a) -> SortDir x
441464 SortDesc :: Ord a => (x -> a) -> SortDir x
442− deriving Semigroup via (S.Last (SortDir x))
465+ deriving (Semigroup) via (S.Last (SortDir x))
443466
444467instance Contravariant SortDir where
445468 contramap f (SortAsc g) = SortAsc $ g . f
@@ -468,7 +491,7 @@ tableHeader = do
468491 elClass "span" "visuallyhidden" $ text "Menu"
469492
470493data SortingChanged = SortingChanged
471− deriving Semigroup via (S.Last SortingChanged)
494+ deriving (Semigroup) via (S.Last SortingChanged)
472495
473496type SortableTableGroup t m =
474497 ( MonadReader (Event t SortingChanged) m
@@ -478,14 +501,14 @@ type SortableTableGroup t m =
478501
479502-- | Group all sortable headers ('sortHeader', 'sortHeaderInitially').
480503-- Makes sure that only one can be active at a time.
481−runSortableTableGroup
482− :: (Reflex t, MonadFix m)
483− => Event t SortingChanged
484− -> ReaderT
485− (Event t SortingChanged)
486− (EventWriterT t (SortDir DeploymentFullInfo) (EventWriterT t SortingChanged m))
487− x
488− -> m (x, Event t (SortDir DeploymentFullInfo))
504+runSortableTableGroup ::
505+ (Reflex t, MonadFix m) =>
506+ Event t SortingChanged ->
507+ ReaderT
508+ (Event t SortingChanged)
509+ (EventWriterT t (SortDir DeploymentFullInfo) (EventWriterT t SortingChanged m))
510+ x ->
511+ m (x, Event t (SortDir DeploymentFullInfo))
489512runSortableTableGroup sChanged m = mdo
490513 ((x, sDyn), changed) <-
491514 runEventWriterT . runEventWriterT . flip runReaderT (leftmost [sChanged, changed]) $ m
@@ -494,61 +517,74 @@ runSortableTableGroup sChanged m = mdo
494517type SortingDirection a = (DeploymentFullInfo -> a) -> SortDir DeploymentFullInfo
495518
496519-- | Special column header with a sort button.
497−sortHeader
498− :: forall t m a. (MonadWidget t m, Ord a, SortableTableGroup t m)
499− => (DeploymentFullInfo -> a)
500− -> Text
501− -> SortingDirection a -- ^ The direction to sort when clicked
502− -> m ()
520+sortHeader ::
521+ forall t m a.
522+ (MonadWidget t m, SortableTableGroup t m) =>
523+ (DeploymentFullInfo -> a) ->
524+ Text ->
525+ -- | The direction to sort when clicked
526+ SortingDirection a ->
527+ m ()
503528sortHeader f l defaultSorting =
504529 sortHeaderWithInitial f l defaultSorting (Nothing @(SortingDirection a))
505530
506531-- | Special column header with a sort button.
507−sortHeaderInitially
508− :: forall t m a. (MonadWidget t m, Ord a, SortableTableGroup t m)
509− => (DeploymentFullInfo -> a)
510− -> Text
511− -> SortingDirection a -- ^ The direction to sort when clicked and when the page loads
512− -> m ()
532+sortHeaderInitially ::
533+ forall t m a.
534+ (MonadWidget t m, SortableTableGroup t m) =>
535+ (DeploymentFullInfo -> a) ->
536+ Text ->
537+ -- | The direction to sort when clicked and when the page loads
538+ SortingDirection a ->
539+ m ()
513540sortHeaderInitially f l defaultSorting =
514541 sortHeaderWithInitial f l defaultSorting (Just @(SortingDirection a) defaultSorting)
515542
516543-- | Special column header with a sort button.
517−sortHeaderWithInitial
518− :: forall t m a. (MonadWidget t m, Ord a, SortableTableGroup t m)
519− => (DeploymentFullInfo -> a)
520− -> Text
521− -> SortingDirection a -- ^ The direction to sort when clicked
522− -> Maybe (SortingDirection a) -- ^ The direction to sort when the page loads
523− -> m ()
544+sortHeaderWithInitial ::
545+ forall t m a.
546+ (MonadWidget t m, SortableTableGroup t m) =>
547+ (DeploymentFullInfo -> a) ->
548+ Text ->
549+ -- | The direction to sort when clicked
550+ SortingDirection a ->
551+ -- | The direction to sort when the page loads
552+ Maybe (SortingDirection a) ->
553+ m ()
524554sortHeaderWithInitial f l defaultSorting initSortingM = do
525555 let initSorting = case initSortingM of
526556 Nothing -> Nothing
527557 Just x -> Just $ x f
528558 el "th" $ mdo
529559 sortingChanged <- ask
530− sortDyn <- foldDyn ($) initSorting $ alignWith (curry $ \case
531− (That SortingChanged, _) -> Nothing -- Some other column has started sorting
532− (_, Nothing) -> Just $ defaultSorting f -- This column started sorting
533− (_, Just x) -> Just $ toggleSort x -- This column was sorting and was clicked
534− ) sortBtnEv sortingChanged
560+ sortDyn <-
561+ foldDyn ($) initSorting $
562+ alignWith
563+ ( curry $ \case
564+ (That SortingChanged, _) -> Nothing -- Some other column has started sorting
565+ (_, Nothing) -> Just $ defaultSorting f -- This column started sorting
566+ (_, Just x) -> Just $ toggleSort x -- This column was sorting and was clicked
567+ )
568+ sortBtnEv
569+ sortingChanged
535570 tellMultiEvent . fmapMaybe id $ updated sortDyn
536− let
537− classDyn = fmap ("sort " <>) $ sortDyn <&> \case
538− Just (SortDesc _) -> "sort--active sort--desc"
539− Just (SortAsc _) -> "sort--active sort--asc"
540− Nothing -> ""
571+ let classDyn =
572+ fmap ("sort " <>) $
573+ sortDyn <&> \case
574+ Just (SortDesc _) -> "sort--active sort--desc"
575+ Just (SortAsc _) -> "sort--active sort--asc"
576+ Nothing -> ""
541577 sortBtnEv <- buttonDynClass classDyn (pure l)
542578 tellMultiEvent $ sortBtnEv $> SortingChanged
543579 pure ()
544580
545581-- | A wrapper that adds a header to the table.
546−tableWrapper
547− :: (MonadWidget t m)
548− => Event t SortingChanged
549− -> (Dynamic t (Maybe (SortDir DeploymentFullInfo)) -> m a)
550− -- ^ Sorting direction is obtained from the table header.
551− -> m a
582+tableWrapper ::
583+ (MonadWidget t m) =>
584+ Event t SortingChanged ->
585+ -- | Sorting direction is obtained from the table header.
586+ (Dynamic t (Maybe (SortDir DeploymentFullInfo)) -> m a) ->
587+ m a
552588tableWrapper sChanged ma =
553589 divClass "table table--deployments table--clickable table--double-click" $
554590 el "table" $ mdo
@@ -560,8 +596,9 @@ tableWrapper sChanged ma =
560596initTableWrapper :: MonadWidget t m => m () -> m ()
561597initTableWrapper ma = do
562598 divClass "data_primary" $
563− tableWrapper never $ const $
564− emptyTableBody $ ma
599+ tableWrapper never $
600+ const $
601+ emptyTableBody $ ma
565602
566603-- | Widget with a loading spinner.
567604loadingDeploymentsWidget :: MonadWidget t m => m ()
@@ -572,16 +609,18 @@ loadingDeploymentsWidget =
572609
573610-- | Widget with an error message.
574611errDeploymentsWidget :: MonadWidget t m => m ()
575−errDeploymentsWidget = deploymentsWidgetWrapper $ dataWidgetWrapper $
576− initTableWrapper $
577− errorCommonWidget
612+errDeploymentsWidget =
613+ deploymentsWidgetWrapper $
614+ dataWidgetWrapper $
615+ initTableWrapper $
616+ errorCommonWidget
578617
579618-- | Widget for an empty table with a custom message.
580−noDeploymentsWidget'
581− :: MonadWidget t m
582− => m ()
583− -- ^ Text widget
584− -> m ()
619+noDeploymentsWidget' ::
620+ MonadWidget t m =>
621+ -- | Text widget
622+ m () ->
623+ m ()
585624noDeploymentsWidget' h =
586625 divClass "null null--search" $ do
587626 elClass "b" "null__heading" h
@@ -605,13 +644,14 @@ dataWidgetWrapper ma = divClass "page__body" $ divClass "data" ma
605644toggleButton :: MonadWidget t m => m (Dynamic t Bool)
606645toggleButton = mdo
607646 showDyn <- toggle False $ domEvent Click archivedBtnEl
608− let
609− btnClassDyn = ffor showDyn $ \case
610− True -> "data__show-archive expander expander--stand-alone expander--open"
611− False -> "data__show-archive expander expander--stand-alone"
612− btnAttrsDyn = ffor btnClassDyn $ \btnClass ->
613− ( "class" =: btnClass
614− <> "type" =: "button" )
615− (archivedBtnEl, _) <- elDynAttr' "button" btnAttrsDyn $
616− text "Show Archived deployments"
647+ let btnClassDyn = ffor showDyn $ \case
648+ True -> "data__show-archive expander expander--stand-alone expander--open"
649+ False -> "data__show-archive expander expander--stand-alone"
650+ btnAttrsDyn = ffor btnClassDyn $ \btnClass ->
651+ ( "class" =: btnClass
652+ <> "type" =: "button"
653+ )
654+ (archivedBtnEl, _) <-
655+ elDynAttr' "button" btnAttrsDyn $
656+ text "Show Archived deployments"
617657 pure showDyn