mirror of
https://github.com/typeable/octopod.git
synced 2024-09-11 14:56:38 +03:00
Improved handling metadata links (#9)
* Added link titles * Preserve metadata ordering * Added dev run commands to Makefile * Added explanation to the development guide * Made caddy 2 the only config since we probably don't want to maintain two versions. * Use `id` instead of `ord` * Added ghcid version of Caddyfile * Renamed staging to deployment
This commit is contained in:
parent
ef65a9bbdc
commit
9bcfdcf065
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ dist-newstyle
|
|||||||
octopod-config.json
|
octopod-config.json
|
||||||
frontend-result
|
frontend-result
|
||||||
octopod-css/node_modules
|
octopod-css/node_modules
|
||||||
|
dev/certs/*
|
||||||
|
27
Caddyfile
Normal file
27
Caddyfile
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
http://localhost:8000
|
||||||
|
|
||||||
|
|
||||||
|
file_server
|
||||||
|
|
||||||
|
rewrite /api/* {path}
|
||||||
|
reverse_proxy /api/* localhost:3002
|
||||||
|
|
||||||
|
@production {
|
||||||
|
path_regexp production /static/(.*)
|
||||||
|
}
|
||||||
|
rewrite @production /octopod-css/production/{http.regexp.production.1}
|
||||||
|
|
||||||
|
rewrite /config.json /dev/config.json
|
||||||
|
rewrite * /frontend-result/bin/frontend.jsexe/{path}
|
||||||
|
|
||||||
|
|
||||||
|
@3003 {
|
||||||
|
not path /octopod-css/* /frontend-result/* /dev/config.json
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy @3003 localhost:3003
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format single_field common_log
|
||||||
|
}
|
42
Caddyfile1
42
Caddyfile1
@ -1,42 +0,0 @@
|
|||||||
localhost:8000
|
|
||||||
|
|
||||||
mime .css text/css
|
|
||||||
|
|
||||||
rewrite /static/styles {
|
|
||||||
r (.*)
|
|
||||||
to /octopod-css/production/styles/{1}
|
|
||||||
}
|
|
||||||
|
|
||||||
mime .js application/javascript
|
|
||||||
|
|
||||||
rewrite /static/vendors {
|
|
||||||
r (.*)
|
|
||||||
to /octopod-css/production/vendors/{1}
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite /static/scripts {
|
|
||||||
r (.*)
|
|
||||||
to /octopod-css/production/scripts/{1}
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite /static/images {
|
|
||||||
r (.*)
|
|
||||||
to /octopod-css/production/images/{1}
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy /api localhost:3002 {
|
|
||||||
transparent
|
|
||||||
}
|
|
||||||
|
|
||||||
mime .json application/json
|
|
||||||
rewrite /config.json /octopod-config.json
|
|
||||||
|
|
||||||
rewrite /ghcjs /frontend-result/bin/frontend.jsexe/index.html
|
|
||||||
|
|
||||||
proxy / localhost:3003 {
|
|
||||||
transparent
|
|
||||||
websocket
|
|
||||||
except /octopod-css/ /frontend-result/ /octopod-config.json
|
|
||||||
}
|
|
||||||
|
|
||||||
log / stdout
|
|
@ -3,20 +3,18 @@ http://localhost:8000
|
|||||||
|
|
||||||
file_server
|
file_server
|
||||||
|
|
||||||
|
rewrite /api/* {path}
|
||||||
|
reverse_proxy /api/* localhost:3002
|
||||||
|
|
||||||
@production {
|
@production {
|
||||||
path_regexp production /static/(.*)
|
path_regexp production /static/(.*)
|
||||||
}
|
}
|
||||||
|
|
||||||
rewrite @production /octopod-css/production/{http.regexp.production.1}
|
rewrite @production /octopod-css/production/{http.regexp.production.1}
|
||||||
|
|
||||||
reverse_proxy /api localhost:3002
|
rewrite /config.json /dev/config.json
|
||||||
|
|
||||||
rewrite /config.json /octopod-config.json
|
|
||||||
|
|
||||||
rewrite /ghcjs /frontend-result/bin/frontend.jsexe/index.html
|
|
||||||
|
|
||||||
@3003 {
|
@3003 {
|
||||||
not path /octopod-css/* /frontend-result/* /octopod-config.json
|
not path /octopod-css/* /frontend-result/* /dev/config.json
|
||||||
}
|
}
|
||||||
|
|
||||||
reverse_proxy @3003 localhost:3003
|
reverse_proxy @3003 localhost:3003
|
@ -64,9 +64,36 @@ For development, we have set up `ghcid` commands that rebuild the project every
|
|||||||
- `ghcid-cli`
|
- `ghcid-cli`
|
||||||
- `ghcid-frontend`
|
- `ghcid-frontend`
|
||||||
|
|
||||||
### Frontend proxy
|
### Running the project locally
|
||||||
|
|
||||||
The frontend should be accessed through a proxy. We have set up [caddy](https://caddyserver.com) configs to ease development. You will need place an `octopod-config.json` file at the root of the repository containing a [config](../../charts/octopod/templates/octopod-nginx-configmap.yaml#L15-L20). `app_auth` can be an arbitrary string – it will not affect anything when running locally.
|
We have two commands to run the backend and frontend in the `Makefile`:
|
||||||
|
|
||||||
|
- `run-backend-dev`
|
||||||
|
|
||||||
|
Builds a production version of the backend server, runs database migrations and starts the server with mock control scripts. You can see the used config in [`dev/dev_backend.sh`](./dev/dev_backend.sh).
|
||||||
|
|
||||||
|
### NOTE:
|
||||||
|
|
||||||
|
1. You need to have a [Postgres](https://www.postgresql.org) database running on `localhost:5432` (the default port).
|
||||||
|
|
||||||
|
2. You need to create an empty `octopod` database.
|
||||||
|
|
||||||
|
3. You also need to have an `octopod:octopod` user set up as it will be used by the server to access the database.
|
||||||
|
|
||||||
|
The easiest way to do this is by running the following command after you have _Postgres running_:
|
||||||
|
```bash
|
||||||
|
psql -c "CREATE ROLE IF NOT EXISTS octopod WITH PASSWORD 'octopod' SUPERUSER LOGIN;"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. You will need [sqitch](https://sqitch.org) installed on your system as it will be used to run migrations on the database.
|
||||||
|
|
||||||
|
- `run-frontend-dev`
|
||||||
|
|
||||||
|
Build a production version of the frontend and runs it locally, pointing it to the locally running backend server.
|
||||||
|
|
||||||
|
### NOTE:
|
||||||
|
|
||||||
|
You need to have [_Caddy 2_](https://caddyserver.com/v2) installed on your system as it is automatically used as a proxy.
|
||||||
|
|
||||||
### Stack
|
### Stack
|
||||||
|
|
||||||
|
17
Makefile
17
Makefile
@ -1,4 +1,4 @@
|
|||||||
.PHONY: build-backend build-octo-cli build-frontend docs backend-docs frontend-docs repl shell shell-ghcjs ghcid ghcid-cli ghcid-frontend push-octopod
|
.PHONY: build-backend build-octo-cli build-frontend docs backend-docs frontend-docs repl shell shell-ghcjs ghcid ghcid-cli ghcid-frontend push-octopod run-backend-dev run-frontend-dev
|
||||||
|
|
||||||
build-backend:
|
build-backend:
|
||||||
nix-build . -A ghc.octopod-backend
|
nix-build . -A ghc.octopod-backend
|
||||||
@ -37,3 +37,18 @@ ghcid-frontend:
|
|||||||
|
|
||||||
push-octopod:
|
push-octopod:
|
||||||
./build.sh build-and-push latest
|
./build.sh build-and-push latest
|
||||||
|
|
||||||
|
run-backend-dev: dev/certs/server_cert.pem dev/certs/server_key.pem
|
||||||
|
./dev/dev_backend.sh `nix-build . -A ghc.octopod-backend`
|
||||||
|
|
||||||
|
run-frontend-dev: build-frontend
|
||||||
|
caddy run
|
||||||
|
|
||||||
|
dev/certs/server_cert.pem dev/certs/server_key.pem:
|
||||||
|
openssl req -x509 -newkey rsa:4096 -keyout dev/certs/server_key.pem -out dev/certs/server_cert.pem -nodes -subj "/CN=localhost/O=Server"
|
||||||
|
|
||||||
|
dev/certs/client_csr.pem dev/certs/client_key.pem:
|
||||||
|
openssl req -newkey rsa:4096 -keyout dev/certs/client_key.pem -out dev/certs/client_csr.pem -nodes -subj "/CN=Client"
|
||||||
|
|
||||||
|
dev/certs/client_cert.pem: dev/certs/client_csr.pem dev/certs/server_cert.pem dev/certs/server_key.pem
|
||||||
|
openssl x509 -req -in dev/certs/client_csr.pem -CA dev/certs/server_cert.pem -CAkey dev/certs/server_key.pem -out dev/certs/client_cert.pem -set_serial 01 -days 3650)
|
||||||
|
5
dev/config.json
Normal file
5
dev/config.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"app_url": "http://localhost:8000",
|
||||||
|
"ws_url": "ws://localhost:4020",
|
||||||
|
"app_auth": ""
|
||||||
|
}
|
29
dev/dev_backend.sh
Executable file
29
dev/dev_backend.sh
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
(cd migrations && sqitch deploy -t postgresql://octopod:octopod@localhost:5432/octopod)
|
||||||
|
|
||||||
|
export MOUNT_DIR=./dev
|
||||||
|
|
||||||
|
export PROJECT_NAME="Example Project"
|
||||||
|
export BASE_DOMAIN=octopod.example.com
|
||||||
|
export NAMESPACE=deployment
|
||||||
|
export ARCHIVE_RETENTION=60
|
||||||
|
export STATUS_UPDATE_TIMEOUT=600
|
||||||
|
export CREATION_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export UPDATE_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export UPDATE_ENVS_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export ARCHIVE_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export CHECKING_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export CLEANUP_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export ARCHIVE_CHECKING_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export TAG_CHECKING_COMMAND=$MOUNT_DIR/echo.sh
|
||||||
|
export INFO_COMMAND=$MOUNT_DIR/info.sh
|
||||||
|
$1/bin/octopod-exe \
|
||||||
|
--port 4443 \
|
||||||
|
--ui-port 3002 \
|
||||||
|
--ws-port 4020 \
|
||||||
|
--db "host='127.0.0.1' port=5432 user='octopod' password='octopod'" \
|
||||||
|
--db-pool-size 10 \
|
||||||
|
--tls-cert-path dev/certs/server_cert.pem \
|
||||||
|
--tls-key-path dev/certs/server_key.pem \
|
||||||
|
--tls-store-path /tmp/tls_store
|
4
dev/echo.sh
Executable file
4
dev/echo.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo $0 $@
|
||||||
|
exit 0
|
6
dev/info.sh
Executable file
6
dev/info.sh
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "key,value"
|
||||||
|
echo "key2,value2"
|
||||||
|
|
||||||
|
exit 0
|
@ -41,6 +41,7 @@ import System.Process.Typed
|
|||||||
|
|
||||||
import Common.Utils
|
import Common.Utils
|
||||||
import Common.Validation (isNameValid)
|
import Common.Validation (isNameValid)
|
||||||
|
import Database.PostgreSQL.Simple.Transaction
|
||||||
import Octopod.API
|
import Octopod.API
|
||||||
import Octopod.PowerAPI
|
import Octopod.PowerAPI
|
||||||
import Octopod.Server.Args
|
import Octopod.Server.Args
|
||||||
@ -385,8 +386,7 @@ updateDeploymentInfo dName st = do
|
|||||||
case ec of
|
case ec of
|
||||||
ExitSuccess -> do
|
ExitSuccess -> do
|
||||||
dMeta <- parseDeploymentMetadata (lines . unStdout $ out)
|
dMeta <- parseDeploymentMetadata (lines . unStdout $ out)
|
||||||
forM_ dMeta $ \meta ->
|
upsertDeploymentMetadata (pool st) dName dMeta
|
||||||
upsertDeploymentMetadata (pool st) dName meta
|
|
||||||
ExitFailure _ ->
|
ExitFailure _ ->
|
||||||
log $
|
log $
|
||||||
"could not get deployment info, exit code: " <> (pack . show $ ec)
|
"could not get deployment info, exit code: " <> (pack . show $ ec)
|
||||||
@ -1045,7 +1045,8 @@ selectDeploymentMetadata conn dName = do
|
|||||||
let
|
let
|
||||||
q =
|
q =
|
||||||
"SELECT key, value FROM deployment_metadata \
|
"SELECT key, value FROM deployment_metadata \
|
||||||
\WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)"
|
\WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?) \
|
||||||
|
\ORDER BY id ASC"
|
||||||
rows <- query conn q (Only dName)
|
rows <- query conn q (Only dName)
|
||||||
for rows $ \(k, v) -> pure $ DeploymentMetadata k v
|
for rows $ \(k, v) -> pure $ DeploymentMetadata k v
|
||||||
|
|
||||||
@ -1065,21 +1066,23 @@ deleteDeploymentMetadata pgPool dName = do
|
|||||||
upsertDeploymentMetadata
|
upsertDeploymentMetadata
|
||||||
:: PgPool
|
:: PgPool
|
||||||
-> DeploymentName
|
-> DeploymentName
|
||||||
-> DeploymentMetadata
|
-> [DeploymentMetadata]
|
||||||
-> IO ()
|
-> IO ()
|
||||||
upsertDeploymentMetadata pgPool dName dMetadata = do
|
upsertDeploymentMetadata pgPool dName dMetadatas = do
|
||||||
let
|
let
|
||||||
q =
|
withResource pgPool $ \conn -> withTransactionSerializable conn $ do
|
||||||
"INSERT INTO deployment_metadata \
|
void $ execute
|
||||||
\(deployment_id, key, value, created_at, updated_at) \
|
conn
|
||||||
\(SELECT id, ?, ?, now(), now() FROM deployments WHERE name = ?) \
|
"DELETE FROM deployment_metadata \
|
||||||
\ON CONFLICT (deployment_id, key) \
|
\WHERE deployment_id = (SELECT id FROM deployments WHERE name = ?)"
|
||||||
\DO \
|
(Only dName)
|
||||||
\UPDATE SET key = ?, value = ?, updated_at = now()"
|
forM_ dMetadatas $ \dMeta ->
|
||||||
key = deploymentMetadataKey dMetadata
|
execute
|
||||||
value = deploymentMetadataValue dMetadata
|
conn
|
||||||
void $ withResource pgPool $ \conn ->
|
"INSERT INTO deployment_metadata \
|
||||||
execute conn q (key, value, dName, key, value)
|
\(deployment_id, key, value, created_at, updated_at) \
|
||||||
|
\(SELECT id, ?, ?, now(), now() FROM deployments WHERE name = ?)"
|
||||||
|
(deploymentMetadataKey dMeta, deploymentMetadataValue dMeta, dName)
|
||||||
|
|
||||||
-- | Checks the existence of a deployment tag.
|
-- | Checks the existence of a deployment tag.
|
||||||
-- Returns 404 'Tag not found' response if the deployment tag doesn't exist.
|
-- Returns 404 'Tag not found' response if the deployment tag doesn't exist.
|
||||||
|
@ -117,8 +117,8 @@ data DeploymentLog = DeploymentLog
|
|||||||
deriving (FromJSON, ToJSON) via Snake DeploymentLog
|
deriving (FromJSON, ToJSON) via Snake DeploymentLog
|
||||||
|
|
||||||
data DeploymentMetadata = DeploymentMetadata
|
data DeploymentMetadata = DeploymentMetadata
|
||||||
{ deploymentMetadataKey :: Text
|
{ deploymentMetadataKey :: Text -- ^ The name of the link
|
||||||
, deploymentMetadataValue :: Text
|
, deploymentMetadataValue :: Text -- ^ The URL
|
||||||
}
|
}
|
||||||
deriving (Generic, Show, Eq)
|
deriving (Generic, Show, Eq)
|
||||||
deriving (FromJSON, ToJSON) via Snake DeploymentMetadata
|
deriving (FromJSON, ToJSON) via Snake DeploymentMetadata
|
||||||
|
@ -33,6 +33,7 @@ executable frontend
|
|||||||
, Page.Popup.EditDeployment
|
, Page.Popup.EditDeployment
|
||||||
, Page.Popup.NewDeployment
|
, Page.Popup.NewDeployment
|
||||||
, Servant.Reflex.Extra
|
, Servant.Reflex.Extra
|
||||||
|
, Page.Elements.Links
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
-Werror
|
-Werror
|
||||||
-Wno-missing-home-modules
|
-Wno-missing-home-modules
|
||||||
|
@ -22,6 +22,7 @@ import Frontend.API
|
|||||||
import Frontend.Route
|
import Frontend.Route
|
||||||
import Frontend.Utils
|
import Frontend.Utils
|
||||||
import Page.ClassicPopup
|
import Page.ClassicPopup
|
||||||
|
import Page.Elements.Links
|
||||||
import Page.Popup.EditDeployment
|
import Page.Popup.EditDeployment
|
||||||
|
|
||||||
-- | The root widget of a deployment page. It requests the deployment data.
|
-- | The root widget of a deployment page. It requests the deployment data.
|
||||||
@ -68,7 +69,7 @@ deploymentWidget updEv dfi = mdo
|
|||||||
[ DPMError "Couldn't update status of deployment" <$ errEv
|
[ DPMError "Couldn't update status of deployment" <$ errEv
|
||||||
, DPMClear <$ okEv ]
|
, DPMClear <$ okEv ]
|
||||||
deploymentBody updEv dfiDyn
|
deploymentBody updEv dfiDyn
|
||||||
pure (editEv')
|
pure editEv'
|
||||||
sentEv <- editDeploymentPopup editEv never
|
sentEv <- editDeploymentPopup editEv never
|
||||||
blank
|
blank
|
||||||
|
|
||||||
@ -161,14 +162,7 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $ do
|
|||||||
elClass "h3" "deployment__sub-heading" $ text "Links"
|
elClass "h3" "deployment__sub-heading" $ text "Links"
|
||||||
divClass "deployment__widget" $
|
divClass "deployment__widget" $
|
||||||
divClass "listing" $
|
divClass "listing" $
|
||||||
void $ simpleList urlsDyn $ \urlDyn' -> do
|
void $ simpleList urlsDyn renderMetadataLink
|
||||||
let
|
|
||||||
urlDyn = deploymentMetadataValue <$> urlDyn'
|
|
||||||
attrDyn = urlDyn <&> \url ->
|
|
||||||
( "class" =: "listing__item external bar bar--larger"
|
|
||||||
<> "href" =: url
|
|
||||||
<> "target" =: "_blank" )
|
|
||||||
elDynAttr "a" attrDyn $ dynText urlDyn
|
|
||||||
elClass "section" "deployment__section" $ do
|
elClass "section" "deployment__section" $ do
|
||||||
let
|
let
|
||||||
envsDyn = dfiDyn <^.> field @"deployment"
|
envsDyn = dfiDyn <^.> field @"deployment"
|
||||||
|
@ -29,6 +29,7 @@ import Frontend.API
|
|||||||
import Frontend.Route
|
import Frontend.Route
|
||||||
import Frontend.Utils
|
import Frontend.Utils
|
||||||
import Page.ClassicPopup
|
import Page.ClassicPopup
|
||||||
|
import Page.Elements.Links
|
||||||
import Page.Popup.EditDeployment
|
import Page.Popup.EditDeployment
|
||||||
import Page.Popup.NewDeployment
|
import Page.Popup.NewDeployment
|
||||||
|
|
||||||
@ -239,15 +240,7 @@ activeDeploymentWidget clickedEv dDyn' = do
|
|||||||
el "td" $ do
|
el "td" $ do
|
||||||
text $ coerce dname
|
text $ coerce dname
|
||||||
statusWidget $ constDyn status
|
statusWidget $ constDyn status
|
||||||
el "td" $ do
|
el "td" $ divClass "listing" $ forM_ metadata (renderMetadataLink . pure)
|
||||||
divClass "listing" $
|
|
||||||
forM_ metadata $ \meta -> do
|
|
||||||
let
|
|
||||||
url = deploymentMetadataValue meta
|
|
||||||
void $ elAttr' "a"
|
|
||||||
( "class" =: "listing__item external bar"
|
|
||||||
<> "href" =: url
|
|
||||||
<> "target" =: "_blank") $ text url
|
|
||||||
el "td" $
|
el "td" $
|
||||||
text $ coerce $ deployment ^. field @"tag"
|
text $ coerce $ deployment ^. field @"tag"
|
||||||
el "td" $
|
el "td" $
|
||||||
|
23
octopod-frontend/src/Page/Elements/Links.hs
Normal file
23
octopod-frontend/src/Page/Elements/Links.hs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
module Page.Elements.Links
|
||||||
|
( renderMetadataLink
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common.Types
|
||||||
|
import Data.Functor
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Reflex.Dom
|
||||||
|
|
||||||
|
renderMetadataLink
|
||||||
|
:: (DomBuilder t m, PostBuild t m)
|
||||||
|
=> Dynamic t DeploymentMetadata -> m ()
|
||||||
|
renderMetadataLink metadataD = do
|
||||||
|
let
|
||||||
|
attrDyn = metadataD <&> \metadata ->
|
||||||
|
"class" =: "listing__item external bar bar--larger"
|
||||||
|
<> "href" =: deploymentMetadataValue metadata
|
||||||
|
<> "target" =: "_blank"
|
||||||
|
elDynAttr "a" attrDyn . dynText $ metadataD <&> \case
|
||||||
|
-- If the name is empty, then use the url
|
||||||
|
DeploymentMetadata {deploymentMetadataKey = name}
|
||||||
|
| (not . T.null . T.strip) name -> name
|
||||||
|
DeploymentMetadata {deploymentMetadataValue = url} -> url
|
Loading…
Reference in New Issue
Block a user