Merge pull request #1243 from dod-ccpo/staging
Update master from staging
This commit is contained in:
commit
cc3863d926
@ -30,7 +30,7 @@ commands:
|
|||||||
default: atat_test
|
default: atat_test
|
||||||
container_env:
|
container_env:
|
||||||
type: string
|
type: string
|
||||||
default: -e PGHOST=postgres -e REDIS_URI=redis://redis:6379
|
default: -e PGHOST=postgres -e REDIS_HOST=redis:6379
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- run:
|
||||||
name: Set up temporary docker network
|
name: Set up temporary docker network
|
||||||
@ -172,7 +172,7 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
docker run \
|
docker run \
|
||||||
-e PGHOST=postgres \
|
-e PGHOST=postgres \
|
||||||
-e REDIS_URI=redis://redis:6379 \
|
-e REDIS_HOST=redis:6379 \
|
||||||
--network atat \
|
--network atat \
|
||||||
atat:builder \
|
atat:builder \
|
||||||
/bin/sh -c "pipenv install --dev && /bin/sh script/cibuild"
|
/bin/sh -c "pipenv install --dev && /bin/sh script/cibuild"
|
||||||
@ -195,7 +195,7 @@ jobs:
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
-e DISABLE_CRL_CHECK=true \
|
-e DISABLE_CRL_CHECK=true \
|
||||||
-e PGHOST=postgres \
|
-e PGHOST=postgres \
|
||||||
-e REDIS_URI=redis://redis:6379 \
|
-e REDIS_HOST=redis:6379 \
|
||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
--network atat \
|
--network atat \
|
||||||
--name test-atat \
|
--name test-atat \
|
||||||
@ -253,7 +253,7 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
docker run \
|
docker run \
|
||||||
-e PGHOST=postgres \
|
-e PGHOST=postgres \
|
||||||
-e REDIS_URI=redis://redis:6379 \
|
-e REDIS_HOST=redis:6379 \
|
||||||
--network atat \
|
--network atat \
|
||||||
atat:builder \
|
atat:builder \
|
||||||
/bin/sh -c "pipenv install --dev && /bin/sh script/sync-crls && pipenv run pytest --no-cov tests/check_crl_parse.py"
|
/bin/sh -c "pipenv install --dev && /bin/sh script/sync-crls && pipenv run pytest --no-cov tests/check_crl_parse.py"
|
||||||
@ -293,6 +293,11 @@ workflows:
|
|||||||
- integration-tests:
|
- integration-tests:
|
||||||
requires:
|
requires:
|
||||||
- docker-build
|
- docker-build
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- staging
|
||||||
|
- master
|
||||||
- deploy-staging:
|
- deploy-staging:
|
||||||
requires:
|
requires:
|
||||||
- test
|
- test
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -33,6 +33,7 @@ static/buildinfo.*
|
|||||||
log/*
|
log/*
|
||||||
|
|
||||||
config/dev.ini
|
config/dev.ini
|
||||||
|
.env*
|
||||||
|
|
||||||
# CRLs
|
# CRLs
|
||||||
/crl
|
/crl
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||||
"lines": null
|
"lines": null
|
||||||
},
|
},
|
||||||
"generated_at": "2019-11-26T21:33:43Z",
|
"generated_at": "2019-12-06T21:22:07Z",
|
||||||
"plugins_used": [
|
"plugins_used": [
|
||||||
{
|
{
|
||||||
"base64_limit": 4.5,
|
"base64_limit": 4.5,
|
||||||
@ -98,7 +98,7 @@
|
|||||||
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
||||||
"is_secret": false,
|
"is_secret": false,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 21,
|
"line_number": 29,
|
||||||
"type": "Secret Keyword"
|
"type": "Secret Keyword"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -161,7 +161,7 @@
|
|||||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||||
"is_secret": false,
|
"is_secret": false,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 32,
|
"line_number": 41,
|
||||||
"type": "Hex High Entropy String"
|
"type": "Hex High Entropy String"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -87,6 +87,7 @@ COPY --from=builder /install/translations.yaml .
|
|||||||
COPY --from=builder /install/script/seed_roles.py ./script/seed_roles.py
|
COPY --from=builder /install/script/seed_roles.py ./script/seed_roles.py
|
||||||
COPY --from=builder /install/script/sync-crls ./script/sync-crls
|
COPY --from=builder /install/script/sync-crls ./script/sync-crls
|
||||||
COPY --from=builder /install/static/ ./static/
|
COPY --from=builder /install/static/ ./static/
|
||||||
|
COPY --from=builder /install/fixtures/ ./fixtures
|
||||||
COPY --from=builder /install/uwsgi.ini .
|
COPY --from=builder /install/uwsgi.ini .
|
||||||
COPY --from=builder /usr/local/bin/uwsgi /usr/local/bin/uwsgi
|
COPY --from=builder /usr/local/bin/uwsgi /usr/local/bin/uwsgi
|
||||||
|
|
||||||
|
1
Pipfile
1
Pipfile
@ -29,6 +29,7 @@ azure-mgmt-subscription = "*"
|
|||||||
azure-graphrbac = "*"
|
azure-graphrbac = "*"
|
||||||
msrestazure = "*"
|
msrestazure = "*"
|
||||||
azure-mgmt-authorization = "*"
|
azure-mgmt-authorization = "*"
|
||||||
|
azure-mgmt-managementgroups = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
|
177
Pipfile.lock
generated
177
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "6d2ab855267daac877ae7464de9dba5b62b7d89288992f87d8fc6ff0c0d2520f"
|
"sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -39,11 +39,11 @@
|
|||||||
},
|
},
|
||||||
"apache-libcloud": {
|
"apache-libcloud": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:201751f738109f25d58dcdfb5804e17216e0dc8f68b522e9e26ac16e0b9ff2ea",
|
"sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f",
|
||||||
"sha256:40215db1bd489d17dc1abfdb289d7f035313c7297b6a7462c79d8287cbbeae91"
|
"sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.6.0"
|
"version": "==2.6.1"
|
||||||
},
|
},
|
||||||
"azure-common": {
|
"azure-common": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -68,6 +68,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.60.0"
|
"version": "==0.60.0"
|
||||||
},
|
},
|
||||||
|
"azure-mgmt-managementgroups": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682",
|
||||||
|
"sha256:8194ee6274df865eccd1ed9d385ea625aeba9b8058b9e4fdf547f5207271a775"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
"azure-mgmt-subscription": {
|
"azure-mgmt-subscription": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
|
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
|
||||||
@ -117,10 +125,10 @@
|
|||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
||||||
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||||
],
|
],
|
||||||
"version": "==2019.9.11"
|
"version": "==2019.11.28"
|
||||||
},
|
},
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -248,10 +256,10 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
|
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
|
||||||
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
|
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
|
||||||
],
|
],
|
||||||
"version": "==0.23"
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"isodate": {
|
"isodate": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -330,10 +338,10 @@
|
|||||||
},
|
},
|
||||||
"more-itertools": {
|
"more-itertools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
|
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
|
||||||
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
|
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
|
||||||
],
|
],
|
||||||
"version": "==7.2.0"
|
"version": "==8.0.0"
|
||||||
},
|
},
|
||||||
"msrest": {
|
"msrest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -422,11 +430,11 @@
|
|||||||
},
|
},
|
||||||
"pyopenssl": {
|
"pyopenssl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200",
|
"sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
|
||||||
"sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6"
|
"sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==19.0.0"
|
"version": "==19.1.0"
|
||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -459,22 +467,20 @@
|
|||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
|
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||||
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
|
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||||
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
|
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||||
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
|
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||||
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
|
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||||
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
|
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||||
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
|
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||||
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
|
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||||
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
|
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||||
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
|
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||||
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
|
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
|
||||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.1.2"
|
"version": "==5.2"
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -650,10 +656,10 @@
|
|||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
||||||
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||||
],
|
],
|
||||||
"version": "==2019.9.11"
|
"version": "==2019.11.28"
|
||||||
},
|
},
|
||||||
"chardet": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -785,10 +791,10 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
|
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
|
||||||
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
|
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
|
||||||
],
|
],
|
||||||
"version": "==0.23"
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"ipdb": {
|
"ipdb": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -799,11 +805,11 @@
|
|||||||
},
|
},
|
||||||
"ipython": {
|
"ipython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
|
"sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51",
|
||||||
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
|
"sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==7.9.0"
|
"version": "==7.10.1"
|
||||||
},
|
},
|
||||||
"ipython-genutils": {
|
"ipython-genutils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -908,30 +914,30 @@
|
|||||||
},
|
},
|
||||||
"more-itertools": {
|
"more-itertools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
|
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
|
||||||
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
|
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
|
||||||
],
|
],
|
||||||
"version": "==7.2.0"
|
"version": "==8.0.0"
|
||||||
},
|
},
|
||||||
"mypy": {
|
"mypy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f",
|
"sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768",
|
||||||
"sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00",
|
"sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646",
|
||||||
"sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae",
|
"sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c",
|
||||||
"sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d",
|
"sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939",
|
||||||
"sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9",
|
"sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279",
|
||||||
"sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391",
|
"sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2",
|
||||||
"sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f",
|
"sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2",
|
||||||
"sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9",
|
"sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640",
|
||||||
"sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990",
|
"sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7",
|
||||||
"sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7",
|
"sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c",
|
||||||
"sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb",
|
"sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17",
|
||||||
"sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453",
|
"sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78",
|
||||||
"sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b",
|
"sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931",
|
||||||
"sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb"
|
"sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.740"
|
"version": "==0.750"
|
||||||
},
|
},
|
||||||
"mypy-extensions": {
|
"mypy-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -961,10 +967,10 @@
|
|||||||
},
|
},
|
||||||
"pbr": {
|
"pbr": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8",
|
"sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
|
||||||
"sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"
|
"sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
|
||||||
],
|
],
|
||||||
"version": "==5.4.3"
|
"version": "==5.4.4"
|
||||||
},
|
},
|
||||||
"pexpect": {
|
"pexpect": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -983,18 +989,17 @@
|
|||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
|
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||||
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
|
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||||
],
|
],
|
||||||
"version": "==0.13.0"
|
"version": "==0.13.1"
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
|
"sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7",
|
||||||
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
|
"sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990"
|
||||||
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
|
|
||||||
],
|
],
|
||||||
"version": "==2.0.10"
|
"version": "==3.0.2"
|
||||||
},
|
},
|
||||||
"ptyprocess": {
|
"ptyprocess": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1012,10 +1017,10 @@
|
|||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
|
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
|
||||||
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
|
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
|
||||||
],
|
],
|
||||||
"version": "==2.4.2"
|
"version": "==2.5.2"
|
||||||
},
|
},
|
||||||
"pylint": {
|
"pylint": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1058,11 +1063,11 @@
|
|||||||
},
|
},
|
||||||
"pytest-mock": {
|
"pytest-mock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b3514caac35fe3f05555923eabd9546abce11571cc2ddf7d8615959d04f2c89e",
|
"sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba",
|
||||||
"sha256:ea502c3891599c26243a3a847ccf0b1d20556678c528f86c98e3cd6d40c5cf11"
|
"sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.11.2"
|
"version": "==1.12.1"
|
||||||
},
|
},
|
||||||
"pytest-watch": {
|
"pytest-watch": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1080,22 +1085,20 @@
|
|||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
|
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
|
||||||
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
|
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
|
||||||
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
|
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
|
||||||
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
|
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
|
||||||
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
|
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
|
||||||
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
|
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
|
||||||
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
|
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
|
||||||
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
|
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
|
||||||
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
|
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
|
||||||
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
|
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
|
||||||
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
|
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
|
||||||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
|
||||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.1.2"
|
"version": "==5.2"
|
||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
46
README.md
46
README.md
@ -168,12 +168,7 @@ Testing file uploads and downloads locally requires a few configuration options.
|
|||||||
In the flask config (`config/base.ini`, perhaps):
|
In the flask config (`config/base.ini`, perhaps):
|
||||||
|
|
||||||
```
|
```
|
||||||
CSP=<aws | azure | mock>
|
CSP=< azure | mock>
|
||||||
|
|
||||||
AWS_REGION_NAME=""
|
|
||||||
AWS_ACCESS_KEY=""
|
|
||||||
AWS_SECRET_KEY=""
|
|
||||||
AWS_BUCKET_NAME=""
|
|
||||||
|
|
||||||
AZURE_STORAGE_KEY=""
|
AZURE_STORAGE_KEY=""
|
||||||
AZURE_ACCOUNT_NAME=""
|
AZURE_ACCOUNT_NAME=""
|
||||||
@ -183,7 +178,7 @@ AZURE_TO_BUCKET_NAME=""
|
|||||||
There are also some build-time configuration that are used by parcel. Add these to `.env.local`, and run `rm -r .cache/` before running `yarn build`:
|
There are also some build-time configuration that are used by parcel. Add these to `.env.local`, and run `rm -r .cache/` before running `yarn build`:
|
||||||
|
|
||||||
```
|
```
|
||||||
CLOUD_PROVIDER=<aws | azure | mock>
|
CLOUD_PROVIDER=<azure | mock>
|
||||||
AZURE_ACCOUNT_NAME=""
|
AZURE_ACCOUNT_NAME=""
|
||||||
AZURE_CONTAINER_NAME=""
|
AZURE_CONTAINER_NAME=""
|
||||||
```
|
```
|
||||||
@ -223,6 +218,9 @@ To generate coverage reports for the Javascript tests:
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- `ASSETS_URL`: URL to host which serves static assets (such as a CDN).
|
- `ASSETS_URL`: URL to host which serves static assets (such as a CDN).
|
||||||
|
- `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account
|
||||||
|
- `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account
|
||||||
|
- `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads
|
||||||
- `BLOB_STORAGE_URL`: URL to Azure blob storage container.
|
- `BLOB_STORAGE_URL`: URL to Azure blob storage container.
|
||||||
- `CAC_URL`: URL for the CAC authentication route.
|
- `CAC_URL`: URL for the CAC authentication route.
|
||||||
- `CA_CHAIN`: Path to the CA chain file.
|
- `CA_CHAIN`: Path to the CA chain file.
|
||||||
@ -238,6 +236,11 @@ To generate coverage reports for the Javascript tests:
|
|||||||
- `ENVIRONMENT`: String specifying the current environment. Acceptable values: "dev", "prod".
|
- `ENVIRONMENT`: String specifying the current environment. Acceptable values: "dev", "prod".
|
||||||
- `LIMIT_CONCURRENT_SESSIONS`: Boolean specifying if users should be allowed only one active session at a time.
|
- `LIMIT_CONCURRENT_SESSIONS`: Boolean specifying if users should be allowed only one active session at a time.
|
||||||
- `LOG_JSON`: Boolean specifying whether app should log in a json format.
|
- `LOG_JSON`: Boolean specifying whether app should log in a json format.
|
||||||
|
- `MAIL_PASSWORD`: String. Password for the SMTP server.
|
||||||
|
- `MAIL_PORT`: Integer. Port to use on the SMTP server.
|
||||||
|
- `MAIL_SENDER`: String. Email address to send outgoing mail from.
|
||||||
|
- `MAIL_SERVER`: The SMTP host
|
||||||
|
- `MAIL_TLS`: Boolean. Use TLS to connect to the SMTP server.
|
||||||
- `PERMANENT_SESSION_LIFETIME`: Integer specifying how many seconds a user's session can stay valid for. https://flask.palletsprojects.com/en/1.1.x/config/#PERMANENT_SESSION_LIFETIME
|
- `PERMANENT_SESSION_LIFETIME`: Integer specifying how many seconds a user's session can stay valid for. https://flask.palletsprojects.com/en/1.1.x/config/#PERMANENT_SESSION_LIFETIME
|
||||||
- `PGDATABASE`: String specifying the name of the postgres database.
|
- `PGDATABASE`: String specifying the name of the postgres database.
|
||||||
- `PGHOST`: String specifying the hostname of the postgres database.
|
- `PGHOST`: String specifying the hostname of the postgres database.
|
||||||
@ -270,33 +273,8 @@ execute UI tests than vanilla Selenium. Ghost Inspector tests and steps can
|
|||||||
be exported to files that the Selenium IDE can import. We export these tests/steps
|
be exported to files that the Selenium IDE can import. We export these tests/steps
|
||||||
regularly and archive them with the AT-AT codebase in the `uitests` directory.
|
regularly and archive them with the AT-AT codebase in the `uitests` directory.
|
||||||
|
|
||||||
To run the Ghost Inspector tests against a local instance of AT-AT,
|
For further information about Ghost Inspector and its use in AT-AT, check out [its README](./uitests/README.md)
|
||||||
you will need the following:
|
in the `uitests` directory.
|
||||||
|
|
||||||
- [docker](https://docs.docker.com/v17.12/install/)
|
|
||||||
- [circleci CLI tool](https://circleci.com/docs/2.0/local-cli/#installation)
|
|
||||||
- the prerequisite variable information listed [here](https://ghostinspector.com/docs/integration/circle-ci/)
|
|
||||||
|
|
||||||
The version of our CircleCI config (2.1) is incompatible with the
|
|
||||||
`circleci` tool. First run:
|
|
||||||
|
|
||||||
```
|
|
||||||
circleci config process .circleci/config.yml > local-ci.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the job:
|
|
||||||
|
|
||||||
```
|
|
||||||
circleci local execute -e GI_SUITE=<SUITE_ID> -e GI_API_KEY=<API KEY> -e NGROK_TOKEN=<NGROK TOKEN> --job integration-tests -c local-ci.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
If the job fails and you want to re-run it, you may receive errors
|
|
||||||
about running docker containers or the network already existing.
|
|
||||||
Some version of the following should reset your local docker state:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker container stop redis postgres test-atat; docker container rm redis postgres test-atat ; docker network rm atat
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
"""add unique constraint to task order number
|
||||||
|
|
||||||
|
Revision ID: 3bd8552f1c57
|
||||||
|
Revises: 802071bcd013
|
||||||
|
Create Date: 2019-12-10 12:45:17.535973
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3bd8552f1c57' # pragma: allowlist secret
|
||||||
|
down_revision = '802071bcd013' # pragma: allowlist secret
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_unique_constraint('task_orders_number_key', 'task_orders', ['number'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('task_orders_number_key', 'task_orders', type_='unique')
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,198 @@
|
|||||||
|
"""update schema based on business logic
|
||||||
|
|
||||||
|
Revision ID: 67a2151d6269
|
||||||
|
Revises: 687fd43489d6
|
||||||
|
Create Date: 2019-12-02 14:16:24.902108
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '67a2151d6269' # pragma: allowlist secret
|
||||||
|
down_revision = '687fd43489d6' # pragma: allowlist secret
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('application_invitations', 'application_role_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('application_invitations', 'dod_id',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('application_invitations', 'expiration_time',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('application_invitations', 'first_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('application_invitations', 'inviter_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('application_invitations', 'last_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('application_invitations', 'token',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('application_roles', 'status',
|
||||||
|
existing_type=sa.VARCHAR(length=8),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('clins', 'end_date',
|
||||||
|
existing_type=sa.DATE(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('clins', 'jedi_clin_type',
|
||||||
|
existing_type=sa.VARCHAR(length=11),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('clins', 'number',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('clins', 'obligated_amount',
|
||||||
|
existing_type=sa.NUMERIC(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('clins', 'start_date',
|
||||||
|
existing_type=sa.DATE(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('clins', 'total_amount',
|
||||||
|
existing_type=sa.NUMERIC(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('environment_roles', 'status',
|
||||||
|
existing_type=sa.VARCHAR(length=9),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_invitations', 'dod_id',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_invitations', 'expiration_time',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_invitations', 'first_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_invitations', 'inviter_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_invitations', 'last_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_invitations', 'portfolio_role_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_invitations', 'token',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolio_roles', 'status',
|
||||||
|
existing_type=sa.VARCHAR(length=8),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolios', 'defense_component',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('portfolios', 'name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('task_orders', 'portfolio_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
op.drop_constraint('task_orders_user_id_fkey', 'task_orders', type_='foreignkey')
|
||||||
|
op.drop_column('task_orders', 'user_id')
|
||||||
|
op.alter_column('users', 'first_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
op.alter_column('users', 'last_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('users', 'last_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('users', 'first_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.add_column('task_orders', sa.Column('user_id', postgresql.UUID(), autoincrement=False, nullable=True))
|
||||||
|
op.create_foreign_key('task_orders_user_id_fkey', 'task_orders', 'users', ['user_id'], ['id'])
|
||||||
|
op.alter_column('task_orders', 'portfolio_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolios', 'name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolios', 'defense_component',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_roles', 'status',
|
||||||
|
existing_type=sa.VARCHAR(length=8),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_invitations', 'token',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_invitations', 'portfolio_role_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_invitations', 'last_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_invitations', 'inviter_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_invitations', 'first_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_invitations', 'expiration_time',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('portfolio_invitations', 'dod_id',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('environment_roles', 'status',
|
||||||
|
existing_type=sa.VARCHAR(length=9),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('clins', 'total_amount',
|
||||||
|
existing_type=sa.NUMERIC(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('clins', 'start_date',
|
||||||
|
existing_type=sa.DATE(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('clins', 'obligated_amount',
|
||||||
|
existing_type=sa.NUMERIC(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('clins', 'number',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('clins', 'jedi_clin_type',
|
||||||
|
existing_type=sa.VARCHAR(length=11),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('clins', 'end_date',
|
||||||
|
existing_type=sa.DATE(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_roles', 'status',
|
||||||
|
existing_type=sa.VARCHAR(length=8),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_invitations', 'token',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_invitations', 'last_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_invitations', 'inviter_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_invitations', 'first_name',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_invitations', 'expiration_time',
|
||||||
|
existing_type=postgresql.TIMESTAMP(timezone=True),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_invitations', 'dod_id',
|
||||||
|
existing_type=sa.VARCHAR(),
|
||||||
|
nullable=True)
|
||||||
|
op.alter_column('application_invitations', 'application_role_id',
|
||||||
|
existing_type=postgresql.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,40 @@
|
|||||||
|
"""Remove unneeded portfolio columns
|
||||||
|
|
||||||
|
Revision ID: 802071bcd013
|
||||||
|
Revises: 67a2151d6269
|
||||||
|
Create Date: 2019-12-11 13:26:34.770480
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '802071bcd013' # pragma: allowlist secret
|
||||||
|
down_revision = '67a2151d6269' # pragma: allowlist secret
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('portfolios', 'dev_team')
|
||||||
|
op.drop_column('portfolios', 'complexity')
|
||||||
|
op.drop_column('portfolios', 'team_experience')
|
||||||
|
op.drop_column('portfolios', 'dev_team_other')
|
||||||
|
op.drop_column('portfolios', 'app_migration')
|
||||||
|
op.drop_column('portfolios', 'native_apps')
|
||||||
|
op.drop_column('portfolios', 'complexity_other')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('portfolios', sa.Column('complexity_other', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('portfolios', sa.Column('native_apps', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('portfolios', sa.Column('app_migration', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('portfolios', sa.Column('dev_team_other', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('portfolios', sa.Column('team_experience', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('portfolios', sa.Column('complexity', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('portfolios', sa.Column('dev_team', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
68
atst/app.py
68
atst/app.py
@ -200,46 +200,78 @@ def make_config(direct_config=None):
|
|||||||
ENV_CONFIG_FILENAME = os.path.join(
|
ENV_CONFIG_FILENAME = os.path.join(
|
||||||
os.path.dirname(__file__), "../config/", "{}.ini".format(ENV.lower())
|
os.path.dirname(__file__), "../config/", "{}.ini".format(ENV.lower())
|
||||||
)
|
)
|
||||||
OVERRIDE_CONFIG_FILENAME = os.getenv("OVERRIDE_CONFIG_FULLPATH")
|
OVERRIDE_CONFIG_DIRECTORY = os.getenv("OVERRIDE_CONFIG_DIRECTORY")
|
||||||
|
|
||||||
config = ConfigParser(allow_no_value=True)
|
config = ConfigParser(allow_no_value=True)
|
||||||
config.optionxform = str
|
config.optionxform = str
|
||||||
|
|
||||||
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
|
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
|
||||||
if OVERRIDE_CONFIG_FILENAME:
|
|
||||||
config_files.append(OVERRIDE_CONFIG_FILENAME)
|
|
||||||
|
|
||||||
# ENV_CONFIG will override values in BASE_CONFIG.
|
# ENV_CONFIG will override values in BASE_CONFIG.
|
||||||
config.read(config_files)
|
config.read(config_files)
|
||||||
|
|
||||||
|
if OVERRIDE_CONFIG_DIRECTORY:
|
||||||
|
apply_config_from_directory(OVERRIDE_CONFIG_DIRECTORY, config)
|
||||||
|
|
||||||
# Check for ENV variables as a final source of overrides
|
# Check for ENV variables as a final source of overrides
|
||||||
for confsetting in config.options("default"):
|
apply_config_from_environment(config)
|
||||||
env_override = os.getenv(confsetting.upper())
|
|
||||||
if env_override:
|
|
||||||
config.set("default", confsetting, env_override)
|
|
||||||
|
|
||||||
# override if a dictionary of options has been given
|
# override if a dictionary of options has been given
|
||||||
if direct_config:
|
if direct_config:
|
||||||
config.read_dict({"default": direct_config})
|
config.read_dict({"default": direct_config})
|
||||||
|
|
||||||
# Assemble DATABASE_URI value
|
# Assemble DATABASE_URI value
|
||||||
database_uri = (
|
database_uri = "postgres://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
|
||||||
"postgres://"
|
config.get("default", "PGUSER"),
|
||||||
+ config.get("default", "PGUSER")
|
config.get("default", "PGPASSWORD"),
|
||||||
+ ":"
|
config.get("default", "PGHOST"),
|
||||||
+ config.get("default", "PGPASSWORD")
|
config.get("default", "PGPORT"),
|
||||||
+ "@"
|
config.get("default", "PGDATABASE"),
|
||||||
+ config.get("default", "PGHOST")
|
|
||||||
+ ":"
|
|
||||||
+ config.get("default", "PGPORT")
|
|
||||||
+ "/"
|
|
||||||
+ config.get("default", "PGDATABASE")
|
|
||||||
)
|
)
|
||||||
config.set("default", "DATABASE_URI", database_uri)
|
config.set("default", "DATABASE_URI", database_uri)
|
||||||
|
|
||||||
|
# Assemble REDIS_URI value
|
||||||
|
redis_uri = "redis{}://{}:{}@{}".format( # pragma: allowlist secret
|
||||||
|
("s" if config["default"].getboolean("REDIS_TLS") else ""),
|
||||||
|
(config.get("default", "REDIS_USER") or ""),
|
||||||
|
(config.get("default", "REDIS_PASSWORD") or ""),
|
||||||
|
config.get("default", "REDIS_HOST"),
|
||||||
|
)
|
||||||
|
config.set("default", "REDIS_URI", redis_uri)
|
||||||
|
|
||||||
return map_config(config)
|
return map_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_config_from_directory(config_dir, config, section="default"):
|
||||||
|
"""
|
||||||
|
Loop files in a directory, check if the names correspond to
|
||||||
|
known config values, and apply the file contents as the value
|
||||||
|
for that setting if they do.
|
||||||
|
"""
|
||||||
|
for confsetting in os.listdir(config_dir):
|
||||||
|
if confsetting in config.options(section):
|
||||||
|
full_path = os.path.join(config_dir, confsetting)
|
||||||
|
with open(full_path, "r") as conf_file:
|
||||||
|
config.set(section, confsetting, conf_file.read().strip())
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def apply_config_from_environment(config, section="default"):
|
||||||
|
"""
|
||||||
|
Loops all the configuration settins in a given section of a
|
||||||
|
config object and checks whether those settings also exist as
|
||||||
|
environment variables. If so, it applies the environment
|
||||||
|
variables value as the new configuration setting value.
|
||||||
|
"""
|
||||||
|
for confsetting in config.options(section):
|
||||||
|
env_override = os.getenv(confsetting.upper())
|
||||||
|
if env_override:
|
||||||
|
config.set(section, confsetting, env_override)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def make_redis(app, config):
|
def make_redis(app, config):
|
||||||
r = redis.Redis.from_url(config["REDIS_URI"])
|
r = redis.Redis.from_url(config["REDIS_URI"])
|
||||||
app.redis = r
|
app.redis = r
|
||||||
|
@ -3,6 +3,7 @@ import re
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from atst.models.user import User
|
from atst.models.user import User
|
||||||
|
from atst.models.application import Application
|
||||||
from atst.models.environment import Environment
|
from atst.models.environment import Environment
|
||||||
from atst.models.environment_role import EnvironmentRole
|
from atst.models.environment_role import EnvironmentRole
|
||||||
|
|
||||||
@ -399,13 +400,14 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
|
|||||||
|
|
||||||
class AzureSDKProvider(object):
|
class AzureSDKProvider(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from azure.mgmt import subscription, authorization
|
from azure.mgmt import subscription, authorization, managementgroups
|
||||||
import azure.graphrbac as graphrbac
|
import azure.graphrbac as graphrbac
|
||||||
import azure.common.credentials as credentials
|
import azure.common.credentials as credentials
|
||||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||||
|
|
||||||
self.subscription = subscription
|
self.subscription = subscription
|
||||||
self.authorization = authorization
|
self.authorization = authorization
|
||||||
|
self.managementgroups = managementgroups
|
||||||
self.graphrbac = graphrbac
|
self.graphrbac = graphrbac
|
||||||
self.credentials = credentials
|
self.credentials = credentials
|
||||||
# may change to a JEDI cloud
|
# may change to a JEDI cloud
|
||||||
@ -428,42 +430,23 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
def create_environment(
|
def create_environment(
|
||||||
self, auth_credentials: Dict, user: User, environment: Environment
|
self, auth_credentials: Dict, user: User, environment: Environment
|
||||||
):
|
):
|
||||||
|
# since this operation would only occur within a tenant, should we source the tenant
|
||||||
|
# via lookup from environment once we've created the portfolio csp data schema
|
||||||
|
# something like this:
|
||||||
|
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
|
||||||
|
# though we'd probably source the whole credentials for these calls from the portfolio csp
|
||||||
|
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
|
||||||
|
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
|
||||||
credentials = self._get_credential_obj(self._root_creds)
|
credentials = self._get_credential_obj(self._root_creds)
|
||||||
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
|
||||||
|
|
||||||
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
|
||||||
|
management_group_id = "?" # management group id chained from environment
|
||||||
|
parent_id = "?" # from environment.application
|
||||||
|
|
||||||
billing_profile_id = "?" # something chained from environment?
|
management_group = self._create_management_group(
|
||||||
sku_id = AZURE_SKU_ID
|
credentials, management_group_id, display_name, parent_id,
|
||||||
# we want to set AT-AT as an owner here
|
|
||||||
# we could potentially associate subscriptions with "management groups" per DOD component
|
|
||||||
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
|
||||||
display_name,
|
|
||||||
billing_profile_id,
|
|
||||||
sku_id,
|
|
||||||
# owner=<AdPrincipal: for AT-AT user>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
return management_group
|
||||||
# TOs filed for the environment
|
|
||||||
billing_account_name = "?"
|
|
||||||
invoice_section_name = "?"
|
|
||||||
# We may also want to create billing sections in the enrollment account
|
|
||||||
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
|
||||||
billing_account_name, invoice_section_name, body
|
|
||||||
)
|
|
||||||
|
|
||||||
# the resulting object from this process is a link to the new subscription
|
|
||||||
# not a subscription model, so we'll have to unpack the ID
|
|
||||||
new_sub = sub_creation_operation.result()
|
|
||||||
|
|
||||||
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
|
||||||
if subscription_id:
|
|
||||||
return subscription_id
|
|
||||||
else:
|
|
||||||
# troublesome error, subscription should exist at this point
|
|
||||||
# but we just don't have a valid ID
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_atat_admin_user(
|
def create_atat_admin_user(
|
||||||
self, auth_credentials: Dict, csp_environment_id: str
|
self, auth_credentials: Dict, csp_environment_id: str
|
||||||
@ -502,6 +485,82 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
"role_name": role_assignment_id,
|
"role_name": role_assignment_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _create_application(self, auth_credentials: Dict, application: Application):
|
||||||
|
management_group_name = str(uuid4()) # can be anything, not just uuid
|
||||||
|
display_name = application.name # Does this need to be unique?
|
||||||
|
credentials = self._get_credential_obj(auth_credentials)
|
||||||
|
parent_id = "?" # application.portfolio.csp_details.management_group_id
|
||||||
|
|
||||||
|
return self._create_management_group(
|
||||||
|
credentials, management_group_name, display_name, parent_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_management_group(
|
||||||
|
self, credentials, management_group_id, display_name, parent_id=None,
|
||||||
|
):
|
||||||
|
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
|
||||||
|
create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo(
|
||||||
|
id=parent_id
|
||||||
|
)
|
||||||
|
create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails(
|
||||||
|
parent=create_parent_grp_info
|
||||||
|
)
|
||||||
|
mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest(
|
||||||
|
name=management_group_id,
|
||||||
|
display_name=display_name,
|
||||||
|
details=create_mgmt_grp_details,
|
||||||
|
)
|
||||||
|
create_request = mgmgt_group_client.management_groups.create_or_update(
|
||||||
|
management_group_id, mgmt_grp_create
|
||||||
|
)
|
||||||
|
|
||||||
|
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
|
||||||
|
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally
|
||||||
|
return create_request.result()
|
||||||
|
|
||||||
|
def _create_subscription(
|
||||||
|
self,
|
||||||
|
credentials,
|
||||||
|
display_name,
|
||||||
|
billing_profile_id,
|
||||||
|
sku_id,
|
||||||
|
management_group_id,
|
||||||
|
billing_account_name,
|
||||||
|
invoice_section_name,
|
||||||
|
):
|
||||||
|
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
|
||||||
|
|
||||||
|
billing_profile_id = "?" # where do we source this?
|
||||||
|
sku_id = AZURE_SKU_ID
|
||||||
|
# These 2 seem like something that might be worthwhile to allow tiebacks to
|
||||||
|
# TOs filed for the environment
|
||||||
|
billing_account_name = "?" # from TO?
|
||||||
|
invoice_section_name = "?" # from TO?
|
||||||
|
|
||||||
|
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
|
||||||
|
display_name=display_name,
|
||||||
|
billing_profile_id=billing_profile_id,
|
||||||
|
sku_id=sku_id,
|
||||||
|
management_group_id=management_group_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We may also want to create billing sections in the enrollment account
|
||||||
|
sub_creation_operation = sub_client.subscription_factory.create_subscription(
|
||||||
|
billing_account_name, invoice_section_name, body
|
||||||
|
)
|
||||||
|
|
||||||
|
# the resulting object from this process is a link to the new subscription
|
||||||
|
# not a subscription model, so we'll have to unpack the ID
|
||||||
|
new_sub = sub_creation_operation.result()
|
||||||
|
|
||||||
|
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
|
||||||
|
if subscription_id:
|
||||||
|
return subscription_id
|
||||||
|
else:
|
||||||
|
# troublesome error, subscription should exist at this point
|
||||||
|
# but we just don't have a valid ID
|
||||||
|
pass
|
||||||
|
|
||||||
def _get_management_service_principal(self):
|
def _get_management_service_principal(self):
|
||||||
# we really should be using graph.microsoft.com, but i'm getting
|
# we really should be using graph.microsoft.com, but i'm getting
|
||||||
# "expired token" errors for that
|
# "expired token" errors for that
|
||||||
|
@ -1,327 +1,121 @@
|
|||||||
from itertools import groupby
|
from collections import defaultdict
|
||||||
import pendulum
|
import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
|
|
||||||
class ReportingInterface:
|
def load_fixture_data():
|
||||||
def monthly_totals_for_environment(environment):
|
with open("fixtures/fixture_spend_data.json") as json_file:
|
||||||
"""Return the monthly totals for the specified environment.
|
return json.load(json_file)
|
||||||
|
|
||||||
Data should be in the format of a dictionary with the month as the key
|
|
||||||
and the spend in that month as the value. For example:
|
|
||||||
|
|
||||||
{ "01/2018": 79.85, "02/2018": 86.54 }
|
class MockReportingProvider:
|
||||||
|
FIXTURE_SPEND_DATA = load_fixture_data()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_portfolio_monthly_spending(cls, portfolio):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
returns an array of application and environment spending for the
|
||||||
|
portfolio. Applications and their nested environments are sorted in
|
||||||
|
alphabetical order by name.
|
||||||
class MockEnvironment:
|
|
||||||
def __init__(self, id_, env_name):
|
|
||||||
self.id = id_
|
|
||||||
self.name = env_name
|
|
||||||
|
|
||||||
|
|
||||||
class MockApplication:
|
|
||||||
def __init__(self, application_name, envs):
|
|
||||||
def make_env(name):
|
|
||||||
return MockEnvironment("{}_{}".format(application_name, name), name)
|
|
||||||
|
|
||||||
self.name = application_name
|
|
||||||
self.environments = [make_env(env_name) for env_name in envs]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_sample_dates(_max=8):
|
|
||||||
current = pendulum.now()
|
|
||||||
sample_dates = []
|
|
||||||
for _i in range(_max):
|
|
||||||
sample_dates.append(current.strftime("%m/%Y"))
|
|
||||||
current = current.subtract(months=1)
|
|
||||||
|
|
||||||
reversed(sample_dates)
|
|
||||||
return sample_dates
|
|
||||||
|
|
||||||
|
|
||||||
class MockReportingProvider(ReportingInterface):
|
|
||||||
MOCK_PERCENT_EXPENDED_FUNDS = 0.75
|
|
||||||
|
|
||||||
FIXTURE_MONTHS = generate_sample_dates()
|
|
||||||
|
|
||||||
MONTHLY_SPEND_BY_ENVIRONMENT = {
|
|
||||||
"LC04_Integ": {
|
|
||||||
FIXTURE_MONTHS[7]: 284,
|
|
||||||
FIXTURE_MONTHS[6]: 1210,
|
|
||||||
FIXTURE_MONTHS[5]: 1430,
|
|
||||||
FIXTURE_MONTHS[4]: 1366,
|
|
||||||
FIXTURE_MONTHS[3]: 1169,
|
|
||||||
FIXTURE_MONTHS[2]: 991,
|
|
||||||
FIXTURE_MONTHS[1]: 978,
|
|
||||||
FIXTURE_MONTHS[0]: 737,
|
|
||||||
},
|
|
||||||
"LC04_PreProd": {
|
|
||||||
FIXTURE_MONTHS[7]: 812,
|
|
||||||
FIXTURE_MONTHS[6]: 1389,
|
|
||||||
FIXTURE_MONTHS[5]: 1425,
|
|
||||||
FIXTURE_MONTHS[4]: 1306,
|
|
||||||
FIXTURE_MONTHS[3]: 1112,
|
|
||||||
FIXTURE_MONTHS[2]: 936,
|
|
||||||
FIXTURE_MONTHS[1]: 921,
|
|
||||||
FIXTURE_MONTHS[0]: 694,
|
|
||||||
},
|
|
||||||
"LC04_Prod": {
|
|
||||||
FIXTURE_MONTHS[7]: 1742,
|
|
||||||
FIXTURE_MONTHS[6]: 1716,
|
|
||||||
FIXTURE_MONTHS[5]: 1866,
|
|
||||||
FIXTURE_MONTHS[4]: 1809,
|
|
||||||
FIXTURE_MONTHS[3]: 1839,
|
|
||||||
FIXTURE_MONTHS[2]: 1633,
|
|
||||||
FIXTURE_MONTHS[1]: 1654,
|
|
||||||
FIXTURE_MONTHS[0]: 1103,
|
|
||||||
},
|
|
||||||
"SF18_Integ": {
|
|
||||||
FIXTURE_MONTHS[5]: 1498,
|
|
||||||
FIXTURE_MONTHS[4]: 1400,
|
|
||||||
FIXTURE_MONTHS[3]: 1394,
|
|
||||||
FIXTURE_MONTHS[2]: 1171,
|
|
||||||
FIXTURE_MONTHS[1]: 1200,
|
|
||||||
FIXTURE_MONTHS[0]: 963,
|
|
||||||
},
|
|
||||||
"SF18_PreProd": {
|
|
||||||
FIXTURE_MONTHS[5]: 1780,
|
|
||||||
FIXTURE_MONTHS[4]: 1667,
|
|
||||||
FIXTURE_MONTHS[3]: 1703,
|
|
||||||
FIXTURE_MONTHS[2]: 1474,
|
|
||||||
FIXTURE_MONTHS[1]: 1441,
|
|
||||||
FIXTURE_MONTHS[0]: 933,
|
|
||||||
},
|
|
||||||
"SF18_Prod": {
|
|
||||||
FIXTURE_MONTHS[5]: 1686,
|
|
||||||
FIXTURE_MONTHS[4]: 1779,
|
|
||||||
FIXTURE_MONTHS[3]: 1792,
|
|
||||||
FIXTURE_MONTHS[2]: 1570,
|
|
||||||
FIXTURE_MONTHS[1]: 1539,
|
|
||||||
FIXTURE_MONTHS[0]: 986,
|
|
||||||
},
|
|
||||||
"Canton_Prod": {
|
|
||||||
FIXTURE_MONTHS[4]: 28699,
|
|
||||||
FIXTURE_MONTHS[3]: 26766,
|
|
||||||
FIXTURE_MONTHS[2]: 22619,
|
|
||||||
FIXTURE_MONTHS[1]: 24090,
|
|
||||||
FIXTURE_MONTHS[0]: 16719,
|
|
||||||
},
|
|
||||||
"BD04_Integ": {},
|
|
||||||
"BD04_PreProd": {
|
|
||||||
FIXTURE_MONTHS[7]: 7019,
|
|
||||||
FIXTURE_MONTHS[6]: 3004,
|
|
||||||
FIXTURE_MONTHS[5]: 2691,
|
|
||||||
FIXTURE_MONTHS[4]: 2901,
|
|
||||||
FIXTURE_MONTHS[3]: 3463,
|
|
||||||
FIXTURE_MONTHS[2]: 3314,
|
|
||||||
FIXTURE_MONTHS[1]: 3432,
|
|
||||||
FIXTURE_MONTHS[0]: 723,
|
|
||||||
},
|
|
||||||
"SCV18_Dev": {FIXTURE_MONTHS[1]: 9797},
|
|
||||||
"Crown_CR Portal Dev": {
|
|
||||||
FIXTURE_MONTHS[6]: 208,
|
|
||||||
FIXTURE_MONTHS[5]: 457,
|
|
||||||
FIXTURE_MONTHS[4]: 671,
|
|
||||||
FIXTURE_MONTHS[3]: 136,
|
|
||||||
FIXTURE_MONTHS[2]: 1524,
|
|
||||||
FIXTURE_MONTHS[1]: 2077,
|
|
||||||
FIXTURE_MONTHS[0]: 1858,
|
|
||||||
},
|
|
||||||
"Crown_CR Staging": {
|
|
||||||
FIXTURE_MONTHS[6]: 208,
|
|
||||||
FIXTURE_MONTHS[5]: 457,
|
|
||||||
FIXTURE_MONTHS[4]: 671,
|
|
||||||
FIXTURE_MONTHS[3]: 136,
|
|
||||||
FIXTURE_MONTHS[2]: 1524,
|
|
||||||
FIXTURE_MONTHS[1]: 2077,
|
|
||||||
FIXTURE_MONTHS[0]: 1858,
|
|
||||||
},
|
|
||||||
"Crown_CR Portal Test 1": {
|
|
||||||
FIXTURE_MONTHS[2]: 806,
|
|
||||||
FIXTURE_MONTHS[1]: 1966,
|
|
||||||
FIXTURE_MONTHS[0]: 2597,
|
|
||||||
},
|
|
||||||
"Crown_Jewels Prod": {
|
|
||||||
FIXTURE_MONTHS[2]: 806,
|
|
||||||
FIXTURE_MONTHS[1]: 1966,
|
|
||||||
FIXTURE_MONTHS[0]: 2597,
|
|
||||||
},
|
|
||||||
"Crown_Jewels Dev": {
|
|
||||||
FIXTURE_MONTHS[6]: 145,
|
|
||||||
FIXTURE_MONTHS[5]: 719,
|
|
||||||
FIXTURE_MONTHS[4]: 1243,
|
|
||||||
FIXTURE_MONTHS[3]: 2214,
|
|
||||||
FIXTURE_MONTHS[2]: 2959,
|
|
||||||
FIXTURE_MONTHS[1]: 4151,
|
|
||||||
FIXTURE_MONTHS[0]: 4260,
|
|
||||||
},
|
|
||||||
"NP02_Integ": {FIXTURE_MONTHS[1]: 284, FIXTURE_MONTHS[0]: 1210},
|
|
||||||
"NP02_PreProd": {FIXTURE_MONTHS[1]: 812, FIXTURE_MONTHS[0]: 1389},
|
|
||||||
"NP02_Prod": {FIXTURE_MONTHS[1]: 3742, FIXTURE_MONTHS[0]: 4716},
|
|
||||||
"FM_Integ": {FIXTURE_MONTHS[1]: 1498},
|
|
||||||
"FM_Prod": {FIXTURE_MONTHS[0]: 5686},
|
|
||||||
}
|
|
||||||
|
|
||||||
REPORT_FIXTURE_MAP = {
|
|
||||||
"A-Wing": {
|
|
||||||
"applications": [
|
|
||||||
MockApplication("LC04", ["Integ", "PreProd", "Prod"]),
|
|
||||||
MockApplication("SF18", ["Integ", "PreProd", "Prod"]),
|
|
||||||
MockApplication("Canton", ["Prod"]),
|
|
||||||
MockApplication("BD04", ["Integ", "PreProd"]),
|
|
||||||
MockApplication("SCV18", ["Dev"]),
|
|
||||||
MockApplication(
|
|
||||||
"Crown",
|
|
||||||
[
|
[
|
||||||
"CR Portal Dev",
|
|
||||||
"CR Staging",
|
|
||||||
"CR Portal Test 1",
|
|
||||||
"Jewels Prod",
|
|
||||||
"Jewels Dev",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
"budget": 500_000,
|
|
||||||
},
|
|
||||||
"B-Wing": {
|
|
||||||
"applications": [
|
|
||||||
MockApplication("NP02", ["Integ", "PreProd", "Prod"]),
|
|
||||||
MockApplication("FM", ["Integ", "Prod"]),
|
|
||||||
],
|
|
||||||
"budget": 70000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _rollup_application_totals(self, data):
|
|
||||||
application_totals = {}
|
|
||||||
for application, environments in data.items():
|
|
||||||
application_spend = [
|
|
||||||
(month, spend)
|
|
||||||
for env in environments.values()
|
|
||||||
if env
|
|
||||||
for month, spend in env.items()
|
|
||||||
]
|
|
||||||
application_totals[application] = {
|
|
||||||
month: sum([spend[1] for spend in spends])
|
|
||||||
for month, spends in groupby(sorted(application_spend), lambda x: x[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return application_totals
|
|
||||||
|
|
||||||
def _rollup_portfolio_totals(self, application_totals):
|
|
||||||
monthly_spend = [
|
|
||||||
(month, spend)
|
|
||||||
for application in application_totals.values()
|
|
||||||
for month, spend in application.items()
|
|
||||||
]
|
|
||||||
portfolio_totals = {}
|
|
||||||
for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]):
|
|
||||||
portfolio_totals[month] = sum([spend[1] for spend in spends])
|
|
||||||
|
|
||||||
return portfolio_totals
|
|
||||||
|
|
||||||
def monthly_totals_for_environment(self, environment_id):
|
|
||||||
"""Return the monthly totals for the specified environment.
|
|
||||||
|
|
||||||
Data should be in the format of a dictionary with the month as the key
|
|
||||||
and the spend in that month as the value. For example:
|
|
||||||
|
|
||||||
{ "01/2018": 79.85, "02/2018": 86.54 }
|
|
||||||
|
|
||||||
"""
|
|
||||||
environment_monthly_totals = self.MONTHLY_SPEND_BY_ENVIRONMENT.get(
|
|
||||||
environment_id, {}
|
|
||||||
).copy()
|
|
||||||
|
|
||||||
environment_monthly_totals["total_spend_to_date"] = sum(
|
|
||||||
monthly_total for monthly_total in environment_monthly_totals.values()
|
|
||||||
)
|
|
||||||
return environment_monthly_totals
|
|
||||||
|
|
||||||
def monthly_totals(self, portfolio):
|
|
||||||
"""Return month totals rolled up by environment, application, and portfolio.
|
|
||||||
|
|
||||||
Data should returned with three top level keys, "portfolio", "applications",
|
|
||||||
and "environments".
|
|
||||||
The "applications" key will have budget data per month for each application,
|
|
||||||
The "environments" key will have budget data for each environment.
|
|
||||||
The "portfolio" key will be total monthly spending for the portfolio.
|
|
||||||
For example:
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } },
|
name
|
||||||
"applications": { "X-Wing": { "01/2018": 75.42 } },
|
this_month
|
||||||
"portfolio": { "01/2018": 75.42 },
|
last_month
|
||||||
|
total
|
||||||
|
environments [
|
||||||
|
{
|
||||||
|
name
|
||||||
|
this_month
|
||||||
|
last_month
|
||||||
|
total
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
"""
|
"""
|
||||||
applications = portfolio.applications
|
if portfolio.name in cls.FIXTURE_SPEND_DATA:
|
||||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
applications = cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]
|
||||||
applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"]
|
return sorted(
|
||||||
environments = {
|
[
|
||||||
application.name: {
|
cls._get_application_monthly_totals(application)
|
||||||
env.name: self.monthly_totals_for_environment(env.id)
|
|
||||||
for env in application.environments
|
|
||||||
}
|
|
||||||
for application in applications
|
for application in applications
|
||||||
}
|
],
|
||||||
|
key=lambda app: app["name"],
|
||||||
application_totals = self._rollup_application_totals(environments)
|
|
||||||
portfolio_totals = self._rollup_portfolio_totals(application_totals)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"environments": environments,
|
|
||||||
"applications": application_totals,
|
|
||||||
"portfolio": portfolio_totals,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_obligated_funds_by_JEDI_clin(self, portfolio):
|
|
||||||
"""
|
|
||||||
Returns a dictionary of obligated funds and spending per JEDI CLIN
|
|
||||||
{
|
|
||||||
JEDI_CLIN: {
|
|
||||||
obligated_funds,
|
|
||||||
expended_funds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
if portfolio.name in self.REPORT_FIXTURE_MAP:
|
|
||||||
return_dict = {}
|
|
||||||
for jedi_clin, clins in groupby(
|
|
||||||
portfolio.active_clins, lambda clin: clin.jedi_clin_type
|
|
||||||
):
|
|
||||||
obligated_funds = sum(clin.obligated_amount for clin in clins)
|
|
||||||
return_dict[jedi_clin.value] = {
|
|
||||||
"obligated_funds": obligated_funds,
|
|
||||||
"expended_funds": (
|
|
||||||
obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return OrderedDict(
|
|
||||||
# 0 index for dict item, -1 for last digit of 4 digit CLIN, e.g. 0001
|
|
||||||
sorted(return_dict.items(), key=lambda clin: clin[0][-1])
|
|
||||||
)
|
)
|
||||||
return {}
|
return []
|
||||||
|
|
||||||
def get_expired_task_orders(self, portfolio):
|
@classmethod
|
||||||
return [
|
def _get_environment_monthly_totals(cls, environment):
|
||||||
|
"""
|
||||||
|
returns a dictionary that represents spending totals for an environment e.g.
|
||||||
{
|
{
|
||||||
"id": task_order.id,
|
name
|
||||||
"number": task_order.number,
|
this_month
|
||||||
"period_of_performance": {
|
last_month
|
||||||
"start_date": task_order.start_date,
|
total
|
||||||
"end_date": task_order.end_date,
|
}
|
||||||
},
|
"""
|
||||||
"total_obligated_funds": task_order.total_obligated_funds,
|
return {
|
||||||
"expended_funds": (
|
"name": environment["name"],
|
||||||
task_order.total_obligated_funds
|
"this_month": sum(environment["spending"]["this_month"].values()),
|
||||||
* Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
|
"last_month": sum(environment["spending"]["last_month"].values()),
|
||||||
),
|
"total": sum(environment["spending"]["total"].values()),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_application_monthly_totals(cls, application):
|
||||||
|
"""
|
||||||
|
returns a dictionary that represents spending totals for an application
|
||||||
|
and its environments e.g.
|
||||||
|
{
|
||||||
|
name
|
||||||
|
this_month
|
||||||
|
last_month
|
||||||
|
total
|
||||||
|
environments: [
|
||||||
|
{
|
||||||
|
name
|
||||||
|
this_month
|
||||||
|
last_month
|
||||||
|
total
|
||||||
}
|
}
|
||||||
for task_order in portfolio.task_orders
|
|
||||||
if task_order.is_expired
|
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
environments = sorted(
|
||||||
|
[
|
||||||
|
cls._get_environment_monthly_totals(env)
|
||||||
|
for env in application["environments"]
|
||||||
|
],
|
||||||
|
key=lambda env: env["name"],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"name": application["name"],
|
||||||
|
"this_month": sum(env["this_month"] for env in environments),
|
||||||
|
"last_month": sum(env["last_month"] for env in environments),
|
||||||
|
"total": sum(env["total"] for env in environments),
|
||||||
|
"environments": environments,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_spending_by_JEDI_clin(cls, portfolio):
|
||||||
|
"""
|
||||||
|
returns an dictionary of spending per JEDI CLIN for a portfolio
|
||||||
|
{
|
||||||
|
jedi_clin: {
|
||||||
|
invoiced
|
||||||
|
estimated
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if portfolio.name in cls.FIXTURE_SPEND_DATA:
|
||||||
|
CLIN_spend_dict = defaultdict(lambda: defaultdict(Decimal))
|
||||||
|
for application in cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]:
|
||||||
|
for environment in application["environments"]:
|
||||||
|
for clin, spend in environment["spending"]["this_month"].items():
|
||||||
|
CLIN_spend_dict[clin]["estimated"] += Decimal(spend)
|
||||||
|
for clin, spend in environment["spending"]["total"].items():
|
||||||
|
CLIN_spend_dict[clin]["invoiced"] += Decimal(spend)
|
||||||
|
return CLIN_spend_dict
|
||||||
|
return {}
|
||||||
|
@ -3,9 +3,10 @@ from flask import current_app as app
|
|||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models import (
|
from atst.models import (
|
||||||
EnvironmentRole,
|
|
||||||
ApplicationRole,
|
|
||||||
Environment,
|
Environment,
|
||||||
|
EnvironmentRole,
|
||||||
|
Application,
|
||||||
|
ApplicationRole,
|
||||||
ApplicationRoleStatus,
|
ApplicationRoleStatus,
|
||||||
)
|
)
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
@ -105,6 +106,7 @@ class EnvironmentRoles(object):
|
|||||||
def disable(cls, environment_role_id):
|
def disable(cls, environment_role_id):
|
||||||
environment_role = EnvironmentRoles.get_by_id(environment_role_id)
|
environment_role = EnvironmentRoles.get_by_id(environment_role_id)
|
||||||
|
|
||||||
|
if environment_role.csp_user_id and not environment_role.environment.is_pending:
|
||||||
credentials = environment_role.environment.csp_credentials
|
credentials = environment_role.environment.csp_credentials
|
||||||
app.csp.cloud.disable_user(credentials, environment_role.csp_user_id)
|
app.csp.cloud.disable_user(credentials, environment_role.csp_user_id)
|
||||||
|
|
||||||
@ -125,3 +127,15 @@ class EnvironmentRoles(object):
|
|||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
return existing_env_role
|
return existing_env_role
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_user(cls, user_id, portfolio_id):
|
||||||
|
return (
|
||||||
|
db.session.query(EnvironmentRole)
|
||||||
|
.join(ApplicationRole)
|
||||||
|
.join(Application)
|
||||||
|
.filter(Application.portfolio_id == portfolio_id)
|
||||||
|
.filter(ApplicationRole.application_id == Application.id)
|
||||||
|
.filter(ApplicationRole.user_id == user_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
@ -1,15 +1,44 @@
|
|||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
|
||||||
class Reports:
|
class Reports:
|
||||||
@classmethod
|
@classmethod
|
||||||
def monthly_totals(cls, portfolio):
|
def monthly_spending(cls, portfolio):
|
||||||
return current_app.csp.reports.monthly_totals(portfolio)
|
return current_app.csp.reports.get_portfolio_monthly_spending(portfolio)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def expired_task_orders(cls, portfolio):
|
def expired_task_orders(cls, portfolio):
|
||||||
return current_app.csp.reports.get_expired_task_orders(portfolio)
|
return [
|
||||||
|
task_order for task_order in portfolio.task_orders if task_order.is_expired
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def obligated_funds_by_JEDI_clin(cls, portfolio):
|
def obligated_funds_by_JEDI_clin(cls, portfolio):
|
||||||
return current_app.csp.reports.get_obligated_funds_by_JEDI_clin(portfolio)
|
clin_spending = current_app.csp.reports.get_spending_by_JEDI_clin(portfolio)
|
||||||
|
active_clins = portfolio.active_clins
|
||||||
|
for jedi_clin, clins in groupby(
|
||||||
|
active_clins, key=lambda clin: clin.jedi_clin_type
|
||||||
|
):
|
||||||
|
if not clin_spending.get(jedi_clin.name):
|
||||||
|
clin_spending[jedi_clin.name] = {}
|
||||||
|
clin_spending[jedi_clin.name]["obligated"] = sum(
|
||||||
|
clin.obligated_amount for clin in clins
|
||||||
|
)
|
||||||
|
|
||||||
|
output = []
|
||||||
|
for clin in clin_spending.keys():
|
||||||
|
invoiced = clin_spending[clin].get("invoiced", 0)
|
||||||
|
estimated = clin_spending[clin].get("estimated", 0)
|
||||||
|
obligated = clin_spending[clin].get("obligated", 0)
|
||||||
|
remaining = obligated - (invoiced + estimated)
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
"name": clin,
|
||||||
|
"invoiced": invoiced,
|
||||||
|
"estimated": estimated,
|
||||||
|
"obligated": obligated,
|
||||||
|
"remaining": remaining,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.clin import CLIN
|
from atst.models.clin import CLIN
|
||||||
from atst.models.task_order import TaskOrder, SORT_ORDERING
|
from atst.models.task_order import TaskOrder, SORT_ORDERING
|
||||||
from . import BaseDomainClass
|
from . import BaseDomainClass
|
||||||
|
from .exceptions import AlreadyExistsError
|
||||||
|
|
||||||
|
|
||||||
class TaskOrders(BaseDomainClass):
|
class TaskOrders(BaseDomainClass):
|
||||||
@ -11,12 +13,15 @@ class TaskOrders(BaseDomainClass):
|
|||||||
resource_name = "task_order"
|
resource_name = "task_order"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, creator, portfolio_id, number, clins, pdf):
|
def create(cls, portfolio_id, number, clins, pdf):
|
||||||
task_order = TaskOrder(
|
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
|
||||||
portfolio_id=portfolio_id, creator=creator, number=number, pdf=pdf
|
|
||||||
)
|
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
|
|
||||||
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
except IntegrityError:
|
||||||
|
db.session.rollback()
|
||||||
|
raise AlreadyExistsError("task_order")
|
||||||
|
|
||||||
TaskOrders.create_clins(task_order.id, clins)
|
TaskOrders.create_clins(task_order.id, clins)
|
||||||
|
|
||||||
@ -37,7 +42,12 @@ class TaskOrders(BaseDomainClass):
|
|||||||
task_order.number = number
|
task_order.number = number
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
|
|
||||||
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
except IntegrityError:
|
||||||
|
db.session.rollback()
|
||||||
|
raise AlreadyExistsError("task_order")
|
||||||
|
|
||||||
return task_order
|
return task_order
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -66,10 +76,12 @@ class TaskOrders(BaseDomainClass):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sort(cls, task_orders: [TaskOrder]) -> [TaskOrder]:
|
def sort_by_status(cls, task_orders):
|
||||||
# Sorts a list of task orders on two keys: status (primary) and time_created (secondary)
|
by_status = {status.value: [] for status in SORT_ORDERING}
|
||||||
by_time_created = sorted(task_orders, key=lambda to: to.time_created)
|
|
||||||
by_status = sorted(by_time_created, key=lambda to: SORT_ORDERING.get(to.status))
|
for task_order in task_orders:
|
||||||
|
by_status[task_order.display_status].append(task_order)
|
||||||
|
|
||||||
return by_status
|
return by_status
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -5,6 +5,7 @@ from flask import render_template
|
|||||||
from jinja2 import contextfilter
|
from jinja2 import contextfilter
|
||||||
from jinja2.exceptions import TemplateNotFound
|
from jinja2.exceptions import TemplateNotFound
|
||||||
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
|
||||||
|
from decimal import DivisionByZero as DivisionByZeroException
|
||||||
|
|
||||||
|
|
||||||
def iconSvg(name):
|
def iconSvg(name):
|
||||||
@ -38,6 +39,14 @@ def usPhone(number):
|
|||||||
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
|
return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:])
|
||||||
|
|
||||||
|
|
||||||
|
def obligatedFundingGraphWidth(values):
|
||||||
|
numerator, denominator = values
|
||||||
|
try:
|
||||||
|
return (numerator / denominator) * 100
|
||||||
|
except DivisionByZeroException:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def formattedDate(value, formatter="%m/%d/%Y"):
|
def formattedDate(value, formatter="%m/%d/%Y"):
|
||||||
if value:
|
if value:
|
||||||
return value.strftime(formatter)
|
return value.strftime(formatter)
|
||||||
@ -76,6 +85,7 @@ def register_filters(app):
|
|||||||
app.jinja_env.filters["pageWindow"] = pageWindow
|
app.jinja_env.filters["pageWindow"] = pageWindow
|
||||||
app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent
|
app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent
|
||||||
app.jinja_env.filters["withExtraParams"] = with_extra_params
|
app.jinja_env.filters["withExtraParams"] = with_extra_params
|
||||||
|
app.jinja_env.filters["obligatedFundingGraphWidth"] = obligatedFundingGraphWidth
|
||||||
|
|
||||||
@contextfilter
|
@contextfilter
|
||||||
def translateWithoutCache(context, *kwargs):
|
def translateWithoutCache(context, *kwargs):
|
||||||
|
@ -3,112 +3,14 @@ from atst.utils.localization import translate
|
|||||||
|
|
||||||
|
|
||||||
SERVICE_BRANCHES = [
|
SERVICE_BRANCHES = [
|
||||||
("", "- Select -"),
|
("air_force", translate("forms.portfolio.defense_component.choices.air_force")),
|
||||||
("Air Force, Department of the", "Air Force, Department of the"),
|
("army", translate("forms.portfolio.defense_component.choices.army")),
|
||||||
("Army and Air Force Exchange Service", "Army and Air Force Exchange Service"),
|
|
||||||
("Army, Department of the", "Army, Department of the"),
|
|
||||||
(
|
(
|
||||||
"Defense Advanced Research Applications Agency",
|
"marine_corps",
|
||||||
"Defense Advanced Research Applications Agency",
|
translate("forms.portfolio.defense_component.choices.marine_corps"),
|
||||||
),
|
),
|
||||||
("Defense Commissary Agency", "Defense Commissary Agency"),
|
("navy", translate("forms.portfolio.defense_component.choices.navy")),
|
||||||
("Defense Contract Audit Agency", "Defense Contract Audit Agency"),
|
("other", translate("forms.portfolio.defense_component.choices.other")),
|
||||||
("Defense Contract Management Agency", "Defense Contract Management Agency"),
|
|
||||||
("Defense Finance & Accounting Service", "Defense Finance & Accounting Service"),
|
|
||||||
("Defense Health Agency", "Defense Health Agency"),
|
|
||||||
("Defense Information System Agency", "Defense Information System Agency"),
|
|
||||||
("Defense Intelligence Agency", "Defense Intelligence Agency"),
|
|
||||||
("Defense Legal Services Agency", "Defense Legal Services Agency"),
|
|
||||||
("Defense Logistics Agency", "Defense Logistics Agency"),
|
|
||||||
("Defense Media Activity", "Defense Media Activity"),
|
|
||||||
("Defense Micro Electronics Activity", "Defense Micro Electronics Activity"),
|
|
||||||
("Defense POW-MIA Accounting Agency", "Defense POW-MIA Accounting Agency"),
|
|
||||||
("Defense Security Cooperation Agency", "Defense Security Cooperation Agency"),
|
|
||||||
("Defense Security Service", "Defense Security Service"),
|
|
||||||
("Defense Technical Information Center", "Defense Technical Information Center"),
|
|
||||||
(
|
|
||||||
"Defense Technology Security Administration",
|
|
||||||
"Defense Technology Security Administration",
|
|
||||||
),
|
|
||||||
("Defense Threat Reduction Agency", "Defense Threat Reduction Agency"),
|
|
||||||
("DoD Education Activity", "DoD Education Activity"),
|
|
||||||
("DoD Human Recourses Activity", "DoD Human Recourses Activity"),
|
|
||||||
("DoD Inspector General", "DoD Inspector General"),
|
|
||||||
("DoD Test Resource Management Center", "DoD Test Resource Management Center"),
|
|
||||||
(
|
|
||||||
"Headquarters Defense Human Resource Activity ",
|
|
||||||
"Headquarters Defense Human Resource Activity ",
|
|
||||||
),
|
|
||||||
("Joint Staff", "Joint Staff"),
|
|
||||||
("Missile Defense Agency", "Missile Defense Agency"),
|
|
||||||
("National Defense University", "National Defense University"),
|
|
||||||
(
|
|
||||||
"National Geospatial Intelligence Agency (NGA)",
|
|
||||||
"National Geospatial Intelligence Agency (NGA)",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"National Oceanic and Atmospheric Administration (NOAA)",
|
|
||||||
"National Oceanic and Atmospheric Administration (NOAA)",
|
|
||||||
),
|
|
||||||
("National Reconnaissance Office", "National Reconnaissance Office"),
|
|
||||||
("National Reconnaissance Office (NRO)", "National Reconnaissance Office (NRO)"),
|
|
||||||
("National Security Agency (NSA)", "National Security Agency (NSA)"),
|
|
||||||
(
|
|
||||||
"National Security Agency-Central Security Service",
|
|
||||||
"National Security Agency-Central Security Service",
|
|
||||||
),
|
|
||||||
("Navy, Department of the", "Navy, Department of the"),
|
|
||||||
("Office of Economic Adjustment", "Office of Economic Adjustment"),
|
|
||||||
("Office of the Secretary of Defense", "Office of the Secretary of Defense"),
|
|
||||||
("Pentagon Force Protection Agency", "Pentagon Force Protection Agency"),
|
|
||||||
(
|
|
||||||
"Uniform Services University of the Health Sciences",
|
|
||||||
"Uniform Services University of the Health Sciences",
|
|
||||||
),
|
|
||||||
("US Cyber Command (USCYBERCOM)", "US Cyber Command (USCYBERCOM)"),
|
|
||||||
(
|
|
||||||
"US Special Operations Command (USSOCOM)",
|
|
||||||
"US Special Operations Command (USSOCOM)",
|
|
||||||
),
|
|
||||||
("US Strategic Command (USSTRATCOM)", "US Strategic Command (USSTRATCOM)"),
|
|
||||||
(
|
|
||||||
"US Transportation Command (USTRANSCOM)",
|
|
||||||
"US Transportation Command (USTRANSCOM)",
|
|
||||||
),
|
|
||||||
("Washington Headquarters Services", "Washington Headquarters Services"),
|
|
||||||
]
|
|
||||||
|
|
||||||
APP_MIGRATION = [
|
|
||||||
("on_premise", translate("forms.task_order.app_migration.on_premise")),
|
|
||||||
("cloud", translate("forms.task_order.app_migration.cloud")),
|
|
||||||
("both", translate("forms.task_order.app_migration.both")),
|
|
||||||
("none", translate("forms.task_order.app_migration.none")),
|
|
||||||
("not_sure", translate("forms.task_order.app_migration.not_sure")),
|
|
||||||
]
|
|
||||||
|
|
||||||
APPLICATION_COMPLEXITY = [
|
|
||||||
("storage", translate("forms.task_order.complexity.storage")),
|
|
||||||
("data_analytics", translate("forms.task_order.complexity.data_analytics")),
|
|
||||||
("conus", translate("forms.task_order.complexity.conus")),
|
|
||||||
("oconus", translate("forms.task_order.complexity.oconus")),
|
|
||||||
("tactical_edge", translate("forms.task_order.complexity.tactical_edge")),
|
|
||||||
("not_sure", translate("forms.task_order.complexity.not_sure")),
|
|
||||||
("other", translate("forms.task_order.complexity.other")),
|
|
||||||
]
|
|
||||||
|
|
||||||
DEV_TEAM = [
|
|
||||||
("civilians", translate("forms.task_order.dev_team.civilians")),
|
|
||||||
("military", translate("forms.task_order.dev_team.military")),
|
|
||||||
("contractor", translate("forms.task_order.dev_team.contractor")),
|
|
||||||
("other", translate("forms.task_order.dev_team.other")),
|
|
||||||
]
|
|
||||||
|
|
||||||
TEAM_EXPERIENCE = [
|
|
||||||
("none", translate("forms.task_order.team_experience.none")),
|
|
||||||
("planned", translate("forms.task_order.team_experience.planned")),
|
|
||||||
("built_1", translate("forms.task_order.team_experience.built_1")),
|
|
||||||
("built_3", translate("forms.task_order.team_experience.built_3")),
|
|
||||||
("built_many", translate("forms.task_order.team_experience.built_many")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ENV_ROLE_NO_ACCESS = "No Access"
|
ENV_ROLE_NO_ACCESS = "No Access"
|
||||||
|
@ -1,33 +1,25 @@
|
|||||||
from wtforms.fields import (
|
from wtforms.fields import (
|
||||||
RadioField,
|
|
||||||
SelectField,
|
|
||||||
SelectMultipleField,
|
SelectMultipleField,
|
||||||
StringField,
|
StringField,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
)
|
)
|
||||||
from wtforms.validators import Length, Optional
|
from wtforms.validators import Length, InputRequired
|
||||||
from wtforms.widgets import ListWidget, CheckboxInput
|
from wtforms.widgets import ListWidget, CheckboxInput
|
||||||
|
|
||||||
from .forms import BaseForm, remove_empty_string
|
from .forms import BaseForm
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
from .data import (
|
from .data import SERVICE_BRANCHES
|
||||||
APPLICATION_COMPLEXITY,
|
|
||||||
APP_MIGRATION,
|
|
||||||
DEV_TEAM,
|
|
||||||
SERVICE_BRANCHES,
|
|
||||||
TEAM_EXPERIENCE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PortfolioForm(BaseForm):
|
class PortfolioForm(BaseForm):
|
||||||
name = StringField(
|
name = StringField(
|
||||||
translate("forms.portfolio.name_label"),
|
translate("forms.portfolio.name.label"),
|
||||||
validators=[
|
validators=[
|
||||||
Length(
|
Length(
|
||||||
min=4,
|
min=4,
|
||||||
max=100,
|
max=100,
|
||||||
message=translate("forms.portfolio.name_length_validation_message"),
|
message=translate("forms.portfolio.name.length_validation_message"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -35,78 +27,25 @@ class PortfolioForm(BaseForm):
|
|||||||
|
|
||||||
class PortfolioCreationForm(BaseForm):
|
class PortfolioCreationForm(BaseForm):
|
||||||
name = StringField(
|
name = StringField(
|
||||||
translate("forms.portfolio.name_label"),
|
translate("forms.portfolio.name.label"),
|
||||||
validators=[
|
validators=[
|
||||||
Length(
|
Length(
|
||||||
min=4,
|
min=4,
|
||||||
max=100,
|
max=100,
|
||||||
message=translate("forms.portfolio.name_length_validation_message"),
|
message=translate("forms.portfolio.name.length_validation_message"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
description = TextAreaField(translate("forms.portfolio.description.label"),)
|
||||||
defense_component = SelectField(
|
defense_component = SelectMultipleField(
|
||||||
translate("forms.task_order.defense_component_label"),
|
|
||||||
choices=SERVICE_BRANCHES,
|
choices=SERVICE_BRANCHES,
|
||||||
default="",
|
|
||||||
filters=[remove_empty_string],
|
|
||||||
)
|
|
||||||
|
|
||||||
description = TextAreaField(
|
|
||||||
translate("forms.task_order.scope_label"),
|
|
||||||
description=translate("forms.task_order.scope_description"),
|
|
||||||
)
|
|
||||||
|
|
||||||
app_migration = RadioField(
|
|
||||||
translate("forms.task_order.app_migration.label"),
|
|
||||||
description=translate("forms.task_order.app_migration.description"),
|
|
||||||
choices=APP_MIGRATION,
|
|
||||||
default="",
|
|
||||||
validators=[Optional()],
|
|
||||||
)
|
|
||||||
|
|
||||||
native_apps = RadioField(
|
|
||||||
translate("forms.task_order.native_apps.label"),
|
|
||||||
description=translate("forms.task_order.native_apps.description"),
|
|
||||||
choices=[("yes", "Yes"), ("no", "No"), ("not_sure", "Not Sure")],
|
|
||||||
default="",
|
|
||||||
validators=[Optional()],
|
|
||||||
)
|
|
||||||
|
|
||||||
complexity = SelectMultipleField(
|
|
||||||
translate("forms.task_order.complexity.label"),
|
|
||||||
description=translate("forms.task_order.complexity.description"),
|
|
||||||
choices=APPLICATION_COMPLEXITY,
|
|
||||||
default=None,
|
|
||||||
widget=ListWidget(prefix_label=False),
|
widget=ListWidget(prefix_label=False),
|
||||||
option_widget=CheckboxInput(),
|
option_widget=CheckboxInput(),
|
||||||
|
validators=[
|
||||||
|
InputRequired(
|
||||||
|
message=translate(
|
||||||
|
"forms.portfolio.defense_component.validation_message"
|
||||||
)
|
)
|
||||||
|
|
||||||
complexity_other = StringField(
|
|
||||||
translate("forms.task_order.complexity_other_label"),
|
|
||||||
default=None,
|
|
||||||
filters=[remove_empty_string],
|
|
||||||
)
|
)
|
||||||
|
],
|
||||||
dev_team = SelectMultipleField(
|
|
||||||
translate("forms.task_order.dev_team.label"),
|
|
||||||
description=translate("forms.task_order.dev_team.description"),
|
|
||||||
choices=DEV_TEAM,
|
|
||||||
default=None,
|
|
||||||
widget=ListWidget(prefix_label=False),
|
|
||||||
option_widget=CheckboxInput(),
|
|
||||||
)
|
|
||||||
|
|
||||||
dev_team_other = StringField(
|
|
||||||
translate("forms.task_order.dev_team_other_label"),
|
|
||||||
default=None,
|
|
||||||
filters=[remove_empty_string],
|
|
||||||
)
|
|
||||||
|
|
||||||
team_experience = RadioField(
|
|
||||||
translate("forms.task_order.team_experience.label"),
|
|
||||||
description=translate("forms.task_order.team_experience.description"),
|
|
||||||
choices=TEAM_EXPERIENCE,
|
|
||||||
default="",
|
|
||||||
validators=[Optional()],
|
|
||||||
)
|
)
|
||||||
|
@ -7,13 +7,13 @@ from wtforms.fields import (
|
|||||||
HiddenField,
|
HiddenField,
|
||||||
)
|
)
|
||||||
from wtforms.fields.html5 import DateField
|
from wtforms.fields.html5 import DateField
|
||||||
from wtforms.validators import Required, Optional, Length, NumberRange, ValidationError
|
from wtforms.validators import Required, Length, NumberRange, ValidationError
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
|
|
||||||
from .data import JEDI_CLIN_TYPES
|
from .data import JEDI_CLIN_TYPES
|
||||||
from .fields import SelectField
|
from .fields import SelectField
|
||||||
from .forms import BaseForm
|
from .forms import BaseForm, remove_empty_string
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
|
||||||
@ -61,9 +61,7 @@ class CLINForm(FlaskForm):
|
|||||||
coerce=coerce_enum,
|
coerce=coerce_enum,
|
||||||
)
|
)
|
||||||
|
|
||||||
number = StringField(
|
number = StringField(label=translate("task_orders.form.clin_number_label"))
|
||||||
label=translate("task_orders.form.clin_number_label"), validators=[Optional()]
|
|
||||||
)
|
|
||||||
start_date = DateField(
|
start_date = DateField(
|
||||||
translate("task_orders.form.pop_start"),
|
translate("task_orders.form.pop_start"),
|
||||||
description=translate("task_orders.form.pop_example"),
|
description=translate("task_orders.form.pop_example"),
|
||||||
@ -136,7 +134,10 @@ class AttachmentForm(BaseForm):
|
|||||||
|
|
||||||
|
|
||||||
class TaskOrderForm(BaseForm):
|
class TaskOrderForm(BaseForm):
|
||||||
number = StringField(label=translate("forms.task_order.number_description"))
|
number = StringField(
|
||||||
|
label=translate("forms.task_order.number_description"),
|
||||||
|
filters=[remove_empty_string],
|
||||||
|
)
|
||||||
pdf = FormField(
|
pdf = FormField(
|
||||||
AttachmentForm,
|
AttachmentForm,
|
||||||
label=translate("task_orders.form.supporting_docs_size_limit"),
|
label=translate("task_orders.form.supporting_docs_size_limit"),
|
||||||
|
@ -12,7 +12,10 @@ class ApplicationInvitation(
|
|||||||
__tablename__ = "application_invitations"
|
__tablename__ = "application_invitations"
|
||||||
|
|
||||||
application_role_id = Column(
|
application_role_id = Column(
|
||||||
UUID(as_uuid=True), ForeignKey("application_roles.id"), index=True
|
UUID(as_uuid=True),
|
||||||
|
ForeignKey("application_roles.id"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
)
|
)
|
||||||
role = relationship(
|
role = relationship(
|
||||||
"ApplicationRole",
|
"ApplicationRole",
|
||||||
|
@ -46,7 +46,9 @@ class ApplicationRole(
|
|||||||
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True
|
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING)
|
status = Column(
|
||||||
|
SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
permission_sets = relationship(
|
permission_sets = relationship(
|
||||||
"PermissionSet", secondary=application_roles_permission_sets
|
"PermissionSet", secondary=application_roles_permission_sets
|
||||||
|
@ -23,12 +23,12 @@ class CLIN(Base, mixins.TimestampsMixin):
|
|||||||
task_order_id = Column(ForeignKey("task_orders.id"), nullable=False)
|
task_order_id = Column(ForeignKey("task_orders.id"), nullable=False)
|
||||||
task_order = relationship("TaskOrder")
|
task_order = relationship("TaskOrder")
|
||||||
|
|
||||||
number = Column(String, nullable=True)
|
number = Column(String, nullable=False)
|
||||||
start_date = Column(Date, nullable=True)
|
start_date = Column(Date, nullable=False)
|
||||||
end_date = Column(Date, nullable=True)
|
end_date = Column(Date, nullable=False)
|
||||||
total_amount = Column(Numeric(scale=2), nullable=True)
|
total_amount = Column(Numeric(scale=2), nullable=False)
|
||||||
obligated_amount = Column(Numeric(scale=2), nullable=True)
|
obligated_amount = Column(Numeric(scale=2), nullable=False)
|
||||||
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=True)
|
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
|
||||||
|
|
||||||
#
|
#
|
||||||
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3
|
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3
|
||||||
@ -65,4 +65,6 @@ class CLIN(Base, mixins.TimestampsMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
return self.start_date <= date.today() <= self.end_date
|
return (
|
||||||
|
self.start_date <= date.today() <= self.end_date
|
||||||
|
) and self.task_order.signed_at
|
||||||
|
@ -43,7 +43,9 @@ class EnvironmentRole(
|
|||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
DISABLED = "disabled"
|
DISABLED = "disabled"
|
||||||
|
|
||||||
status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING)
|
status = Column(
|
||||||
|
SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<EnvironmentRole(role='{}', user='{}', environment='{}', id='{}')>".format(
|
return "<EnvironmentRole(role='{}', user='{}', environment='{}', id='{}')>".format(
|
||||||
|
@ -31,23 +31,29 @@ class InvitesMixin(object):
|
|||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def inviter_id(cls):
|
def inviter_id(cls):
|
||||||
return Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
return Column(
|
||||||
|
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def inviter(cls):
|
def inviter(cls):
|
||||||
return relationship("User", foreign_keys=[cls.inviter_id])
|
return relationship("User", foreign_keys=[cls.inviter_id])
|
||||||
|
|
||||||
status = Column(SQLAEnum(Status, native_enum=False, default=Status.PENDING))
|
status = Column(
|
||||||
|
SQLAEnum(Status, native_enum=False, default=Status.PENDING, nullable=False)
|
||||||
|
)
|
||||||
|
|
||||||
expiration_time = Column(TIMESTAMP(timezone=True))
|
expiration_time = Column(TIMESTAMP(timezone=True), nullable=False)
|
||||||
|
|
||||||
token = Column(String, index=True, default=lambda: secrets.token_urlsafe())
|
token = Column(
|
||||||
|
String, index=True, default=lambda: secrets.token_urlsafe(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
email = Column(String, nullable=False)
|
email = Column(String, nullable=False)
|
||||||
|
|
||||||
dod_id = Column(String)
|
dod_id = Column(String, nullable=False)
|
||||||
first_name = Column(String)
|
first_name = Column(String, nullable=False)
|
||||||
last_name = Column(String)
|
last_name = Column(String, nullable=False)
|
||||||
phone_number = Column(String)
|
phone_number = Column(String)
|
||||||
phone_ext = Column(String)
|
phone_ext = Column(String)
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from sqlalchemy import Column, String
|
from sqlalchemy import Column, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.types import ARRAY
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from atst.models.base import Base
|
from atst.models.base import Base
|
||||||
@ -18,17 +17,11 @@ class Portfolio(
|
|||||||
__tablename__ = "portfolios"
|
__tablename__ = "portfolios"
|
||||||
|
|
||||||
id = types.Id()
|
id = types.Id()
|
||||||
name = Column(String)
|
name = Column(String, nullable=False)
|
||||||
defense_component = Column(String) # Department of Defense Component
|
|
||||||
|
|
||||||
app_migration = Column(String) # App Migration
|
|
||||||
complexity = Column(ARRAY(String)) # Application Complexity
|
|
||||||
complexity_other = Column(String)
|
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
dev_team = Column(ARRAY(String)) # Development Team
|
defense_component = Column(
|
||||||
dev_team_other = Column(String)
|
String, nullable=False
|
||||||
native_apps = Column(String) # Native Apps
|
) # Department of Defense Component
|
||||||
team_experience = Column(String) # Team Experience
|
|
||||||
|
|
||||||
applications = relationship(
|
applications = relationship(
|
||||||
"Application",
|
"Application",
|
||||||
|
@ -12,7 +12,7 @@ class PortfolioInvitation(
|
|||||||
__tablename__ = "portfolio_invitations"
|
__tablename__ = "portfolio_invitations"
|
||||||
|
|
||||||
portfolio_role_id = Column(
|
portfolio_role_id = Column(
|
||||||
UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True
|
UUID(as_uuid=True), ForeignKey("portfolio_roles.id"), index=True, nullable=False
|
||||||
)
|
)
|
||||||
role = relationship(
|
role = relationship(
|
||||||
"PortfolioRole",
|
"PortfolioRole",
|
||||||
|
@ -52,7 +52,9 @@ class PortfolioRole(
|
|||||||
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True
|
UUID(as_uuid=True), ForeignKey("users.id"), index=True, nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
status = Column(SQLAEnum(Status, native_enum=False), default=Status.PENDING)
|
status = Column(
|
||||||
|
SQLAEnum(Status, native_enum=False), default=Status.PENDING, nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
permission_sets = relationship(
|
permission_sets = relationship(
|
||||||
"PermissionSet", secondary=portfolio_roles_permission_sets
|
"PermissionSet", secondary=portfolio_roles_permission_sets
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from datetime import timedelta
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, ForeignKey, String
|
from sqlalchemy import Column, DateTime, ForeignKey, String
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
@ -17,15 +17,16 @@ class Status(Enum):
|
|||||||
ACTIVE = "Active"
|
ACTIVE = "Active"
|
||||||
UPCOMING = "Upcoming"
|
UPCOMING = "Upcoming"
|
||||||
EXPIRED = "Expired"
|
EXPIRED = "Expired"
|
||||||
UNSIGNED = "Not signed"
|
UNSIGNED = "Unsigned"
|
||||||
|
|
||||||
|
|
||||||
SORT_ORDERING = {
|
SORT_ORDERING = [
|
||||||
status: order
|
Status.ACTIVE,
|
||||||
for (order, status) in enumerate(
|
Status.DRAFT,
|
||||||
[Status.DRAFT, Status.ACTIVE, Status.UPCOMING, Status.EXPIRED, Status.UNSIGNED]
|
Status.UPCOMING,
|
||||||
)
|
Status.EXPIRED,
|
||||||
}
|
Status.UNSIGNED,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TaskOrder(Base, mixins.TimestampsMixin):
|
class TaskOrder(Base, mixins.TimestampsMixin):
|
||||||
@ -33,15 +34,12 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
|
|
||||||
id = types.Id()
|
id = types.Id()
|
||||||
|
|
||||||
portfolio_id = Column(ForeignKey("portfolios.id"))
|
portfolio_id = Column(ForeignKey("portfolios.id"), nullable=False)
|
||||||
portfolio = relationship("Portfolio")
|
portfolio = relationship("Portfolio")
|
||||||
|
|
||||||
user_id = Column(ForeignKey("users.id"))
|
|
||||||
creator = relationship("User", foreign_keys="TaskOrder.user_id")
|
|
||||||
|
|
||||||
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
pdf_attachment_id = Column(ForeignKey("attachments.id"))
|
||||||
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
|
||||||
number = Column(String) # Task Order Number
|
number = Column(String, unique=True,) # Task Order Number
|
||||||
signer_dod_id = Column(String)
|
signer_dod_id = Column(String)
|
||||||
signed_at = Column(DateTime)
|
signed_at = Column(DateTime)
|
||||||
|
|
||||||
@ -134,12 +132,11 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def start_date(self):
|
def start_date(self):
|
||||||
return min((c.start_date for c in self.clins), default=self.time_created.date())
|
return min((c.start_date for c in self.clins), default=None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end_date(self):
|
def end_date(self):
|
||||||
default_end_date = self.start_date + timedelta(days=1)
|
return max((c.end_date for c in self.clins), default=None)
|
||||||
return max((c.end_date for c in self.clins), default=default_end_date)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def days_to_expiration(self):
|
def days_to_expiration(self):
|
||||||
@ -173,6 +170,11 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
# Faked for display purposes
|
# Faked for display purposes
|
||||||
return 50
|
return 50
|
||||||
|
|
||||||
|
@property
|
||||||
|
def invoiced_funds(self):
|
||||||
|
# TODO: implement this using reporting data from the CSP
|
||||||
|
return self.total_obligated_funds * Decimal(0.75)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_status(self):
|
def display_status(self):
|
||||||
return self.status.value
|
return self.status.value
|
||||||
|
@ -56,8 +56,8 @@ class User(
|
|||||||
|
|
||||||
email = Column(String)
|
email = Column(String)
|
||||||
dod_id = Column(String, unique=True, nullable=False)
|
dod_id = Column(String, unique=True, nullable=False)
|
||||||
first_name = Column(String)
|
first_name = Column(String, nullable=False)
|
||||||
last_name = Column(String)
|
last_name = Column(String, nullable=False)
|
||||||
phone_number = Column(String)
|
phone_number = Column(String)
|
||||||
phone_ext = Column(String)
|
phone_ext = Column(String)
|
||||||
service_branch = Column(String)
|
service_branch = Column(String)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from flask import render_template
|
from flask import render_template, g
|
||||||
|
|
||||||
from .blueprint import applications_bp
|
from .blueprint import applications_bp
|
||||||
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
||||||
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
|
|
||||||
|
|
||||||
@ -23,4 +24,11 @@ def has_portfolio_applications(_user, portfolio=None, **_kwargs):
|
|||||||
message="view portfolio applications",
|
message="view portfolio applications",
|
||||||
)
|
)
|
||||||
def portfolio_applications(portfolio_id):
|
def portfolio_applications(portfolio_id):
|
||||||
return render_template("applications/index.html")
|
user_env_roles = EnvironmentRoles.for_user(g.current_user.id, portfolio_id)
|
||||||
|
environment_access = {
|
||||||
|
env_role.environment_id: env_role.role for env_role in user_env_roles
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"applications/index.html", environment_access=environment_access
|
||||||
|
)
|
||||||
|
@ -20,6 +20,7 @@ from atst.domain.permission_sets import PermissionSets
|
|||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
from atst.jobs import send_mail
|
from atst.jobs import send_mail
|
||||||
|
from atst.routes.errors import log_error
|
||||||
|
|
||||||
|
|
||||||
def get_environments_obj_for_app(application):
|
def get_environments_obj_for_app(application):
|
||||||
@ -234,7 +235,8 @@ def handle_update_member(application_id, application_role_id, form_data):
|
|||||||
|
|
||||||
flash("application_member_updated", user_name=app_role.user_name)
|
flash("application_member_updated", user_name=app_role.user_name)
|
||||||
|
|
||||||
except GeneralCSPException:
|
except GeneralCSPException as exc:
|
||||||
|
log_error(exc)
|
||||||
flash(
|
flash(
|
||||||
"application_member_update_error", user_name=app_role.user_name,
|
"application_member_update_error", user_name=app_role.user_name,
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from datetime import date, datetime, timedelta
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import redirect, render_template, url_for, request as http_request, g
|
from flask import redirect, render_template, url_for, request as http_request, g
|
||||||
|
|
||||||
@ -11,21 +11,10 @@ from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
|||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios")
|
|
||||||
def portfolios():
|
|
||||||
portfolios = Portfolios.for_user(g.current_user)
|
|
||||||
|
|
||||||
if portfolios:
|
|
||||||
return render_template("portfolios/index.html", page=5, portfolios=portfolios)
|
|
||||||
else:
|
|
||||||
return render_template("portfolios/blank_slate.html")
|
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios/new")
|
@portfolios_bp.route("/portfolios/new")
|
||||||
def new_portfolio():
|
def new_portfolio_step_1():
|
||||||
form = PortfolioCreationForm()
|
form = PortfolioCreationForm()
|
||||||
|
return render_template("portfolios/new/step_1.html", form=form)
|
||||||
return render_template("portfolios/new.html", form=form)
|
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios", methods=["POST"])
|
@portfolios_bp.route("/portfolios", methods=["POST"])
|
||||||
@ -38,16 +27,19 @@ def create_portfolio():
|
|||||||
url_for("applications.portfolio_applications", portfolio_id=portfolio.id)
|
url_for("applications.portfolio_applications", portfolio_id=portfolio.id)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return render_template("portfolios/new.html", form=form), 400
|
return render_template("portfolios/new/step_1.html", form=form), 400
|
||||||
|
|
||||||
|
|
||||||
@portfolios_bp.route("/portfolios/<portfolio_id>/reports")
|
@portfolios_bp.route("/portfolios/<portfolio_id>/reports")
|
||||||
@user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports")
|
@user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports")
|
||||||
def reports(portfolio_id):
|
def reports(portfolio_id):
|
||||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||||
today = date.today()
|
|
||||||
current_month = date(int(today.year), int(today.month), 15)
|
current_obligated_funds = Reports.obligated_funds_by_JEDI_clin(portfolio)
|
||||||
prev_month = current_month - timedelta(days=28)
|
|
||||||
|
if any(map(lambda clin: clin["remaining"] < 0, current_obligated_funds)):
|
||||||
|
flash("insufficient_funds")
|
||||||
|
|
||||||
# wrapped in str() because the sum of obligated funds returns a Decimal object
|
# wrapped in str() because the sum of obligated funds returns a Decimal object
|
||||||
total_portfolio_value = str(
|
total_portfolio_value = str(
|
||||||
sum(
|
sum(
|
||||||
@ -59,12 +51,10 @@ def reports(portfolio_id):
|
|||||||
"portfolios/reports/index.html",
|
"portfolios/reports/index.html",
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
total_portfolio_value=total_portfolio_value,
|
total_portfolio_value=total_portfolio_value,
|
||||||
current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio),
|
current_obligated_funds=current_obligated_funds,
|
||||||
expired_task_orders=Reports.expired_task_orders(portfolio),
|
expired_task_orders=Reports.expired_task_orders(portfolio),
|
||||||
monthly_totals=Reports.monthly_totals(portfolio),
|
monthly_spending=Reports.monthly_spending(portfolio),
|
||||||
current_month=current_month,
|
retrieved=datetime.now(), # mocked datetime of reporting data retrival
|
||||||
prev_month=prev_month,
|
|
||||||
now=datetime.now(), # mocked datetime of reporting data retrival
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ from atst.domain.portfolios import Portfolios
|
|||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.forms.task_order import SignatureForm
|
from atst.forms.task_order import SignatureForm
|
||||||
from atst.models import Permissions
|
from atst.models import Permissions
|
||||||
from atst.models.task_order import Status as TaskOrderStatus
|
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_orders/<task_order_id>/review")
|
@task_orders_bp.route("/task_orders/<task_order_id>/review")
|
||||||
@ -28,14 +27,9 @@ def review_task_order(task_order_id):
|
|||||||
@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding")
|
@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding")
|
||||||
def portfolio_funding(portfolio_id):
|
def portfolio_funding(portfolio_id):
|
||||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||||
task_orders = TaskOrders.sort(portfolio.task_orders)
|
task_orders = TaskOrders.sort_by_status(portfolio.task_orders)
|
||||||
label_colors = {
|
to_count = len(portfolio.task_orders)
|
||||||
TaskOrderStatus.DRAFT: "warning",
|
# TODO: Get expended amount from the CSP
|
||||||
TaskOrderStatus.ACTIVE: "success",
|
|
||||||
TaskOrderStatus.UPCOMING: "info",
|
|
||||||
TaskOrderStatus.EXPIRED: "error",
|
|
||||||
TaskOrderStatus.UNSIGNED: "purple",
|
|
||||||
}
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"task_orders/index.html", task_orders=task_orders, label_colors=label_colors
|
"task_orders/index.html", task_orders=task_orders, to_count=to_count
|
||||||
)
|
)
|
||||||
|
@ -10,7 +10,7 @@ from flask import (
|
|||||||
|
|
||||||
from .blueprint import task_orders_bp
|
from .blueprint import task_orders_bp
|
||||||
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
||||||
from atst.domain.exceptions import NoAccessError
|
from atst.domain.exceptions import NoAccessError, AlreadyExistsError
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
from atst.forms.task_order import TaskOrderForm, SignatureForm
|
from atst.forms.task_order import TaskOrderForm, SignatureForm
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
@ -50,7 +50,26 @@ def render_task_orders_edit(
|
|||||||
return render_template(template, **render_args)
|
return render_template(template, **render_args)
|
||||||
|
|
||||||
|
|
||||||
def update_task_order(
|
def update_task_order(form, portfolio_id=None, task_order_id=None, flash_invalid=True):
|
||||||
|
if form.validate(flash_invalid=flash_invalid):
|
||||||
|
task_order = None
|
||||||
|
try:
|
||||||
|
if task_order_id:
|
||||||
|
task_order = TaskOrders.update(task_order_id, **form.data)
|
||||||
|
portfolio_id = task_order.portfolio_id
|
||||||
|
else:
|
||||||
|
task_order = TaskOrders.create(portfolio_id, **form.data)
|
||||||
|
|
||||||
|
return task_order
|
||||||
|
|
||||||
|
except AlreadyExistsError:
|
||||||
|
flash("task_order_number_error", to_number=form.data["number"])
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_and_render_next(
|
||||||
form_data, next_page, current_template, portfolio_id=None, task_order_id=None
|
form_data, next_page, current_template, portfolio_id=None, task_order_id=None
|
||||||
):
|
):
|
||||||
form = None
|
form = None
|
||||||
@ -60,14 +79,8 @@ def update_task_order(
|
|||||||
else:
|
else:
|
||||||
form = TaskOrderForm(form_data)
|
form = TaskOrderForm(form_data)
|
||||||
|
|
||||||
if form.validate():
|
task_order = update_task_order(form, portfolio_id, task_order_id)
|
||||||
task_order = None
|
if task_order:
|
||||||
if task_order_id:
|
|
||||||
task_order = TaskOrders.update(task_order_id, **form.data)
|
|
||||||
portfolio_id = task_order.portfolio_id
|
|
||||||
else:
|
|
||||||
task_order = TaskOrders.create(g.current_user, portfolio_id, **form.data)
|
|
||||||
|
|
||||||
return redirect(url_for(next_page, task_order_id=task_order.id))
|
return redirect(url_for(next_page, task_order_id=task_order.id))
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
@ -149,7 +162,7 @@ def submit_form_step_one_add_pdf(portfolio_id=None, task_order_id=None):
|
|||||||
next_page = "task_orders.form_step_two_add_number"
|
next_page = "task_orders.form_step_two_add_number"
|
||||||
current_template = "task_orders/step_1.html"
|
current_template = "task_orders/step_1.html"
|
||||||
|
|
||||||
return update_task_order(
|
return update_and_render_next(
|
||||||
form_data,
|
form_data,
|
||||||
next_page,
|
next_page,
|
||||||
current_template,
|
current_template,
|
||||||
@ -176,14 +189,8 @@ def cancel_edit(task_order_id=None, portfolio_id=None):
|
|||||||
else:
|
else:
|
||||||
form = TaskOrderForm(form_data)
|
form = TaskOrderForm(form_data)
|
||||||
|
|
||||||
if form.validate(flash_invalid=False):
|
update_task_order(form, portfolio_id, task_order_id, flash_invalid=False)
|
||||||
task_order = None
|
|
||||||
if task_order_id:
|
|
||||||
task_order = TaskOrders.update(task_order_id, **form.data)
|
|
||||||
else:
|
|
||||||
task_order = TaskOrders.create(
|
|
||||||
g.current_user, portfolio_id, **form.data
|
|
||||||
)
|
|
||||||
elif not save and task_order_id:
|
elif not save and task_order_id:
|
||||||
TaskOrders.delete(task_order_id)
|
TaskOrders.delete(task_order_id)
|
||||||
|
|
||||||
@ -207,7 +214,7 @@ def submit_form_step_two_add_number(task_order_id):
|
|||||||
next_page = "task_orders.form_step_three_add_clins"
|
next_page = "task_orders.form_step_three_add_clins"
|
||||||
current_template = "task_orders/step_2.html"
|
current_template = "task_orders/step_2.html"
|
||||||
|
|
||||||
return update_task_order(
|
return update_and_render_next(
|
||||||
form_data, next_page, current_template, task_order_id=task_order_id
|
form_data, next_page, current_template, task_order_id=task_order_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -227,7 +234,7 @@ def submit_form_step_three_add_clins(task_order_id):
|
|||||||
next_page = "task_orders.form_step_four_review"
|
next_page = "task_orders.form_step_four_review"
|
||||||
current_template = "task_orders/step_3.html"
|
current_template = "task_orders/step_3.html"
|
||||||
|
|
||||||
return update_task_order(
|
return update_and_render_next(
|
||||||
form_data, next_page, current_template, task_order_id=task_order_id
|
form_data, next_page, current_template, task_order_id=task_order_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -96,6 +96,11 @@ MESSAGES = {
|
|||||||
"message_template": "<p>Please see below.</p>",
|
"message_template": "<p>Please see below.</p>",
|
||||||
"category": "error",
|
"category": "error",
|
||||||
},
|
},
|
||||||
|
"insufficient_funds": {
|
||||||
|
"title_template": "Insufficient Funds",
|
||||||
|
"message_template": "",
|
||||||
|
"category": "warning",
|
||||||
|
},
|
||||||
"logged_out": {
|
"logged_out": {
|
||||||
"title_template": translate("flash.logged_out"),
|
"title_template": translate("flash.logged_out"),
|
||||||
"message_template": """
|
"message_template": """
|
||||||
@ -160,6 +165,11 @@ MESSAGES = {
|
|||||||
"message_template": translate("task_orders.form.draft_alert_message"),
|
"message_template": translate("task_orders.form.draft_alert_message"),
|
||||||
"category": "warning",
|
"category": "warning",
|
||||||
},
|
},
|
||||||
|
"task_order_number_error": {
|
||||||
|
"title_template": "",
|
||||||
|
"message_template": """{{ 'flash.task_order_number_error.message' | translate({ 'to_number': to_number }) }}""",
|
||||||
|
"category": "error",
|
||||||
|
},
|
||||||
"task_order_submitted": {
|
"task_order_submitted": {
|
||||||
"title_template": "Your Task Order has been uploaded successfully.",
|
"title_template": "Your Task Order has been uploaded successfully.",
|
||||||
"message_template": """
|
"message_template": """
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
[default]
|
[default]
|
||||||
ASSETS_URL
|
ASSETS_URL
|
||||||
|
AZURE_ACCOUNT_NAME
|
||||||
|
AZURE_STORAGE_KEY
|
||||||
|
AZURE_TO_BUCKET_NAME
|
||||||
BLOB_STORAGE_URL=http://localhost:8000/
|
BLOB_STORAGE_URL=http://localhost:8000/
|
||||||
CAC_URL = http://localhost:8000/login-redirect
|
CAC_URL = http://localhost:8000/login-redirect
|
||||||
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
||||||
@ -15,6 +18,11 @@ DISABLE_CRL_CHECK = false
|
|||||||
ENVIRONMENT = dev
|
ENVIRONMENT = dev
|
||||||
LIMIT_CONCURRENT_SESSIONS = false
|
LIMIT_CONCURRENT_SESSIONS = false
|
||||||
LOG_JSON = false
|
LOG_JSON = false
|
||||||
|
MAIL_PASSWORD
|
||||||
|
MAIL_PORT
|
||||||
|
MAIL_SENDER
|
||||||
|
MAIL_SERVER
|
||||||
|
MAIL_TLS
|
||||||
PERMANENT_SESSION_LIFETIME = 1800
|
PERMANENT_SESSION_LIFETIME = 1800
|
||||||
PGDATABASE = atat
|
PGDATABASE = atat
|
||||||
PGHOST = localhost
|
PGHOST = localhost
|
||||||
@ -24,7 +32,10 @@ PGSSLMODE = prefer
|
|||||||
PGSSLROOTCERT
|
PGSSLROOTCERT
|
||||||
PGUSER = postgres
|
PGUSER = postgres
|
||||||
PORT=8000
|
PORT=8000
|
||||||
REDIS_URI = redis://localhost:6379
|
REDIS_HOST=localhost:6379
|
||||||
|
REDIS_PASSWORD
|
||||||
|
REDIS_TLS=False
|
||||||
|
REDIS_USER
|
||||||
SECRET_KEY = change_me_into_something_secret
|
SECRET_KEY = change_me_into_something_secret
|
||||||
SERVER_NAME
|
SERVER_NAME
|
||||||
SESSION_COOKIE_NAME=atat
|
SESSION_COOKIE_NAME=atat
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
[default]
|
[default]
|
||||||
DEBUG = true
|
|
||||||
PGHOST = postgreshost
|
|
||||||
PGDATABASE = atat_test
|
|
||||||
REDIS_URI = redis://redishost:6379
|
|
||||||
CRL_STORAGE_CONTAINER = tests/fixtures/crl
|
CRL_STORAGE_CONTAINER = tests/fixtures/crl
|
||||||
WTF_CSRF_ENABLED = false
|
|
||||||
CSP=mock-test
|
CSP=mock-test
|
||||||
|
DEBUG = true
|
||||||
|
PGDATABASE = atat_test
|
||||||
|
WTF_CSRF_ENABLED = false
|
||||||
|
101
deploy/README.md
101
deploy/README.md
@ -14,6 +14,7 @@ The production configuration (azure.atat.code.mil, currently) is reflected in th
|
|||||||
- AUTH_DOMAIN: The host domain for the authentication endpoint for the environment.
|
- AUTH_DOMAIN: The host domain for the authentication endpoint for the environment.
|
||||||
- KV_MI_ID: the fully qualified id (path) of the managed identity for the key vault (instructions on retrieving this are down in section on [Setting up FlexVol](#configuring-the-identity)). Example: /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/RESOURCE_GROUP_NAME/providers/Microsoft.ManagedIdentity/userAssignedIdentities/MANAGED_IDENTITY_NAME
|
- KV_MI_ID: the fully qualified id (path) of the managed identity for the key vault (instructions on retrieving this are down in section on [Setting up FlexVol](#configuring-the-identity)). Example: /subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/RESOURCE_GROUP_NAME/providers/Microsoft.ManagedIdentity/userAssignedIdentities/MANAGED_IDENTITY_NAME
|
||||||
- KV_MI_CLIENT_ID: The client id of the managed identity for the key vault. This is a GUID.
|
- KV_MI_CLIENT_ID: The client id of the managed identity for the key vault. This is a GUID.
|
||||||
|
- TENANT_ID: The id of the active directory tenant in which the cluster and it's associated users exist. This is a GUID.
|
||||||
|
|
||||||
We use envsubst to substitute values for these variables. There is a wrapper script (script/k8s_config) that will output the compiled configuration, using a combination of kustomize and envsubst.
|
We use envsubst to substitute values for these variables. There is a wrapper script (script/k8s_config) that will output the compiled configuration, using a combination of kustomize and envsubst.
|
||||||
|
|
||||||
@ -36,35 +37,6 @@ If you are satisfied with the output from the diff, you can apply the new config
|
|||||||
|
|
||||||
## Secrets and Configuration
|
## Secrets and Configuration
|
||||||
|
|
||||||
### atst-overrides.ini
|
|
||||||
|
|
||||||
Production configuration values are provided to the ATAT Flask app by writing an `atst-overrides.ini` file to the running Docker container. This file is stored as a Kubernetes secret. It contains configuration information for the database connection, mailer, etc.
|
|
||||||
|
|
||||||
To update the configuration, you can do the following:
|
|
||||||
|
|
||||||
```
|
|
||||||
kubectl -n atat get secret atst-config-ini -o=jsonpath='{.data.override\.ini}' | base64 --decode > override.ini
|
|
||||||
```
|
|
||||||
|
|
||||||
This base64 decodes the secret and writes it to a local file called `override.ini`. Make any necessary config changes to that file.
|
|
||||||
|
|
||||||
To apply the new config, first delete the existing copy of the secret:
|
|
||||||
|
|
||||||
```
|
|
||||||
kubectl -n atat delete secret atst-config-ini
|
|
||||||
```
|
|
||||||
|
|
||||||
Then create a new copy of the secret from your updated copy:
|
|
||||||
|
|
||||||
```
|
|
||||||
kubectl -n atat create secret generic atst-config-ini --from-file=./override.ini
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Be careful not to check the override.ini file into source control.
|
|
||||||
- Be careful not to overwrite one CSP cluster's config with the other's. This will break everything.
|
|
||||||
|
|
||||||
### nginx-htpasswd
|
### nginx-htpasswd
|
||||||
|
|
||||||
If the site is running in dev mode, the `/login-dev` endpoint is available. This endpoint is protected by basic HTTP auth. To create a new password file, run:
|
If the site is running in dev mode, the `/login-dev` endpoint is available. This endpoint is protected by basic HTTP auth. To create a new password file, run:
|
||||||
@ -169,13 +141,40 @@ Then:
|
|||||||
kubectl -n atat create secret tls azure-atat-code-mil-tls --key="[path to the private key]" --cert="[path to the full chain]"
|
kubectl -n atat create secret tls azure-atat-code-mil-tls --key="[path to the private key]" --cert="[path to the full chain]"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Create the Diffie-Hellman parameters
|
||||||
|
|
||||||
|
Diffie-Hellman parameters allow per-session encryption of SSL traffic to help improve security. We currently store our parameters in KeyVault, the value can be updated using the following command. Note: Generating the new paramter can take over 10 minutes and there won't be any output while it's running.
|
||||||
|
```
|
||||||
|
az keyvault secret set --vault-name <VAULT NAME> --name <NAME OF PARAM> --value "$(openssl genpkey -genparam -algorithm DH -outform pem -pkeyopt dh_paramgen_prime_len:4096 2> /dev/null)"
|
||||||
|
```
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Secrets Management
|
||||||
|
|
||||||
|
Secrets, keys, and certificates are managed from Azure Key Vault. These items are mounted into the containers at runtime using the FlexVol implementation described below.
|
||||||
|
|
||||||
|
The following are mounted into the NGINX container in the atst pod:
|
||||||
|
|
||||||
|
- The TLS certs for the site
|
||||||
|
- The DH parameter for TLS connections
|
||||||
|
|
||||||
|
These are mounted into every instance of the Flask application container (the atst container, the celery worker, etc.):
|
||||||
|
|
||||||
|
- The Azure storage key used to access blob storage (AZURE_STORAGE_KEY)
|
||||||
|
- The password for the SMTP server used to send mail (MAIL_PASSWORD)
|
||||||
|
- The Postgres database user password (PGPASSWORD)
|
||||||
|
- The Redis user password (REDIS_PASSWORD)
|
||||||
|
- The Flask secret key used for session signing and generating CSRF tokens (SECRET_KEY)
|
||||||
|
|
||||||
|
Secrets should be added to Key Vault with the following naming pattern: [branch/environment]-[all-caps config setting name]. Note that Key Vault does not support underscores. Substitute hyphens. For example, the config setting for the SMTP server password is MAIL_SERVER. The corresponding secret name in Key Vault is "master-MAIL-SERVER" for the credential used in the primary environment.These secrets are mounted into the containers via FlexVol.
|
||||||
|
|
||||||
|
To add or manage secrets, keys, and certificates in Key Vault, see the [documentation](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-cli).
|
||||||
|
|
||||||
# Setting Up FlexVol for Secrets
|
# Setting Up FlexVol for Secrets
|
||||||
|
|
||||||
## Preparing Azure Environment
|
## Preparing Azure Environment
|
||||||
|
|
||||||
A Key Vault will need to be created. Save it's full id (the full path) for use later.
|
A Key Vault will need to be created. Save its full id (the full path) for use later.
|
||||||
|
|
||||||
## Preparing Cluster
|
## Preparing Cluster
|
||||||
|
|
||||||
@ -217,3 +216,45 @@ Example values:
|
|||||||
|
|
||||||
5. The file `deploy/azure/aadpodidentity.yml` is templated via Kustomize, so you'll need to include clientId (as `KV_MI_CLIENT_ID`) and id (as `KV_MI_ID`) of the managed identity as part of the call to Kustomize.
|
5. The file `deploy/azure/aadpodidentity.yml` is templated via Kustomize, so you'll need to include clientId (as `KV_MI_CLIENT_ID`) and id (as `KV_MI_ID`) of the managed identity as part of the call to Kustomize.
|
||||||
|
|
||||||
|
## Using the FlexVol
|
||||||
|
|
||||||
|
There are 3 steps to using the FlexVol to access secrets from KeyVault
|
||||||
|
|
||||||
|
1. For the resource in which you would like to mount a FlexVol, add a metadata label with the selector from `aadpodidentity.yml`
|
||||||
|
```
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: atst
|
||||||
|
role: web
|
||||||
|
aadpodidbinding: atat-kv-id-binding
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register the FlexVol as a mount and specifiy which secrets you want to mount, along with the file name they should have. The `keyvaultobjectnames`, `keyvaultobjectaliases`, and `keyvaultobjecttypes` correspond to one another, positionally. They are passed as semicolon delimited strings, examples below.
|
||||||
|
|
||||||
|
```
|
||||||
|
- name: volume-of-secrets
|
||||||
|
flexVolume:
|
||||||
|
driver: "azure/kv"
|
||||||
|
options:
|
||||||
|
usepodidentity: "true"
|
||||||
|
keyvaultname: "<NAME OF KEY VAULT>"
|
||||||
|
keyvaultobjectnames: "mysecret;mykey;mycert"
|
||||||
|
keyvaultobjectaliases: "mysecret.pem;mykey.txt;mycert.crt"
|
||||||
|
keyvaultobjecttypes: "secret;key;cert"
|
||||||
|
tenantid: $TENANT_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Tell the resource where to mount your new volume, using the same name that you specified for the volume above.
|
||||||
|
```
|
||||||
|
- name: nginx-secret
|
||||||
|
mountPath: "/usr/secrets/"
|
||||||
|
readOnly: true
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Once applied, the directory specified in the `mountPath` argument will contain the files you specified in the flexVolume. In our case, you would be able to do this:
|
||||||
|
```
|
||||||
|
$ kubectl exec -it CONTAINER_NAME -c atst ls /usr/secrets
|
||||||
|
mycert.crt
|
||||||
|
mykey.txt
|
||||||
|
mysecret.pem
|
||||||
|
```
|
||||||
|
@ -6,15 +6,30 @@ metadata:
|
|||||||
namespace: atat
|
namespace: atat
|
||||||
data:
|
data:
|
||||||
ASSETS_URL: https://atat-cdn.azureedge.net/
|
ASSETS_URL: https://atat-cdn.azureedge.net/
|
||||||
|
AZURE_ACCOUNT_NAME: atat
|
||||||
|
AZURE_TO_BUCKET_NAME: task-order-pdfs
|
||||||
BLOB_STORAGE_URL: https://atat.blob.core.windows.net/
|
BLOB_STORAGE_URL: https://atat.blob.core.windows.net/
|
||||||
CELERY_DEFAULT_QUEUE: celery-master
|
CAC_URL: https://auth-staging.atat.code.mil/login-redirect
|
||||||
CDN_ORIGIN: https://azure.atat.code.mil
|
CDN_ORIGIN: https://azure.atat.code.mil
|
||||||
|
CELERY_DEFAULT_QUEUE: celery-master
|
||||||
CSP: azure
|
CSP: azure
|
||||||
|
DEBUG: "0"
|
||||||
FLASK_ENV: master
|
FLASK_ENV: master
|
||||||
LOG_JSON: "true"
|
LOG_JSON: "true"
|
||||||
OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini
|
MAIL_PORT: "587"
|
||||||
|
MAIL_SENDER: postmaster@atat.code.mil
|
||||||
|
MAIL_SERVER: smtp.mailgun.org
|
||||||
|
MAIL_TLS: "true"
|
||||||
|
OVERRIDE_CONFIG_DIRECTORY: /config
|
||||||
|
PGAPPNAME: atst
|
||||||
|
PGDATABASE: staging
|
||||||
|
PGHOST: atat-db.postgres.database.azure.com
|
||||||
|
PGPORT: "5432"
|
||||||
PGSSLMODE: verify-full
|
PGSSLMODE: verify-full
|
||||||
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
|
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
|
||||||
|
PGUSER: atat_master@atat-db
|
||||||
|
REDIS_HOST: atat.redis.cache.windows.net:6380
|
||||||
|
REDIS_TLS: "true"
|
||||||
STATIC_URL: https://atat-cdn.azureedge.net/static/
|
STATIC_URL: https://atat-cdn.azureedge.net/static/
|
||||||
TZ: UTC
|
TZ: UTC
|
||||||
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
||||||
|
@ -5,8 +5,10 @@ metadata:
|
|||||||
name: atst-nginx
|
name: atst-nginx
|
||||||
namespace: atat
|
namespace: atat
|
||||||
data:
|
data:
|
||||||
nginx-config: |-
|
atst.conf: |-
|
||||||
server {
|
server {
|
||||||
|
access_log /var/log/nginx/access.log json;
|
||||||
|
|
||||||
listen ${PORT_PREFIX}342;
|
listen ${PORT_PREFIX}342;
|
||||||
server_name ${MAIN_DOMAIN};
|
server_name ${MAIN_DOMAIN};
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
@ -18,6 +20,8 @@ data:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
|
access_log /var/log/nginx/access.log json;
|
||||||
|
|
||||||
listen ${PORT_PREFIX}343;
|
listen ${PORT_PREFIX}343;
|
||||||
server_name ${AUTH_DOMAIN};
|
server_name ${AUTH_DOMAIN};
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
@ -29,12 +33,17 @@ data:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
|
access_log /var/log/nginx/access.log json;
|
||||||
|
|
||||||
server_name ${MAIN_DOMAIN};
|
server_name ${MAIN_DOMAIN};
|
||||||
# access_log /var/log/nginx/access.log json;
|
# access_log /var/log/nginx/access.log json;
|
||||||
listen ${PORT_PREFIX}442 ssl;
|
listen ${PORT_PREFIX}442 ssl;
|
||||||
listen [::]:${PORT_PREFIX}442 ssl ipv6only=on;
|
listen [::]:${PORT_PREFIX}442 ssl ipv6only=on;
|
||||||
ssl_certificate /etc/ssl/private/atat.crt;
|
ssl_certificate /etc/ssl/atat.crt;
|
||||||
ssl_certificate_key /etc/ssl/private/atat.key;
|
ssl_certificate_key /etc/ssl/atat.key;
|
||||||
|
# additional SSL/TLS settings
|
||||||
|
include /etc/nginx/snippets/ssl.conf;
|
||||||
|
|
||||||
location /login-redirect {
|
location /login-redirect {
|
||||||
return 301 https://auth-azure.atat.code.mil$request_uri;
|
return 301 https://auth-azure.atat.code.mil$request_uri;
|
||||||
}
|
}
|
||||||
@ -58,18 +67,20 @@ data:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
# access_log /var/log/nginx/access.log json;
|
access_log /var/log/nginx/access.log json;
|
||||||
|
|
||||||
server_name ${AUTH_DOMAIN};
|
server_name ${AUTH_DOMAIN};
|
||||||
listen ${PORT_PREFIX}443 ssl;
|
listen ${PORT_PREFIX}443 ssl;
|
||||||
listen [::]:${PORT_PREFIX}443 ssl ipv6only=on;
|
listen [::]:${PORT_PREFIX}443 ssl ipv6only=on;
|
||||||
ssl_certificate /etc/ssl/private/atat.crt;
|
ssl_certificate /etc/ssl/atat.crt;
|
||||||
ssl_certificate_key /etc/ssl/private/atat.key;
|
ssl_certificate_key /etc/ssl/atat.key;
|
||||||
# Request and validate client certificate
|
# Request and validate client certificate
|
||||||
ssl_verify_client on;
|
ssl_verify_client on;
|
||||||
ssl_verify_depth 10;
|
ssl_verify_depth 10;
|
||||||
ssl_client_certificate /etc/ssl/client-ca-bundle.pem;
|
ssl_client_certificate /etc/ssl/client-ca-bundle.pem;
|
||||||
# Guard against HTTPS -> HTTP downgrade
|
# additional SSL/TLS settings
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
include /etc/nginx/snippets/ssl.conf;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
return 301 https://azure.atat.code.mil$request_uri;
|
return 301 https://azure.atat.code.mil$request_uri;
|
||||||
}
|
}
|
||||||
@ -88,3 +99,18 @@ data:
|
|||||||
uwsgi_param HTTP_X_REQUEST_ID $request_id;
|
uwsgi_param HTTP_X_REQUEST_ID $request_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
00json_log.conf: |-
|
||||||
|
log_format json escape=json
|
||||||
|
'{'
|
||||||
|
'"timestamp":"$time_iso8601",'
|
||||||
|
'"msec":"$msec",'
|
||||||
|
'"request_id":"$request_id",'
|
||||||
|
'"remote_addr":"$remote_addr",'
|
||||||
|
'"remote_user":"$remote_user",'
|
||||||
|
'"request":"$request",'
|
||||||
|
'"status":$status,'
|
||||||
|
'"body_bytes_sent":$body_bytes_sent,'
|
||||||
|
'"referer":"$http_referer",'
|
||||||
|
'"user_agent":"$http_user_agent",'
|
||||||
|
'"http_x_forwarded_for":"$http_x_forwarded_for"'
|
||||||
|
'}';
|
||||||
|
@ -5,9 +5,25 @@ metadata:
|
|||||||
name: atst-worker-envvars
|
name: atst-worker-envvars
|
||||||
namespace: atat
|
namespace: atat
|
||||||
data:
|
data:
|
||||||
|
AZURE_ACCOUNT_NAME: atat
|
||||||
|
AZURE_TO_BUCKET_NAME: task-order-pdfs
|
||||||
|
CAC_URL: https://auth-staging.atat.code.mil/login-redirect
|
||||||
CELERY_DEFAULT_QUEUE: celery-master
|
CELERY_DEFAULT_QUEUE: celery-master
|
||||||
DISABLE_CRL_CHECK: "True"
|
DEBUG: "0"
|
||||||
|
DISABLE_CRL_CHECK: "true"
|
||||||
|
MAIL_PORT: "587"
|
||||||
|
MAIL_SENDER: postmaster@atat.code.mil
|
||||||
|
MAIL_SERVER: smtp.mailgun.org
|
||||||
|
MAIL_TLS: "true"
|
||||||
|
OVERRIDE_CONFIG_DIRECTORY: /config
|
||||||
|
PGAPPNAME: atst
|
||||||
|
PGDATABASE: staging
|
||||||
|
PGHOST: atat-db.postgres.database.azure.com
|
||||||
|
PGPORT: "5432"
|
||||||
PGSSLMODE: verify-full
|
PGSSLMODE: verify-full
|
||||||
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
|
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
|
||||||
|
PGUSER: atat_master@atat-db
|
||||||
|
REDIS_HOST: atat.redis.cache.windows.net:6380
|
||||||
|
REDIS_TLS: "true"
|
||||||
SERVER_NAME: azure.atat.code.mil
|
SERVER_NAME: azure.atat.code.mil
|
||||||
TZ: UTC
|
TZ: UTC
|
||||||
|
@ -23,6 +23,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: atst
|
app: atst
|
||||||
role: web
|
role: web
|
||||||
|
aadpodidbinding: atat-kv-id-binding
|
||||||
spec:
|
spec:
|
||||||
securityContext:
|
securityContext:
|
||||||
fsGroup: 101
|
fsGroup: 101
|
||||||
@ -33,9 +34,6 @@ spec:
|
|||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: atst-envvars
|
name: atst-envvars
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: atst-config
|
|
||||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
|
||||||
subPath: atst-overrides.ini
|
|
||||||
- name: nginx-client-ca-bundle
|
- name: nginx-client-ca-bundle
|
||||||
mountPath: "/opt/atat/atst/ssl/server-certs/ca-chain.pem"
|
mountPath: "/opt/atat/atst/ssl/server-certs/ca-chain.pem"
|
||||||
subPath: client-ca-bundle.pem
|
subPath: client-ca-bundle.pem
|
||||||
@ -49,6 +47,8 @@ spec:
|
|||||||
- name: uwsgi-config
|
- name: uwsgi-config
|
||||||
mountPath: "/opt/atat/atst/uwsgi.ini"
|
mountPath: "/opt/atat/atst/uwsgi.ini"
|
||||||
subPath: uwsgi.ini
|
subPath: uwsgi.ini
|
||||||
|
- name: flask-secret
|
||||||
|
mountPath: "/config"
|
||||||
- name: nginx
|
- name: nginx
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
ports:
|
ports:
|
||||||
@ -62,37 +62,32 @@ spec:
|
|||||||
name: auth
|
name: auth
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: nginx-config
|
- name: nginx-config
|
||||||
mountPath: "/etc/nginx/conf.d/atst.conf"
|
mountPath: "/etc/nginx/conf.d/"
|
||||||
subPath: atst.conf
|
|
||||||
- name: uwsgi-socket-dir
|
- name: uwsgi-socket-dir
|
||||||
mountPath: "/var/run/uwsgi"
|
mountPath: "/var/run/uwsgi"
|
||||||
- name: nginx-htpasswd
|
- name: nginx-htpasswd
|
||||||
mountPath: "/etc/nginx/.htpasswd"
|
mountPath: "/etc/nginx/.htpasswd"
|
||||||
subPath: .htpasswd
|
subPath: .htpasswd
|
||||||
- name: tls
|
|
||||||
mountPath: "/etc/ssl/private"
|
|
||||||
- name: nginx-client-ca-bundle
|
- name: nginx-client-ca-bundle
|
||||||
mountPath: "/etc/ssl/"
|
mountPath: "/etc/ssl/client-ca-bundle.pem"
|
||||||
|
subPath: "client-ca-bundle.pem"
|
||||||
- name: acme
|
- name: acme
|
||||||
mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/"
|
mountPath: "/usr/share/nginx/html/.well-known/acme-challenge/"
|
||||||
|
- name: snippets
|
||||||
|
mountPath: "/etc/nginx/snippets/"
|
||||||
|
- name: nginx-secret
|
||||||
|
mountPath: "/etc/ssl/"
|
||||||
volumes:
|
volumes:
|
||||||
- name: atst-config
|
|
||||||
secret:
|
|
||||||
secretName: atst-config-ini
|
|
||||||
items:
|
|
||||||
- key: override.ini
|
|
||||||
path: atst-overrides.ini
|
|
||||||
mode: 0644
|
|
||||||
- name: nginx-client-ca-bundle
|
- name: nginx-client-ca-bundle
|
||||||
configMap:
|
configMap:
|
||||||
name: nginx-client-ca-bundle
|
name: nginx-client-ca-bundle
|
||||||
defaultMode: 0666
|
defaultMode: 0444
|
||||||
|
items:
|
||||||
|
- key: "client-ca-bundle.pem"
|
||||||
|
path: "client-ca-bundle.pem"
|
||||||
- name: nginx-config
|
- name: nginx-config
|
||||||
configMap:
|
configMap:
|
||||||
name: atst-nginx
|
name: atst-nginx
|
||||||
items:
|
|
||||||
- key: nginx-config
|
|
||||||
path: atst.conf
|
|
||||||
- name: uwsgi-socket-dir
|
- name: uwsgi-socket-dir
|
||||||
emptyDir:
|
emptyDir:
|
||||||
medium: Memory
|
medium: Memory
|
||||||
@ -103,16 +98,6 @@ spec:
|
|||||||
- key: htpasswd
|
- key: htpasswd
|
||||||
path: .htpasswd
|
path: .htpasswd
|
||||||
mode: 0640
|
mode: 0640
|
||||||
- name: tls
|
|
||||||
secret:
|
|
||||||
secretName: azure-atat-code-mil-tls
|
|
||||||
items:
|
|
||||||
- key: tls.crt
|
|
||||||
path: atat.crt
|
|
||||||
mode: 0644
|
|
||||||
- key: tls.key
|
|
||||||
path: atat.key
|
|
||||||
mode: 0640
|
|
||||||
- name: crls-vol
|
- name: crls-vol
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: crls-vol-claim
|
claimName: crls-vol-claim
|
||||||
@ -135,6 +120,29 @@ spec:
|
|||||||
- key: uwsgi.ini
|
- key: uwsgi.ini
|
||||||
path: uwsgi.ini
|
path: uwsgi.ini
|
||||||
mode: 0644
|
mode: 0644
|
||||||
|
- name: snippets
|
||||||
|
configMap:
|
||||||
|
name: nginx-snippets
|
||||||
|
- name: nginx-secret
|
||||||
|
flexVolume:
|
||||||
|
driver: "azure/kv"
|
||||||
|
options:
|
||||||
|
usepodidentity: "true"
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "dhparam4096;master-cert;master-cert"
|
||||||
|
keyvaultobjectaliases: "dhparam.pem;atat.key;atat.crt"
|
||||||
|
keyvaultobjecttypes: "secret;secret;secret"
|
||||||
|
tenantid: $TENANT_ID
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
driver: "azure/kv"
|
||||||
|
options:
|
||||||
|
usepodidentity: "true"
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
|
||||||
|
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
|
||||||
|
keyvaultobjecttypes: "secret;secret;secret;secret;key"
|
||||||
|
tenantid: $TENANT_ID
|
||||||
---
|
---
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: extensions/v1beta1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
@ -155,19 +163,21 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: atst
|
app: atst
|
||||||
role: worker
|
role: worker
|
||||||
|
aadpodidbinding: atat-kv-id-binding
|
||||||
spec:
|
spec:
|
||||||
securityContext:
|
securityContext:
|
||||||
fsGroup: 101
|
fsGroup: 101
|
||||||
containers:
|
containers:
|
||||||
- name: atst-worker
|
- name: atst-worker
|
||||||
image: $CONTAINER_IMAGE
|
image: $CONTAINER_IMAGE
|
||||||
args: [
|
args:
|
||||||
|
[
|
||||||
"/opt/atat/atst/.venv/bin/python",
|
"/opt/atat/atst/.venv/bin/python",
|
||||||
"/opt/atat/atst/.venv/bin/celery",
|
"/opt/atat/atst/.venv/bin/celery",
|
||||||
"-A",
|
"-A",
|
||||||
"celery_worker.celery",
|
"celery_worker.celery",
|
||||||
"worker",
|
"worker",
|
||||||
"--loglevel=info"
|
"--loglevel=info",
|
||||||
]
|
]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
@ -175,20 +185,12 @@ spec:
|
|||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: atst-worker-envvars
|
name: atst-worker-envvars
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: atst-config
|
|
||||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
|
||||||
subPath: atst-overrides.ini
|
|
||||||
- name: pgsslrootcert
|
- name: pgsslrootcert
|
||||||
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
|
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
|
||||||
subPath: pgsslrootcert.crt
|
subPath: pgsslrootcert.crt
|
||||||
|
- name: flask-secret
|
||||||
|
mountPath: "/config"
|
||||||
volumes:
|
volumes:
|
||||||
- name: atst-config
|
|
||||||
secret:
|
|
||||||
secretName: atst-config-ini
|
|
||||||
items:
|
|
||||||
- key: override.ini
|
|
||||||
path: atst-overrides.ini
|
|
||||||
mode: 0644
|
|
||||||
- name: pgsslrootcert
|
- name: pgsslrootcert
|
||||||
configMap:
|
configMap:
|
||||||
name: pgsslrootcert
|
name: pgsslrootcert
|
||||||
@ -196,6 +198,16 @@ spec:
|
|||||||
- key: cert
|
- key: cert
|
||||||
path: pgsslrootcert.crt
|
path: pgsslrootcert.crt
|
||||||
mode: 0666
|
mode: 0666
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
driver: "azure/kv"
|
||||||
|
options:
|
||||||
|
usepodidentity: "true"
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
|
||||||
|
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
|
||||||
|
keyvaultobjecttypes: "secret;secret;secret;secret;key"
|
||||||
|
tenantid: $TENANT_ID
|
||||||
---
|
---
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: extensions/v1beta1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
@ -216,19 +228,21 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: atst
|
app: atst
|
||||||
role: beat
|
role: beat
|
||||||
|
aadpodidbinding: atat-kv-id-binding
|
||||||
spec:
|
spec:
|
||||||
securityContext:
|
securityContext:
|
||||||
fsGroup: 101
|
fsGroup: 101
|
||||||
containers:
|
containers:
|
||||||
- name: atst-beat
|
- name: atst-beat
|
||||||
image: $CONTAINER_IMAGE
|
image: $CONTAINER_IMAGE
|
||||||
args: [
|
args:
|
||||||
|
[
|
||||||
"/opt/atat/atst/.venv/bin/python",
|
"/opt/atat/atst/.venv/bin/python",
|
||||||
"/opt/atat/atst/.venv/bin/celery",
|
"/opt/atat/atst/.venv/bin/celery",
|
||||||
"-A",
|
"-A",
|
||||||
"celery_worker.celery",
|
"celery_worker.celery",
|
||||||
"beat",
|
"beat",
|
||||||
"--loglevel=info"
|
"--loglevel=info",
|
||||||
]
|
]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
@ -236,20 +250,12 @@ spec:
|
|||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: atst-worker-envvars
|
name: atst-worker-envvars
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: atst-config
|
|
||||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
|
||||||
subPath: atst-overrides.ini
|
|
||||||
- name: pgsslrootcert
|
- name: pgsslrootcert
|
||||||
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
|
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
|
||||||
subPath: pgsslrootcert.crt
|
subPath: pgsslrootcert.crt
|
||||||
|
- name: flask-secret
|
||||||
|
mountPath: "/config"
|
||||||
volumes:
|
volumes:
|
||||||
- name: atst-config
|
|
||||||
secret:
|
|
||||||
secretName: atst-config-ini
|
|
||||||
items:
|
|
||||||
- key: override.ini
|
|
||||||
path: atst-overrides.ini
|
|
||||||
mode: 0644
|
|
||||||
- name: pgsslrootcert
|
- name: pgsslrootcert
|
||||||
configMap:
|
configMap:
|
||||||
name: pgsslrootcert
|
name: pgsslrootcert
|
||||||
@ -257,6 +263,16 @@ spec:
|
|||||||
- key: cert
|
- key: cert
|
||||||
path: pgsslrootcert.crt
|
path: pgsslrootcert.crt
|
||||||
mode: 0666
|
mode: 0666
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
driver: "azure/kv"
|
||||||
|
options:
|
||||||
|
usepodidentity: "true"
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
|
||||||
|
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
|
||||||
|
keyvaultobjecttypes: "secret;secret;secret;secret;key"
|
||||||
|
tenantid: $TENANT_ID
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
|
@ -5,9 +5,16 @@ metadata:
|
|||||||
namespace: atat
|
namespace: atat
|
||||||
spec:
|
spec:
|
||||||
schedule: "0 * * * *"
|
schedule: "0 * * * *"
|
||||||
|
concurrencyPolicy: Replace
|
||||||
|
successfulJobsHistoryLimit: 1
|
||||||
jobTemplate:
|
jobTemplate:
|
||||||
spec:
|
spec:
|
||||||
template:
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: atst
|
||||||
|
role: crl-sync
|
||||||
|
aadpodidbinding: atat-kv-id-binding
|
||||||
spec:
|
spec:
|
||||||
restartPolicy: OnFailure
|
restartPolicy: OnFailure
|
||||||
containers:
|
containers:
|
||||||
@ -25,19 +32,21 @@ spec:
|
|||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: atst-worker-envvars
|
name: atst-worker-envvars
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: atst-config
|
|
||||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
|
||||||
subPath: atst-overrides.ini
|
|
||||||
- name: crls-vol
|
- name: crls-vol
|
||||||
mountPath: "/opt/atat/atst/crls"
|
mountPath: "/opt/atat/atst/crls"
|
||||||
|
- name: flask-secret
|
||||||
|
mountPath: "/config"
|
||||||
volumes:
|
volumes:
|
||||||
- name: atst-config
|
|
||||||
secret:
|
|
||||||
secretName: atst-config-ini
|
|
||||||
items:
|
|
||||||
- key: override.ini
|
|
||||||
path: atst-overrides.ini
|
|
||||||
mode: 0644
|
|
||||||
- name: crls-vol
|
- name: crls-vol
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: crls-vol-claim
|
claimName: crls-vol-claim
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
driver: "azure/kv"
|
||||||
|
options:
|
||||||
|
usepodidentity: "true"
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
|
||||||
|
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
|
||||||
|
keyvaultobjecttypes: "secret;secret;secret;secret;key"
|
||||||
|
tenantid: $TENANT_ID
|
||||||
|
@ -11,3 +11,4 @@ resources:
|
|||||||
- nginx-client-ca-bundle.yml
|
- nginx-client-ca-bundle.yml
|
||||||
- acme-challenges.yml
|
- acme-challenges.yml
|
||||||
- aadpodidentity.yml
|
- aadpodidentity.yml
|
||||||
|
- nginx-snippets.yml
|
||||||
|
24
deploy/azure/nginx-snippets.yml
Normal file
24
deploy/azure/nginx-snippets.yml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: nginx-snippets
|
||||||
|
namespace: atat
|
||||||
|
data:
|
||||||
|
ssl.conf: |-
|
||||||
|
# Guard against HTTPS -> HTTP downgrade
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always";
|
||||||
|
# Set SSL protocols, ciphers, and related options
|
||||||
|
ssl_protocols TLSv1.3 TLSv1.2;
|
||||||
|
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ecdh_curve X25519:prime256v1:secp384r1;
|
||||||
|
ssl_dhparam /etc/ssl/dhparam.pem;
|
||||||
|
# SSL session options
|
||||||
|
ssl_session_timeout 4h;
|
||||||
|
ssl_session_cache shared:SSL:10m; # 1mb = ~4000 sessions
|
||||||
|
ssl_session_tickets off;
|
||||||
|
# OCSP Stapling
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
resolver 8.8.8.8 8.8.4.4;
|
62
deploy/overlays/staging/flex_vol.yml
Normal file
62
deploy/overlays/staging/flex_vol.yml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: atst
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: nginx-secret
|
||||||
|
flexVolume:
|
||||||
|
options:
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "dhparam4096;staging-cert;staging-cert"
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
options:
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
|
||||||
|
---
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: atst-worker
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
options:
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
|
||||||
|
---
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: atst-beat
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
options:
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
|
||||||
|
---
|
||||||
|
apiVersion: batch/v1beta1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: crls
|
||||||
|
spec:
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
volumes:
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
options:
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
|
@ -7,6 +7,7 @@ patchesStrategicMerge:
|
|||||||
- replica_count.yml
|
- replica_count.yml
|
||||||
- ports.yml
|
- ports.yml
|
||||||
- envvars.yml
|
- envvars.yml
|
||||||
|
- flex_vol.yml
|
||||||
patchesJson6902:
|
patchesJson6902:
|
||||||
- target:
|
- target:
|
||||||
group: extensions
|
group: extensions
|
||||||
|
@ -7,6 +7,11 @@ spec:
|
|||||||
ttlSecondsAfterFinished: 100
|
ttlSecondsAfterFinished: 100
|
||||||
backoffLimit: 2
|
backoffLimit: 2
|
||||||
template:
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: atst
|
||||||
|
role: migration
|
||||||
|
aadpodidbinding: atat-kv-id-binding
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: migration
|
- name: migration
|
||||||
@ -28,20 +33,12 @@ spec:
|
|||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: atst-worker-envvars
|
name: atst-worker-envvars
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: atst-config
|
|
||||||
mountPath: "/opt/atat/atst/atst-overrides.ini"
|
|
||||||
subPath: atst-overrides.ini
|
|
||||||
- name: pgsslrootcert
|
- name: pgsslrootcert
|
||||||
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
|
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
|
||||||
subPath: pgsslrootcert.crt
|
subPath: pgsslrootcert.crt
|
||||||
|
- name: flask-secret
|
||||||
|
mountPath: "/config"
|
||||||
volumes:
|
volumes:
|
||||||
- name: atst-config
|
|
||||||
secret:
|
|
||||||
secretName: atst-config-ini
|
|
||||||
items:
|
|
||||||
- key: override.ini
|
|
||||||
path: atst-overrides.ini
|
|
||||||
mode: 0644
|
|
||||||
- name: pgsslrootcert
|
- name: pgsslrootcert
|
||||||
configMap:
|
configMap:
|
||||||
name: pgsslrootcert
|
name: pgsslrootcert
|
||||||
@ -49,4 +46,14 @@ spec:
|
|||||||
- key: cert
|
- key: cert
|
||||||
path: pgsslrootcert.crt
|
path: pgsslrootcert.crt
|
||||||
mode: 0666
|
mode: 0666
|
||||||
|
- name: flask-secret
|
||||||
|
flexVolume:
|
||||||
|
driver: "azure/kv"
|
||||||
|
options:
|
||||||
|
usepodidentity: "true"
|
||||||
|
keyvaultname: "atat-vault-test"
|
||||||
|
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
|
||||||
|
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
|
||||||
|
keyvaultobjecttypes: "secret;secret;secret;secret;key"
|
||||||
|
tenantid: $TENANT_ID
|
||||||
restartPolicy: Never
|
restartPolicy: Never
|
||||||
|
390
fixtures/fixture_spend_data.json
Normal file
390
fixtures/fixture_spend_data.json
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
{
|
||||||
|
"A-Wing": {
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "LC04",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "Integ",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 663,
|
||||||
|
"JEDI_CLIN_2": 397
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 590,
|
||||||
|
"JEDI_CLIN_2": 829
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 42467,
|
||||||
|
"JEDI_CLIN_2": 33873
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PreProd",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 1000,
|
||||||
|
"JEDI_CLIN_2": 626
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 685,
|
||||||
|
"JEDI_CLIN_2": 331
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 21874,
|
||||||
|
"JEDI_CLIN_2": 25506
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prod",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 856,
|
||||||
|
"JEDI_CLIN_2": 627
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 921,
|
||||||
|
"JEDI_CLIN_2": 473
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 35566,
|
||||||
|
"JEDI_CLIN_2": 42514
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SF18",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "Integ",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 777,
|
||||||
|
"JEDI_CLIN_2": 850
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 584,
|
||||||
|
"JEDI_CLIN_2": 362
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 44505,
|
||||||
|
"JEDI_CLIN_2": 21378
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PreProd",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 487,
|
||||||
|
"JEDI_CLIN_2": 733
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 542,
|
||||||
|
"JEDI_CLIN_2": 999
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 8713,
|
||||||
|
"JEDI_CLIN_2": 10586
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prod",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 420,
|
||||||
|
"JEDI_CLIN_2": 503
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 756,
|
||||||
|
"JEDI_CLIN_2": 941
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 43003,
|
||||||
|
"JEDI_CLIN_2": 20601
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Canton",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "Prod",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 661,
|
||||||
|
"JEDI_CLIN_2": 599
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 962,
|
||||||
|
"JEDI_CLIN_2": 383
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 24501,
|
||||||
|
"JEDI_CLIN_2": 7551
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BD04",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "Integ",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 790,
|
||||||
|
"JEDI_CLIN_2": 513
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 886,
|
||||||
|
"JEDI_CLIN_2": 991
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 43684,
|
||||||
|
"JEDI_CLIN_2": 40196
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PreProd",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 513,
|
||||||
|
"JEDI_CLIN_2": 706
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 945,
|
||||||
|
"JEDI_CLIN_2": 380
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 28189,
|
||||||
|
"JEDI_CLIN_2": 9759
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SCV18",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "Dev",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 933,
|
||||||
|
"JEDI_CLIN_2": 993
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 319,
|
||||||
|
"JEDI_CLIN_2": 619
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 40585,
|
||||||
|
"JEDI_CLIN_2": 28872
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Crown",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "CR Portal Dev",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 711,
|
||||||
|
"JEDI_CLIN_2": 413
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 908,
|
||||||
|
"JEDI_CLIN_2": 632
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 18753,
|
||||||
|
"JEDI_CLIN_2": 4004
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CR Staging",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 440,
|
||||||
|
"JEDI_CLIN_2": 918
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 370,
|
||||||
|
"JEDI_CLIN_2": 472
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 40602,
|
||||||
|
"JEDI_CLIN_2": 6834
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CR Portal Test 1",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 928,
|
||||||
|
"JEDI_CLIN_2": 796
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 680,
|
||||||
|
"JEDI_CLIN_2": 312
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 36058,
|
||||||
|
"JEDI_CLIN_2": 42375
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jewels Prod",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 304,
|
||||||
|
"JEDI_CLIN_2": 428
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 898,
|
||||||
|
"JEDI_CLIN_2": 729
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 3162,
|
||||||
|
"JEDI_CLIN_2": 49836
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jewels Dev",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 498,
|
||||||
|
"JEDI_CLIN_2": 890
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 506,
|
||||||
|
"JEDI_CLIN_2": 659
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 6248,
|
||||||
|
"JEDI_CLIN_2": 3866
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"B-Wing": {
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "NP02",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "Integ",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 455,
|
||||||
|
"JEDI_CLIN_2": 746
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 973,
|
||||||
|
"JEDI_CLIN_2": 504
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 11493,
|
||||||
|
"JEDI_CLIN_2": 17751
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PreProd",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 582,
|
||||||
|
"JEDI_CLIN_2": 339
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 392,
|
||||||
|
"JEDI_CLIN_2": 885
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 41856,
|
||||||
|
"JEDI_CLIN_2": 46399
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prod",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 446,
|
||||||
|
"JEDI_CLIN_2": 670
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 368,
|
||||||
|
"JEDI_CLIN_2": 963
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 10030,
|
||||||
|
"JEDI_CLIN_2": 29253
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FM",
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "Integ",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 994,
|
||||||
|
"JEDI_CLIN_2": 573
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 699,
|
||||||
|
"JEDI_CLIN_2": 418
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 27881,
|
||||||
|
"JEDI_CLIN_2": 37092
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prod",
|
||||||
|
"spending": {
|
||||||
|
"this_month": {
|
||||||
|
"JEDI_CLIN_1": 838,
|
||||||
|
"JEDI_CLIN_2": 839
|
||||||
|
},
|
||||||
|
"last_month": {
|
||||||
|
"JEDI_CLIN_1": 775,
|
||||||
|
"JEDI_CLIN_2": 946
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"JEDI_CLIN_1": 45007,
|
||||||
|
"JEDI_CLIN_2": 16197
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
102
js/components/__tests__/multi_checkbox_input.test.js
Normal file
102
js/components/__tests__/multi_checkbox_input.test.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import multicheckboxinput from '../multi_checkbox_input'
|
||||||
|
import { makeTestWrapper } from '../../test_utils/component_test_helpers'
|
||||||
|
|
||||||
|
const WrapperComponent = makeTestWrapper({
|
||||||
|
components: {
|
||||||
|
multicheckboxinput,
|
||||||
|
},
|
||||||
|
templatePath: 'multi_checkbox_input_template.html',
|
||||||
|
data: function() {
|
||||||
|
const { initialvalue, optional } = this.initialData
|
||||||
|
return { initialvalue, optional }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('MultiCheckboxInput Renders Correctly', () => {
|
||||||
|
it('Should initialize unchecked and with no validation showing', () => {
|
||||||
|
const wrapper = mount(WrapperComponent, {
|
||||||
|
propsData: {
|
||||||
|
name: 'testCheck',
|
||||||
|
initialData: {
|
||||||
|
initialvalue: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(wrapper.contains('.usa-input--success')).toBe(false)
|
||||||
|
expect(wrapper.contains('.usa-input--error')).toBe(false)
|
||||||
|
expect(wrapper.find('.usa-input input[value="a"]').element.checked).toBe(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
expect(wrapper.find('.usa-input input[value="b"]').element.checked).toBe(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should initialize with "a" checked', () => {
|
||||||
|
const wrapper = mount(WrapperComponent, {
|
||||||
|
propsData: {
|
||||||
|
name: 'testCheck',
|
||||||
|
initialData: {
|
||||||
|
initialvalue: ['a'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(wrapper.find('.usa-input input[value="a"]').element.checked).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(wrapper.find('.usa-input input[value="b"]').element.checked).toBe(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Multicheckbox shows validation states correctly', () => {
|
||||||
|
it('Should be valid when any checkbox is clicked', () => {
|
||||||
|
const wrapper = mount(WrapperComponent, {
|
||||||
|
propsData: {
|
||||||
|
name: 'testCheck',
|
||||||
|
initialData: { initialvalue: [] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
wrapper.find('.usa-input input[value="a"]').setChecked()
|
||||||
|
expect(wrapper.contains('.usa-input--success')).toBe(true)
|
||||||
|
expect(wrapper.contains('.usa-input--error')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be invalid when no checkboxes are checked', () => {
|
||||||
|
const wrapper = mount(WrapperComponent, {
|
||||||
|
propsData: {
|
||||||
|
name: 'testCheck',
|
||||||
|
initialData: {
|
||||||
|
initialvalue: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check and then uncheck a checkbox
|
||||||
|
const checkboxA = wrapper.find('.usa-input input[value="a"]')
|
||||||
|
checkboxA.setChecked()
|
||||||
|
checkboxA.setChecked(false)
|
||||||
|
|
||||||
|
expect(wrapper.contains('.usa-input--error')).toBe(true)
|
||||||
|
expect(wrapper.contains('.usa-input--success')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be valid when no checkboxes are checked but it is optional', () => {
|
||||||
|
const wrapper = mount(WrapperComponent, {
|
||||||
|
propsData: {
|
||||||
|
name: 'testCheck',
|
||||||
|
initialData: { initialvalue: [], optional: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check and then uncheck a checkbox
|
||||||
|
const checkboxA = wrapper.find('.usa-input input[value="a"]')
|
||||||
|
checkboxA.setChecked()
|
||||||
|
checkboxA.setChecked(false)
|
||||||
|
|
||||||
|
expect(wrapper.contains('.usa-input--error')).toBe(false)
|
||||||
|
expect(wrapper.contains('.usa-input--success')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
@ -11,4 +11,10 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
collapse: function() {
|
||||||
|
this.isVisible = false
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
16
js/components/accordion_list.js
Normal file
16
js/components/accordion_list.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Accordion from './accordion'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'accordion-list',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Accordion,
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleClick: function(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.$children.forEach(el => el.collapse())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -13,22 +13,14 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
initialOtherValue: String,
|
|
||||||
optional: Boolean,
|
optional: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
const showError = (this.initialErrors && this.initialErrors.length) || false
|
|
||||||
return {
|
return {
|
||||||
showError: showError,
|
showError: this.initialErrors.length > 0,
|
||||||
showValid: !showError && this.initialValue.length > 0,
|
showValid: false,
|
||||||
validationError: this.initialErrors.join(' '),
|
validationError: this.initialErrors.join(' '),
|
||||||
otherChecked: this.initialValue.includes('other')
|
|
||||||
? true
|
|
||||||
: this.otherChecked,
|
|
||||||
otherText: this.initialValue.includes('other')
|
|
||||||
? this.initialOtherValue
|
|
||||||
: '',
|
|
||||||
selections: this.initialValue,
|
selections: this.initialValue,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -36,17 +28,15 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
onInput: function(e) {
|
onInput: function(e) {
|
||||||
emitFieldChange(this)
|
emitFieldChange(this)
|
||||||
this.showError = false
|
this.showError = !this.valid
|
||||||
this.showValid = true
|
this.showValid = !this.showError
|
||||||
},
|
this.validationError = 'This field is required.'
|
||||||
otherToggle: function() {
|
|
||||||
this.otherChecked = !this.otherChecked
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
valid: function() {
|
valid: function() {
|
||||||
return this.optional || this.showValid
|
return this.optional || this.selections.length > 0
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { set } from 'vue/dist/vue'
|
import { set } from 'vue/dist/vue'
|
||||||
import { formatDollars } from '../../lib/dollars'
|
import { formatDollars } from '../../lib/dollars'
|
||||||
|
import { set as _set } from 'lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'spend-table',
|
name: 'spend-table',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
applications: Object,
|
applications: Array,
|
||||||
environments: Object,
|
|
||||||
currentMonthIndex: String,
|
|
||||||
prevMonthIndex: String,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
@ -18,20 +16,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
Object.keys(this.applications).forEach(application => {
|
this.applicationsState.forEach(application => {
|
||||||
set(this.applicationsState[application], 'isVisible', false)
|
application.isVisible = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggle: function(e, applicationName) {
|
toggle: function(e, applicationIndex) {
|
||||||
this.applicationsState = Object.assign(this.applicationsState, {
|
set(this.applicationsState, applicationIndex, {
|
||||||
[applicationName]: Object.assign(
|
...this.applicationsState[applicationIndex],
|
||||||
this.applicationsState[applicationName],
|
isVisible: !this.applicationsState[applicationIndex].isVisible,
|
||||||
{
|
|
||||||
isVisible: !this.applicationsState[applicationName].isVisible,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -58,18 +58,18 @@ export default {
|
|||||||
this.$refs.attachmentFilename.value = file.name
|
this.$refs.attachmentFilename.value = file.name
|
||||||
this.$refs.attachmentObjectName.value = response.objectName
|
this.$refs.attachmentObjectName.value = response.objectName
|
||||||
this.$refs.attachmentInput.disabled = true
|
this.$refs.attachmentInput.disabled = true
|
||||||
|
emitFieldChange(this)
|
||||||
|
this.changed = true
|
||||||
|
|
||||||
this.downloadLink = await this.getDownloadLink(
|
this.downloadLink = await this.getDownloadLink(
|
||||||
file.name,
|
file.name,
|
||||||
response.objectName
|
response.objectName
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
emitFieldChange(this)
|
||||||
|
this.changed = true
|
||||||
this.uploadError = true
|
this.uploadError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.changed = true
|
|
||||||
|
|
||||||
emitFieldChange(this)
|
|
||||||
},
|
},
|
||||||
removeAttachment: function(e) {
|
removeAttachment: function(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -7,6 +7,8 @@ import Vue from 'vue/dist/vue'
|
|||||||
import VTooltip from 'v-tooltip'
|
import VTooltip from 'v-tooltip'
|
||||||
import stickybits from 'stickybits'
|
import stickybits from 'stickybits'
|
||||||
|
|
||||||
|
import Accordion from './components/accordion'
|
||||||
|
import AccordionList from './components/accordion_list'
|
||||||
import dodlogin from './components/dodlogin'
|
import dodlogin from './components/dodlogin'
|
||||||
import optionsinput from './components/options_input'
|
import optionsinput from './components/options_input'
|
||||||
import multicheckboxinput from './components/multi_checkbox_input'
|
import multicheckboxinput from './components/multi_checkbox_input'
|
||||||
@ -29,7 +31,6 @@ import SemiCollapsibleText from './components/semi_collapsible_text'
|
|||||||
import ToForm from './components/forms/to_form'
|
import ToForm from './components/forms/to_form'
|
||||||
import ClinFields from './components/clin_fields'
|
import ClinFields from './components/clin_fields'
|
||||||
import PopDateRange from './components/pop_date_range'
|
import PopDateRange from './components/pop_date_range'
|
||||||
import Accordion from './components/accordion'
|
|
||||||
import ToggleMenu from './components/toggle_menu'
|
import ToggleMenu from './components/toggle_menu'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
@ -42,6 +43,7 @@ const app = new Vue({
|
|||||||
el: '#app-root',
|
el: '#app-root',
|
||||||
components: {
|
components: {
|
||||||
Accordion,
|
Accordion,
|
||||||
|
AccordionList,
|
||||||
dodlogin,
|
dodlogin,
|
||||||
toggler,
|
toggler,
|
||||||
optionsinput,
|
optionsinput,
|
||||||
|
@ -17,6 +17,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
toggle: function(e) {
|
toggle: function(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
this.isVisible = !this.isVisible
|
this.isVisible = !this.isVisible
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -15,7 +15,7 @@ PASSWORD = os.getenv("ATAT_BA_PASSWORD", "")
|
|||||||
DISABLE_VERIFY = os.getenv("DISABLE_VERIFY", "true").lower() == "true"
|
DISABLE_VERIFY = os.getenv("DISABLE_VERIFY", "true").lower() == "true"
|
||||||
|
|
||||||
# Alpha numerics for random entity names
|
# Alpha numerics for random entity names
|
||||||
LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
|
LETTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890" #pragma: allowlist secret
|
||||||
|
|
||||||
NEW_PORTFOLIO_CHANCE = 10
|
NEW_PORTFOLIO_CHANCE = 10
|
||||||
NEW_APPLICATION_CHANCE = 10
|
NEW_APPLICATION_CHANCE = 10
|
||||||
@ -141,15 +141,8 @@ def create_portfolio(l):
|
|||||||
new_portfolio_form = l.client.get("/portfolios/new")
|
new_portfolio_form = l.client.get("/portfolios/new")
|
||||||
new_portfolio_body = {
|
new_portfolio_body = {
|
||||||
"name": f"Load Test Created - {''.join(choices(LETTERS, k=5))}",
|
"name": f"Load Test Created - {''.join(choices(LETTERS, k=5))}",
|
||||||
"defense_component": "Army, Department of the",
|
"defense_component": "army",
|
||||||
"description": "Test",
|
"description": "Test",
|
||||||
"app_migration": "none",
|
|
||||||
"native_apps": "yes",
|
|
||||||
"complexity": "storage",
|
|
||||||
"complexity_other": "",
|
|
||||||
"dev_team": "civilians",
|
|
||||||
"dev_team_other": "",
|
|
||||||
"team_experience": "none",
|
|
||||||
"csrf_token": get_csrf_token(new_portfolio_form),
|
"csrf_token": get_csrf_token(new_portfolio_form),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
94
script/integration_tests
Executable file
94
script/integration_tests
Executable file
@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# script/integration_tests: Run the integration tests via docker.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -z "${CONTAINER_TIMEOUT+is_set}" ]; then
|
||||||
|
CONTAINER_TIMEOUT=200
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Expected settings. Script will error if these are not provided.
|
||||||
|
SETTINGS=(
|
||||||
|
CONTAINER_IMAGE
|
||||||
|
NGROK_TOKEN
|
||||||
|
GI_API_KEY
|
||||||
|
GI_SUITE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Loop all expected settings. Track ones that are missing. If any
|
||||||
|
# are missing, exit.
|
||||||
|
MISSING_SETTINGS=()
|
||||||
|
for envvar in "${SETTINGS[@]}"; do
|
||||||
|
if [ -z "${!envvar}" ]; then
|
||||||
|
MISSING_SETTINGS+=(${envvar})
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#MISSING_SETTINGS[@]} > 0 ]]; then
|
||||||
|
>&2 echo "The following variables need to be set:"
|
||||||
|
for missing in "${MISSING_SETTINGS[@]}"; do
|
||||||
|
>&2 echo $missing
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove any existing container and network instances
|
||||||
|
docker container stop redis postgres test-atat || true && docker container rm redis postgres test-atat || true
|
||||||
|
docker network rm atat || true
|
||||||
|
|
||||||
|
# Create network
|
||||||
|
docker network create atat
|
||||||
|
|
||||||
|
# Start Redis and Postgres
|
||||||
|
docker run -d --network atat --link redis:redis -p 6379:6379 --name redis circleci/redis:4-alpine3.8
|
||||||
|
docker run -d --network atat --link postgres:postgres -p 5432:5432 --name postgres circleci/postgres:10-alpine-ram
|
||||||
|
# Wait for datastores to be available
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Create database and run migrations
|
||||||
|
docker exec postgres createdb -U postgres atat
|
||||||
|
docker run --network atat -e PGDATABASE=atat -e PGHOST=postgres -e REDIS_HOST=redis:6379 $CONTAINER_IMAGE .venv/bin/python .venv/bin/alembic upgrade head
|
||||||
|
docker run --network atat -e PGDATABASE=atat -e PGHOST=postgres -e REDIS_HOST=redis:6379 $CONTAINER_IMAGE .venv/bin/python script/seed_roles.py
|
||||||
|
|
||||||
|
# Start application container
|
||||||
|
docker run -d \
|
||||||
|
-e DISABLE_CRL_CHECK=true \
|
||||||
|
-e PGHOST=postgres \
|
||||||
|
-e REDIS_HOST=redis:6379 \
|
||||||
|
-p 8000:8000 \
|
||||||
|
--network atat \
|
||||||
|
--name test-atat \
|
||||||
|
$CONTAINER_IMAGE \
|
||||||
|
/bin/sh -c "
|
||||||
|
echo CLOUD_PROVIDER=mock > .env &&\
|
||||||
|
yarn build &&\
|
||||||
|
uwsgi \
|
||||||
|
--callable app \
|
||||||
|
--module app \
|
||||||
|
--plugin python3 \
|
||||||
|
--virtualenv /install/.venv \
|
||||||
|
--http-socket :8000
|
||||||
|
"
|
||||||
|
|
||||||
|
# Use curl to wait for application container to become available
|
||||||
|
docker pull curlimages/curl:latest
|
||||||
|
docker run --network atat \
|
||||||
|
curlimages/curl:latest \
|
||||||
|
curl --connect-timeout 3 \
|
||||||
|
--max-time 5 \
|
||||||
|
--retry $CONTAINER_TIMEOUT \
|
||||||
|
--retry-connrefused \
|
||||||
|
--retry-delay 1 \
|
||||||
|
--retry-max-time $CONTAINER_TIMEOUT \
|
||||||
|
test-atat:8000
|
||||||
|
|
||||||
|
# Run Ghost Inspector tests
|
||||||
|
docker pull ghostinspector/test-runner-standalone:latest
|
||||||
|
docker run \
|
||||||
|
-e NGROK_TOKEN=$NGROK_TOKEN \
|
||||||
|
-e GI_API_KEY=$GI_API_KEY \
|
||||||
|
-e GI_SUITE=$GI_SUITE \
|
||||||
|
-e GI_PARAMS_JSON='{}' \
|
||||||
|
-e APP_PORT="test-atat:8000" \
|
||||||
|
--network atat \
|
||||||
|
ghostinspector/test-runner-standalone:latest
|
@ -13,6 +13,7 @@ SETTINGS=(
|
|||||||
AUTH_DOMAIN
|
AUTH_DOMAIN
|
||||||
KV_MI_ID
|
KV_MI_ID
|
||||||
KV_MI_CLIENT_ID
|
KV_MI_CLIENT_ID
|
||||||
|
TENANT_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
# Loop all expected settings. Track ones that are missing and build
|
# Loop all expected settings. Track ones that are missing and build
|
||||||
|
@ -14,6 +14,7 @@ from atst.app import make_config, make_app
|
|||||||
from atst.database import db
|
from atst.database import db
|
||||||
|
|
||||||
from atst.models.application import Application
|
from atst.models.application import Application
|
||||||
|
from atst.models.clin import JEDICLINType
|
||||||
from atst.models.environment_role import CSPRole
|
from atst.models.environment_role import CSPRole
|
||||||
|
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
@ -30,6 +31,8 @@ from atst.domain.users import Users
|
|||||||
|
|
||||||
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
||||||
|
|
||||||
|
from atst.utils import pick
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
random_service_branch,
|
random_service_branch,
|
||||||
TaskOrderFactory,
|
TaskOrderFactory,
|
||||||
@ -197,7 +200,22 @@ def add_task_orders_to_portfolio(portfolio):
|
|||||||
CLINFactory.build(
|
CLINFactory.build(
|
||||||
task_order=expired_to, start_date=(today - five_days), end_date=yesterday
|
task_order=expired_to, start_date=(today - five_days), end_date=yesterday
|
||||||
),
|
),
|
||||||
CLINFactory.build(task_order=active_to, start_date=yesterday, end_date=future),
|
CLINFactory.build(
|
||||||
|
task_order=active_to,
|
||||||
|
start_date=yesterday,
|
||||||
|
end_date=future,
|
||||||
|
total_amount=1_000_000,
|
||||||
|
obligated_amount=500_000,
|
||||||
|
jedi_clin_type=JEDICLINType.JEDI_CLIN_1,
|
||||||
|
),
|
||||||
|
CLINFactory.build(
|
||||||
|
task_order=active_to,
|
||||||
|
start_date=yesterday,
|
||||||
|
end_date=future,
|
||||||
|
total_amount=500_000,
|
||||||
|
obligated_amount=200_000,
|
||||||
|
jedi_clin_type=JEDICLINType.JEDI_CLIN_2,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
task_orders = [draft_to, unsigned_to, upcoming_to, expired_to, active_to]
|
task_orders = [draft_to, unsigned_to, upcoming_to, expired_to, active_to]
|
||||||
@ -238,6 +256,7 @@ def add_applications_to_portfolio(portfolio):
|
|||||||
None,
|
None,
|
||||||
first_name=user_data["first_name"],
|
first_name=user_data["first_name"],
|
||||||
last_name=user_data["last_name"],
|
last_name=user_data["last_name"],
|
||||||
|
email=user_data["email"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app_role = ApplicationRoles.create(
|
app_role = ApplicationRoles.create(
|
||||||
@ -263,7 +282,23 @@ def add_applications_to_portfolio(portfolio):
|
|||||||
|
|
||||||
def create_demo_portfolio(name, data):
|
def create_demo_portfolio(name, data):
|
||||||
try:
|
try:
|
||||||
portfolio_owner = Users.get_or_create_by_dod_id("2345678901") # Amanda
|
portfolio_owner = Users.get_or_create_by_dod_id(
|
||||||
|
"2345678901",
|
||||||
|
**pick(
|
||||||
|
[
|
||||||
|
"permission_sets",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
"service_branch",
|
||||||
|
"phone_number",
|
||||||
|
"citizenship",
|
||||||
|
"designation",
|
||||||
|
"date_latest_training",
|
||||||
|
],
|
||||||
|
DEV_USERS["amanda"],
|
||||||
|
),
|
||||||
|
) # Amanda
|
||||||
# auditor = Users.get_by_dod_id("3453453453") # Sally
|
# auditor = Users.get_by_dod_id("3453453453") # Sally
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
print(
|
print(
|
||||||
@ -281,9 +316,9 @@ def create_demo_portfolio(name, data):
|
|||||||
|
|
||||||
for mock_application in data["applications"]:
|
for mock_application in data["applications"]:
|
||||||
application = Application(
|
application = Application(
|
||||||
portfolio=portfolio, name=mock_application.name, description=""
|
portfolio=portfolio, name=mock_application["name"], description=""
|
||||||
)
|
)
|
||||||
env_names = [env.name for env in mock_application.environments]
|
env_names = [env["name"] for env in mock_application["environments"]]
|
||||||
envs = Environments.create_many(portfolio.owner, application, env_names)
|
envs = Environments.create_many(portfolio.owner, application, env_names)
|
||||||
db.session.add(application)
|
db.session.add(application)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -294,8 +329,8 @@ def seed_db():
|
|||||||
amanda = Users.get_by_dod_id("2345678901")
|
amanda = Users.get_by_dod_id("2345678901")
|
||||||
|
|
||||||
# Create Portfolios for Amanda with mocked reporting data
|
# Create Portfolios for Amanda with mocked reporting data
|
||||||
create_demo_portfolio("A-Wing", MockReportingProvider.REPORT_FIXTURE_MAP["A-Wing"])
|
create_demo_portfolio("A-Wing", MockReportingProvider.FIXTURE_SPEND_DATA["A-Wing"])
|
||||||
create_demo_portfolio("B-Wing", MockReportingProvider.REPORT_FIXTURE_MAP["B-Wing"])
|
create_demo_portfolio("B-Wing", MockReportingProvider.FIXTURE_SPEND_DATA["B-Wing"])
|
||||||
|
|
||||||
tie_interceptor = Portfolios.create(
|
tie_interceptor = Portfolios.create(
|
||||||
user=amanda,
|
user=amanda,
|
||||||
|
@ -45,11 +45,3 @@
|
|||||||
@import "sections/application_edit";
|
@import "sections/application_edit";
|
||||||
@import "sections/reports";
|
@import "sections/reports";
|
||||||
@import "sections/task_order";
|
@import "sections/task_order";
|
||||||
|
|
||||||
//
|
|
||||||
// IE likes to display an outline when focusing on an element. This
|
|
||||||
// fix removes that unwanted outline on focus.
|
|
||||||
//
|
|
||||||
*:focus {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
|
padding: $gap * 3;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: $color-gray-lightest;
|
||||||
|
margin-top: $gap * 5;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-left: -$gap * 3;
|
||||||
|
margin-right: -$gap * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 5rem ($gap * 2) 2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
> .icon {
|
a.usa-button {
|
||||||
@include icon-size(50);
|
width: 60%;
|
||||||
@include icon-color($color-gray-light);
|
display: inline-block;
|
||||||
}
|
|
||||||
|
|
||||||
&__message {
|
|
||||||
font-weight: $font-bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__sub-message {
|
|
||||||
@include h4;
|
|
||||||
|
|
||||||
color: $color-gray;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
@include media($large-screen) {
|
|
||||||
@include h3;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
// Form Grid
|
// Form Grid
|
||||||
.form-row {
|
.form-row {
|
||||||
margin: ($gap * 4) 0;
|
margin: ($gap * 4) 0;
|
||||||
|
&--separated {
|
||||||
|
border-bottom: $color-gray-lighter 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
.form-col {
|
.form-col {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -383,6 +383,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.portfolio-applications {
|
.portfolio-applications {
|
||||||
|
margin-top: $gap * 5;
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
&--title {
|
&--title {
|
||||||
@include subheading;
|
@include subheading;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.sticky-cta {
|
.sticky-cta {
|
||||||
margin-left: -$gap * 4;
|
margin-left: -$gap * 5;
|
||||||
margin-right: -$gap * 5;
|
margin-right: -$gap * 5;
|
||||||
z-index: 10;
|
z-index: 1;
|
||||||
background-color: $color-gray-lightest;
|
background-color: $color-gray-lightest;
|
||||||
border-top: 1px solid $color-gray-lighter;
|
border-top: 1px solid $color-gray-lighter;
|
||||||
border-bottom: 1px solid $color-gray-lighter;
|
border-bottom: 1px solid $color-gray-lighter;
|
||||||
|
@ -40,8 +40,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.col--grow {
|
&.col--grow {
|
||||||
flex: 1;
|
flex: 1 auto;
|
||||||
flex-grow: 1;
|
|
||||||
padding-right: $spacing-small;
|
padding-right: $spacing-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +73,10 @@
|
|||||||
color: $color-green;
|
color: $color-green;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: $color-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
.user-permission {
|
.user-permission {
|
||||||
font-weight: $font-normal;
|
font-weight: $font-normal;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ $footer-height: 5rem;
|
|||||||
$usa-banner-height: 2.8rem;
|
$usa-banner-height: 2.8rem;
|
||||||
$sidenav-expanded-width: 25rem;
|
$sidenav-expanded-width: 25rem;
|
||||||
$sidenav-collapsed-width: 10rem;
|
$sidenav-collapsed-width: 10rem;
|
||||||
|
$max-panel-width: 80rem;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -1,148 +1,65 @@
|
|||||||
.triangle-box {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.triangle-up {
|
|
||||||
$triangle-size: $gap * 1.5;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: $triangle-size solid transparent;
|
|
||||||
border-right: $triangle-size solid transparent;
|
|
||||||
border-bottom: $triangle-size solid $color-blue-light;
|
|
||||||
bottom: -4px;
|
|
||||||
right: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.accordion {
|
.accordion {
|
||||||
@include block-list;
|
@include shadow-panel;
|
||||||
|
margin: $gap * 3 0;
|
||||||
box-shadow: 0 4px 10px 0 rgba(193, 193, 193, 0.5);
|
max-width: $max-panel-width;
|
||||||
margin-bottom: 6 * $gap;
|
|
||||||
|
|
||||||
.icon-link {
|
|
||||||
margin: (-$gap) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-link,
|
|
||||||
.label {
|
|
||||||
&:first-child {
|
|
||||||
margin-left: -$gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: -$gap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
@include block-list-header;
|
padding: $gap * 2 $gap * 3;
|
||||||
|
background-color: $color-white;
|
||||||
|
|
||||||
border-top: 3px solid $color-blue;
|
&-text {
|
||||||
border-bottom: none;
|
|
||||||
box-shadow: 0 2px 4px 0 rgba(216, 218, 222, 0.58);
|
|
||||||
|
|
||||||
&.row {
|
|
||||||
background: $color-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
@include block-list__title;
|
|
||||||
|
|
||||||
color: $color-blue;
|
|
||||||
|
|
||||||
@include h3;
|
|
||||||
|
|
||||||
&.icon-link {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: block;
|
|
||||||
padding: 0 $gap;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__description {
|
&__button {
|
||||||
@include block-list__description;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
font-style: italic;
|
&__content {
|
||||||
font-size: $small-font-size;
|
padding: 0 ($gap * 3) $gap;
|
||||||
|
|
||||||
|
&--list-item {
|
||||||
|
border-bottom: 1px solid $color-gray-lightest;
|
||||||
|
padding: $gap 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
padding-right: $gap * 2;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: $gap * 2 0 $gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1rem;
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
}
|
margin: 0;
|
||||||
|
|
||||||
&__actions {
|
|
||||||
margin-top: $gap;
|
|
||||||
margin-bottom: $gap * 0.5;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.icon-link {
|
|
||||||
font-size: $small-font-size;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&--empty {
|
||||||
@include block-list__footer;
|
font-weight: $font-bold;
|
||||||
|
color: $color-gray-dark;
|
||||||
border-top: 0;
|
padding: $gap * 8;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&-list {
|
||||||
@include block-list-item;
|
max-width: $max-panel-width;
|
||||||
|
|
||||||
opacity: 0.75;
|
&__collapse {
|
||||||
background-color: $color-blue-light;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid rgba($color-gray-light, 0.5);
|
|
||||||
|
|
||||||
&--selectable {
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
|
|
||||||
@include ie-only {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> label {
|
|
||||||
@include block-list-selectable-label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> label {
|
|
||||||
@include block-list-selectable-label;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked {
|
|
||||||
+ label {
|
|
||||||
color: $color-primary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include ie-only {
|
|
||||||
dl {
|
|
||||||
width: 100%;
|
|
||||||
padding-left: $gap * 4;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.counter {
|
|
||||||
background-color: $color-cool-blue-light;
|
|
||||||
color: $color-white;
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: ($gap / 2) $gap;
|
|
||||||
margin-left: $gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
border: 1px solid $color-gray-medium;
|
|
||||||
opacity: 0.75;
|
|
||||||
margin: 0 (0.5 * $gap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -90,4 +90,8 @@
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--primary {
|
||||||
|
@include icon-color($color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.jedi-clin-funding {
|
.jedi-clin-funding {
|
||||||
|
$insufficient-gradient: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
$color-secondary-dark,
|
||||||
|
$color-secondary-dark 10px,
|
||||||
|
$color-secondary-darkest 11px,
|
||||||
|
$color-secondary-darkest 14px
|
||||||
|
);
|
||||||
|
|
||||||
|
$graph-bar-height: 2rem;
|
||||||
|
|
||||||
padding-top: $gap * 3;
|
padding-top: $gap * 3;
|
||||||
padding-bottom: $gap * 3;
|
padding-bottom: $gap * 3;
|
||||||
|
|
||||||
@ -37,14 +47,36 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__meter {
|
&__graph {
|
||||||
margin: 10px 0;
|
|
||||||
-moz-transform: scale(-1, 1);
|
|
||||||
-webkit-transform: scale(-1, 1);
|
|
||||||
-o-transform: scale(-1, 1);
|
|
||||||
-ms-transform: scale(-1, 1);
|
|
||||||
transform: scale(-1, 1);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: $graph-bar-height;
|
||||||
|
margin-top: $gap * 2;
|
||||||
|
margin-bottom: $gap * 2;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&-bar {
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin-right: $gap / 2;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--invoiced {
|
||||||
|
background: $color-green;
|
||||||
|
}
|
||||||
|
&--estimated {
|
||||||
|
background: $color-green-lighter;
|
||||||
|
}
|
||||||
|
&--remaining {
|
||||||
|
background: $color-primary-darkest;
|
||||||
|
}
|
||||||
|
&--insufficient {
|
||||||
|
background: $insufficient-gradient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-values {
|
&-values {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -52,13 +84,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__meta {
|
&__meta {
|
||||||
&--remaining {
|
margin-right: $gap * 5;
|
||||||
margin-left: auto;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
&-header {
|
&-header {
|
||||||
@include small-copy;
|
@include small-copy;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-key {
|
||||||
|
height: $graph-bar-height;
|
||||||
|
width: $graph-bar-height;
|
||||||
|
margin-right: $gap / 2;
|
||||||
|
|
||||||
|
&--invoiced {
|
||||||
|
background: $color-green;
|
||||||
|
}
|
||||||
|
&--estimated {
|
||||||
|
background: $color-green-lighter;
|
||||||
|
}
|
||||||
|
&--remaining {
|
||||||
|
background: $color-primary-darkest;
|
||||||
|
}
|
||||||
|
&--insufficient {
|
||||||
|
background: $insufficient-gradient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&-value {
|
&-value {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -87,4 +138,32 @@
|
|||||||
font-size: $lead-font-size;
|
font-size: $lead-font-size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reporting-expended-funding {
|
||||||
|
&__header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&__content {
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid $color-gray-lighter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reporting-spend-table {
|
||||||
|
&__env-row {
|
||||||
|
&-label {
|
||||||
|
margin-left: $gap * 5;
|
||||||
|
}
|
||||||
|
&--last {
|
||||||
|
& > td {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
& > td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,3 @@
|
|||||||
.task-order-list {
|
|
||||||
margin-top: 6 * $gap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-order-card {
|
|
||||||
&__buttons .usa-button {
|
|
||||||
min-width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__buttons .usa-button-secondary {
|
|
||||||
min-width: 14rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: $small-font-size;
|
|
||||||
margin-right: 2 * $gap;
|
|
||||||
min-width: 7rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-order {
|
.task-order {
|
||||||
margin-top: $gap * 4;
|
margin-top: $gap * 4;
|
||||||
width: 900px;
|
width: 900px;
|
||||||
@ -149,21 +127,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
|
||||||
&--pending,
|
|
||||||
&--started {
|
|
||||||
background-color: $color-gold;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
background-color: $color-green;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--expired {
|
|
||||||
background-color: $color-red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-order-document-link {
|
.task-order-document-link {
|
||||||
&__icon {
|
&__icon {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/accordion.html" import Accordion %}
|
||||||
|
{% from "components/accordion_list.html" import AccordionList %}
|
||||||
{% from "components/empty_state.html" import EmptyState %}
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
|
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
{% extends "portfolios/base.html" %}
|
{% extends "portfolios/base.html" %}
|
||||||
|
|
||||||
@ -7,85 +10,74 @@
|
|||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
<div class='portfolio-applications'>
|
{% call StickyCTA(text="common.applications"|translate) %}
|
||||||
{% include "fragments/flash.html" %}
|
{% if can_create_applications and portfolio.applications %}
|
||||||
<div class='portfolio-applications__header row'>
|
<a href="{{ url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id) }}" class="usa-button usa-button-primary">
|
||||||
<div class='portfolio-applications__header--title col col--grow'>Applications</div>
|
{{ "portfolios.applications.create_button"|translate }}
|
||||||
<div class='portfolio-applications__header--actions col'>
|
|
||||||
{% if can_create_applications %}
|
|
||||||
<a class='icon-link' href='{{ url_for('applications.view_new_application_step_1', portfolio_id=portfolio.id) }}'>
|
|
||||||
{{ 'portfolios.applications.add_application_text' | translate }}
|
|
||||||
{{ Icon("plus", classes="sidenav__link-icon") }}
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% endcall %}
|
||||||
</div>
|
|
||||||
|
<div class='portfolio-applications'>
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
{% if not portfolio.applications %}
|
{% if not portfolio.applications %}
|
||||||
|
|
||||||
{{ EmptyState(
|
{{ EmptyState(
|
||||||
'This portfolio doesn’t have any applications',
|
header="portfolios.applications.empty_state.header"|translate,
|
||||||
action_label='Add a new application' if can_create_applications else None,
|
message="portfolios.applications.empty_state.message"|translate,
|
||||||
action_href=url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id) if can_create_applications else None,
|
button_text="portfolios.applications.empty_state.button_text"|translate,
|
||||||
icon='cloud',
|
button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id),
|
||||||
sub_message=None if can_create_applications else 'Please contact your JEDI Cloud portfolio administrator to set up a new application.',
|
view_only_text="portfolios.applications.empty_state.view_only_text"|translate,
|
||||||
add_perms=can_create_applications
|
user_can_create=can_create_applications,
|
||||||
) }}
|
) }}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{% call AccordionList() %}
|
||||||
<div class='application-list'>
|
|
||||||
{% for application in portfolio.applications|sort(attribute='name') %}
|
{% for application in portfolio.applications|sort(attribute='name') %}
|
||||||
{% set section_name = "application-{}".format(application.id) %}
|
{% set section_name = "application-{}".format(application.id) %}
|
||||||
|
{% set title = "Environments ({})".format(application.environments|length) %}
|
||||||
<toggler inline-template>
|
<div class="accordion">
|
||||||
<div class='accordion application-list-item'>
|
<div class="accordion__header">
|
||||||
<header class='accordion__header row'>
|
<h3 class="accordion__header-text">
|
||||||
<div class='col col-grow'>
|
<a href='{{ url_for("applications.settings", application_id=application.id) }}'>
|
||||||
<h3 class='icon-link accordion__title' v-on:click="toggleSection('{{ section_name }}')">{{ application.name }}</h3>
|
{{ application.name }} {{ Icon("caret_right", classes="icon--tiny icon--primary") }}
|
||||||
<p class='accordion__description'>
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p class="accordion__header-text">
|
||||||
{{ application.description }}
|
{{ application.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class='accordion__actions'>
|
|
||||||
<a class='icon-link' href='{{ url_for("applications.settings", application_id=application.id) }}'>
|
|
||||||
<span>{{ "portfolios.applications.app_settings_text" | translate }}</span>
|
|
||||||
</a>
|
|
||||||
<div class='separator'></div>
|
|
||||||
{% set has_environments = 0 < (application.environments|length) %}
|
|
||||||
<a class='icon-link triangle-box' v-on:click="toggleSection('{{ section_name }}')" disabled="{{ not has_environments }}">
|
|
||||||
<span>Environments ({{ application.environments|length }})</span>
|
|
||||||
{% if has_environments %}
|
|
||||||
<span v-if="selectedSection === '{{ section_name }}'">
|
|
||||||
{{ Icon('caret_up') }}
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{ Icon('caret_down') }}
|
|
||||||
</span>
|
|
||||||
<div class="triangle-up" v-if="selectedSection === '{{ section_name }}'"></div>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% call Accordion(
|
||||||
</header>
|
title=title,
|
||||||
<ul v-show="selectedSection === '{{ section_name }}'">
|
id=section_name,
|
||||||
|
heading_tag="h4"
|
||||||
|
) %}
|
||||||
{% for environment in application.environments %}
|
{% for environment in application.environments %}
|
||||||
<li class='accordion__item application-list-item__environment'>
|
{% set env_access = environment_access[environment.id] %}
|
||||||
<div class='application-list-item__environment__name'>
|
<div class="accordion__content--list-item">
|
||||||
<span>{{ environment.displayname }}</span>
|
<div class="row">
|
||||||
</div>
|
<div class="col col--grow">
|
||||||
{% if g.current_user in environment.users %}
|
{% if env_access %}
|
||||||
<a href='{{ url_for("applications.access_environment", environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer' class='application-list-item__environment__csp_link icon-link'>
|
<a href='{{ url_for("applications.access_environment", environment_id=environment.id)}}' target='_blank' rel='noopener noreferrer'>
|
||||||
<span>{{ "portfolios.applications.csp_console_text" | translate }}</span>
|
{{ environment.displayname }} {{ Icon('link', classes='icon--medium icon--primary') }}
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{{ environment.displayname }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</toggler>
|
{% if env_access %}
|
||||||
{% endfor %}
|
<div class="col">
|
||||||
|
{{ env_access }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endcall %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
{% macro Accordion(title, id, heading_level="h2") %}
|
{% macro Accordion(
|
||||||
<accordion inline-template>
|
title,
|
||||||
<div>
|
id,
|
||||||
<{{heading_level}}>
|
wrapper_tag="div",
|
||||||
|
wrapper_classes="",
|
||||||
|
heading_tag="h2",
|
||||||
|
heading_classes="",
|
||||||
|
content_tag="div",
|
||||||
|
content_classes="") %}
|
||||||
|
<accordion v-cloak inline-template>
|
||||||
|
<{{wrapper_tag}} class="{{ wrapper_classes }}">
|
||||||
|
<{{heading_tag}} class="accordion__button {{ heading_classes }}">
|
||||||
<button
|
<button
|
||||||
v-on:click="toggle($event)"
|
v-on:click="toggle($event)"
|
||||||
class="usa-accordion-button"
|
class="usa-accordion-button"
|
||||||
@ -10,14 +18,13 @@
|
|||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</button>
|
</button>
|
||||||
</{{heading_level}}>
|
</{{heading_tag}}>
|
||||||
<div
|
<{{content_tag}}
|
||||||
id="{{ id }}"
|
id="{{ id }}"
|
||||||
class="usa-accordion-content"
|
class="usa-accordion-content accordion__content {{ content_classes }}"
|
||||||
v-bind:aria-hidden="isVisible ? 'false' : 'true'"
|
v-bind:aria-hidden="isVisible ? 'false' : 'true'">
|
||||||
>
|
|
||||||
{{ caller() }}
|
{{ caller() }}
|
||||||
</div>
|
</{{content_tag}}>
|
||||||
</div>
|
</{{wrapper_tag}}>
|
||||||
</accordion>
|
</accordion>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
11
templates/components/accordion_list.html
Normal file
11
templates/components/accordion_list.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% macro AccordionList() %}
|
||||||
|
<accordion-list inline-template>
|
||||||
|
<div class="accordion-list usa-accordion">
|
||||||
|
<div class="action-group">
|
||||||
|
<a v-on:click="handleClick($event)" class="accordion-list__collapse">Collapse All</a>
|
||||||
|
</div>
|
||||||
|
<!-- caller iterates over accordion vue components or Accordion jinja macros -->
|
||||||
|
{{ caller() }}
|
||||||
|
</div>
|
||||||
|
</accordion-list>
|
||||||
|
{% endmacro %}
|
@ -1,20 +1,14 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% macro EmptyState(header, message, button_text, button_link, view_only_text, user_can_create=True) %}
|
||||||
|
<div class="empty-state">
|
||||||
{% macro EmptyState(message, action_label, action_href, icon=None, sub_message=None, add_perms=True) -%}
|
<h3>{{ header }}</h3>
|
||||||
<div class='empty-state'>
|
<p>{{ message }}</p>
|
||||||
<p class='empty-state__message'>{{ message }}</p>
|
<hr>
|
||||||
|
<div class="empty-state__footer">
|
||||||
{% if icon %}
|
{% if user_can_create %}
|
||||||
{{ Icon(icon) }}
|
<a href="{{ button_link }}" class="usa-button usa-button-primary">{{ button_text }}</a>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ view_only_text }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if sub_message %}
|
|
||||||
<p class='empty-state__sub-message'>{{ sub_message }}</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if add_perms and (action_href and action_label) %}
|
|
||||||
<a href='{{ action_href }}' class='usa-button usa-button-big'>{{ action_label }}</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
@ -45,19 +45,8 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for choice in field.choices %}
|
{% for choice in field.choices %}
|
||||||
<li>
|
<li>
|
||||||
{% if choice[0] != 'other' %}
|
|
||||||
<input type='checkbox' name='{{ field.name }}' id='{{ field.name }}-{{ loop.index0 }}' value='{{ choice[0] }}' v-model="selections"/>
|
<input type='checkbox' name='{{ field.name }}' id='{{ field.name }}-{{ loop.index0 }}' value='{{ choice[0] }}' v-model="selections"/>
|
||||||
<label for='{{ field.name }}-{{ loop.index0 }}'>{{ choice[1] | safe }}</label>
|
<label for='{{ field.name }}-{{ loop.index0 }}'>{{ choice[1] | safe }}</label>
|
||||||
{% else %}
|
|
||||||
<input @click="otherToggle" type='checkbox' name='{{ field.name }}' id='{{ field.name }}-{{ loop.index0 }}' value='other' v-model="selections"/>
|
|
||||||
<label for='{{ field.name }}-{{ loop.index0 }}'>{{ choice[1] | safe }}</label>
|
|
||||||
|
|
||||||
{% if other_input_field %}
|
|
||||||
<div v-show="otherChecked">
|
|
||||||
<input type='text' name='{{ other_input_field.name}}' id='{{ field.name }}-other' v-model:value="otherText" aria-expanded='false' />
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% call StickyCTA(sticky_header) %}
|
{% call StickyCTA(sticky_header) %}
|
||||||
<a href="{{ url_for("portfolios.new_portfolio") }}" class="usa-button-primary">
|
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button-primary">
|
||||||
{{ "home.add_portfolio_button_text" | translate }}
|
{{ "home.add_portfolio_button_text" | translate }}
|
||||||
</a>
|
</a>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% from "components/empty_state.html" import EmptyState %}
|
|
||||||
{% from "components/tooltip.html" import Tooltip %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{{
|
|
||||||
EmptyState(
|
|
||||||
action_href="#",
|
|
||||||
action_label=("portfolios.index.empty.start_button" | translate),
|
|
||||||
icon="cloud",
|
|
||||||
message=("portfolios.index.empty.title" | translate),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{% endblock %}
|
|
@ -1,54 +0,0 @@
|
|||||||
{% from "components/multi_checkbox_input.html" import MultiCheckboxInput %}
|
|
||||||
{% from "components/options_input.html" import OptionsInput %}
|
|
||||||
{% from "components/save_button.html" import SaveButton %}
|
|
||||||
{% from "components/text_input.html" import TextInput %}
|
|
||||||
|
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<main class="usa-section usa-content">
|
|
||||||
{% include "fragments/flash.html" %}
|
|
||||||
<h1>New Portfolio Form</h1>
|
|
||||||
<base-form inline-template>
|
|
||||||
<form class="panel__content" id="portfolio-create" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
|
||||||
{{ form.csrf_token }}
|
|
||||||
|
|
||||||
{{ TextInput(form.name, optional=False) }}
|
|
||||||
{{ OptionsInput(form.defense_component, optional=False) }}
|
|
||||||
{{ TextInput(form.description, paragraph=True) }}
|
|
||||||
|
|
||||||
<h3 id="reporting" class="subheading">{{ "task_orders.new.app_info.project_title" | translate }}</h3>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
{{ OptionsInput(form.app_migration) }}
|
|
||||||
|
|
||||||
{{ OptionsInput(form.native_apps) }}
|
|
||||||
<p>{{ "forms.task_order.native_apps.not_sure_help" | translate }}</p>
|
|
||||||
{{ MultiCheckboxInput(form.complexity, form.complexity_other) }}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3 class="subheading">{{ "task_orders.new.app_info.team_title" | translate }}</h3>
|
|
||||||
<p>{{ "task_orders.new.app_info.subtitle" | translate }}</p>
|
|
||||||
{{ MultiCheckboxInput(form.dev_team, form.dev_team_other) }}
|
|
||||||
{{ OptionsInput(form.team_experience) }}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class='action-group'>
|
|
||||||
{{
|
|
||||||
SaveButton(
|
|
||||||
text=('common.save' | translate),
|
|
||||||
form="portfolio-create",
|
|
||||||
element="input",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</base-form>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
52
templates/portfolios/new/step_1.html
Normal file
52
templates/portfolios/new/step_1.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% from "components/multi_checkbox_input.html" import MultiCheckboxInput %}
|
||||||
|
{% from "components/options_input.html" import OptionsInput %}
|
||||||
|
{% from "components/save_button.html" import SaveButton %}
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<main class="usa-section usa-content">
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
<div class='portfolio-header__name'>
|
||||||
|
<p>{{ "portfolios.header" | translate }}</p>
|
||||||
|
<h1>{{ "New Portfolio" }}</h1>
|
||||||
|
</div>
|
||||||
|
{{ StickyCTA(text="Create New Portfolio") }}
|
||||||
|
<base-form inline-template>
|
||||||
|
<form id="portfolio-create" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<div class="form-row form-row--separated">
|
||||||
|
<div class="form-col">
|
||||||
|
{{ TextInput(form.name, optional=False) }}
|
||||||
|
{{"forms.portfolio.name.help_text" | translate | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row form-row--separated">
|
||||||
|
<div class="form-col">
|
||||||
|
{{ TextInput(form.description, paragraph=True) }}
|
||||||
|
{{"forms.portfolio.description.help_text" | translate | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-col">
|
||||||
|
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
|
||||||
|
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='action-group'>
|
||||||
|
{{
|
||||||
|
SaveButton(
|
||||||
|
text=('common.save' | translate),
|
||||||
|
form="portfolio-create",
|
||||||
|
element="input",
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</base-form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -3,9 +3,6 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2>Funds Expended per Application and Environment</h2>
|
<h2>Funds Expended per Application and Environment</h2>
|
||||||
{% set current_month_index = current_month.strftime('%m/%Y') %}
|
|
||||||
{% set prev_month_index = prev_month.strftime('%m/%Y') %}
|
|
||||||
|
|
||||||
{% if not portfolio.applications %}
|
{% if not portfolio.applications %}
|
||||||
|
|
||||||
{% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %}
|
{% set can_create_applications = user_can(permissions.CREATE_APPLICATION) %}
|
||||||
@ -15,20 +12,16 @@
|
|||||||
%}
|
%}
|
||||||
|
|
||||||
{{ EmptyState(
|
{{ EmptyState(
|
||||||
('portfolios.reports.empty_state.message' | translate),
|
header='portfolios.reports.empty_state.message' | translate,
|
||||||
action_label= ('portfolios.reports.empty_state.action_label' | translate) if can_create_applications else None,
|
message=message,
|
||||||
action_href=url_for('applications.create_new_application_step_1', portfolio_id=portfolio.id) if can_create_applications else None,
|
button_text="portfolios.applications.empty_state.button_text"|translate,
|
||||||
icon='chart',
|
button_link=url_for("applications.view_new_application_step_1", portfolio_id=portfolio.id),
|
||||||
sub_message=message,
|
view_only_text="portfolios.applications.empty_state.view_only_text"|translate,
|
||||||
add_perms=can_create_applications
|
user_can_create=can_create_applications,
|
||||||
) }}
|
) }}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<spend-table
|
<spend-table v-bind:applications='{{ monthly_spending | tojson }}' inline-template>
|
||||||
v-bind:applications='{{ monthly_totals['applications'] | tojson }}'
|
|
||||||
v-bind:environments='{{ monthly_totals['environments'] | tojson }}'
|
|
||||||
current-month-index='{{ current_month_index }}'
|
|
||||||
prev-month-index='{{ prev_month_index }}'
|
|
||||||
inline-template>
|
|
||||||
<div class="responsive-table-wrapper">
|
<div class="responsive-table-wrapper">
|
||||||
<table class="atat-table">
|
<table class="atat-table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -40,37 +33,41 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template v-for='(application, name) in applicationsState'>
|
<template v-for='(application, applicationIndex) in applicationsState'>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<button v-on:click='toggle($event, name)' class='icon-link icon-link--large'>
|
<button v-on:click='toggle($event, applicationIndex)' class='icon-link icon-link--large'>
|
||||||
<span v-html='name'></span>
|
<span v-html='application.name'></span>
|
||||||
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
|
<template v-if='application.isVisible'>{{ Icon('caret_down') }}</template>
|
||||||
<template v-else>{{ Icon('caret_up') }}</template>
|
<template v-else>{{ Icon('caret_up') }}</template>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right">
|
<td class="table-cell--align-right">
|
||||||
<span v-html='formatDollars(application[currentMonthIndex] || 0)'></span>
|
<span v-html='formatDollars(application.this_month || 0)'></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right">
|
<td class="table-cell--align-right">
|
||||||
<span v-html='formatDollars(application[prevMonthIndex] || 0)'></span>
|
<span v-html='formatDollars(application.last_month || 0)'></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right">
|
<td class="table-cell--align-right">
|
||||||
<span v-html='formatDollars(application["total_spend_to_date"])'></span>
|
<span v-html='formatDollars(application.total)'></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for='(environment, envName) in environments[name]' v-show='application.isVisible'>
|
<tr
|
||||||
|
v-show='application.isVisible'
|
||||||
|
v-for='(environment, index) in application.environments'
|
||||||
|
v-bind:class="[ index == application.environments.length -1 ? 'reporting-spend-table__env-row--last' : '']"
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
<span v-html='envName'></span>
|
<span class="reporting-spend-table__env-row-label" v-html='environment.name'></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right">
|
<td class="table-cell--align-right">
|
||||||
<span v-html='formatDollars(environment[currentMonthIndex] || 0)'></span>
|
<span v-html='formatDollars(environment.this_month || 0)'></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right">
|
<td class="table-cell--align-right">
|
||||||
<span v-html='formatDollars(environment[prevMonthIndex] || 0)'></span>
|
<span v-html='formatDollars(environment.last_month || 0)'></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell--align-right">
|
<td class="table-cell--align-right">
|
||||||
<span v-html='formatDollars(environment["total_spend_to_date"])'></span>
|
<span v-html='formatDollars(environment.total)'></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,34 +1,51 @@
|
|||||||
{% from "components/accordion.html" import Accordion %}
|
{% from "components/accordion.html" import Accordion %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="usa-accordion">
|
<div class="usa-accordion">
|
||||||
{% call Accordion("Expired Task Orders", "expired_task_orders", "h3") %}
|
{% call Accordion(
|
||||||
|
"Expired funding",
|
||||||
|
"expired_funding",
|
||||||
|
heading_classes="reporting-expended-funding__header",
|
||||||
|
content_tag="table",
|
||||||
|
content_classes="atat-table reporting-expended-funding__content") %}
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>TO CLIN</th>
|
||||||
|
<th>PoP</th>
|
||||||
|
<th>CLIN Value</th>
|
||||||
|
<th>Amount Obligated</th>
|
||||||
|
<th>Amount Unspent</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for task_order in expired_task_orders %}
|
{% for task_order in expired_task_orders %}
|
||||||
<a href="{{ url_for("task_orders.review_task_order", task_order_id=task_order["id"]) }}">
|
<tr>
|
||||||
Task Order {{ task_order["number"] }}
|
<td colspan="5">
|
||||||
|
<span class="h4 reporting-expended-funding__header">Task Order</span> <a href="{{ url_for("task_orders.review_task_order", task_order_id=task_order.id) }}">
|
||||||
|
{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--blue" ) }}
|
||||||
</a>
|
</a>
|
||||||
<div>
|
</td>
|
||||||
<p>Period of Performance</p>
|
</tr>
|
||||||
<p>
|
{% for clin in task_order.clins %}
|
||||||
{{ task_order["period_of_performance"].start_date | formattedDate(formatter="%B %d, %Y") }}
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="h4 reporting-expended-funding__header">{{ clin.number }}</div>
|
||||||
|
<div>{{ ("{}".format(clin.jedi_clin_type) | translate)[15:] }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ clin.start_date | formattedDate(formatter="%b %d, %Y") }}
|
||||||
-
|
-
|
||||||
{{ task_order["period_of_performance"].end_date | formattedDate(formatter="%B %d, %Y") }}
|
{{ clin.end_date | formattedDate(formatter="%b %d, %Y") }}
|
||||||
</p>
|
</td>
|
||||||
</div>
|
<td>{{ clin.total_amount | dollars }}</td>
|
||||||
<div>
|
<td>{{ clin.obligated_amount | dollars }}</td>
|
||||||
<p>Total Obligated</p>
|
<td>{{ 0 | dollars }}</td>
|
||||||
<p>{{ task_order["total_obligated_funds"] | dollars }}</p>
|
<tr>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>Total Expended</p>
|
|
||||||
<p>{{ task_order["expended_funds"] | dollars }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>Total Unused</p>
|
|
||||||
<p>{{ (task_order["total_obligated_funds"] - task_order["expended_funds"]) | dollars }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
{{ StickyCTA("Reports") }}
|
{{ StickyCTA("Reports") }}
|
||||||
|
|
||||||
<div class="portfolio-reports col col--grow">
|
<div class="portfolio-reports col col--grow">
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
<p class="row estimate-warning">{{ "portfolios.reports.estimate_warning" | translate }}</p>
|
<p class="row estimate-warning">{{ "portfolios.reports.estimate_warning" | translate }}</p>
|
||||||
{% include "portfolios/reports/portfolio_summary.html" %}
|
{% include "portfolios/reports/portfolio_summary.html" %}
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -3,28 +3,61 @@
|
|||||||
<section>
|
<section>
|
||||||
<header class="reporting-section-header">
|
<header class="reporting-section-header">
|
||||||
<h2 class="reporting-section-header__header">Current Obligated funds</h2>
|
<h2 class="reporting-section-header__header">Current Obligated funds</h2>
|
||||||
<span class="reporting-section-header__subheader">As of {{ now | formattedDate(formatter="%B %d, %Y at %H:%M") }}</span>
|
<span class="reporting-section-header__subheader">As of {{ retrieved | formattedDate(formatter="%B %d, %Y at %H:%M") }}</span>
|
||||||
</header>
|
</header>
|
||||||
<div class='panel'>
|
<div class='panel'>
|
||||||
<div class='panel__content jedi-clin-funding'>
|
<div class='panel__content jedi-clin-funding'>
|
||||||
{% for JEDI_clin, funds in current_obligated_funds.items() %}
|
{% for JEDI_clin in current_obligated_funds | sort(attribute='name')%}
|
||||||
{% set remaining_funds = (funds["obligated_funds"] - funds["expended_funds"]) %}
|
|
||||||
<div class="jedi-clin-funding__clin-wrapper">
|
<div class="jedi-clin-funding__clin-wrapper">
|
||||||
<h3 class="h5 jedi-clin-funding__header">
|
<h3 class="h5 jedi-clin-funding__header">
|
||||||
{{ "JEDICLINType.{}".format(JEDI_clin) | translate }}
|
{{ "JEDICLINType.{}".format(JEDI_clin.name) | translate }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="jedi-clin-funding__subheader">Total obligated amount: {{ funds["obligated_funds"] | dollars }}</p>
|
<p class="jedi-clin-funding__subheader">Total obligated amount: {{ JEDI_clin.obligated | dollars }}</p>
|
||||||
<meter class="jedi-clin-funding__meter" value='{{remaining_funds}}' min='0' max='{{ funds["obligated_funds"] }}' title='{{ JEDI_clin }}'>
|
<div class="jedi-clin-funding__graph">
|
||||||
<div class='jedi-clin-funding__meter-fallback' style='width:{{ (funds["expended_funds"] / funds["obligated_funds"]) * 100 }}%;'></div>
|
{% if JEDI_clin.remaining < 0 %}
|
||||||
</meter>
|
<span style="width:100%" class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--insufficient"></span>
|
||||||
<div class="jedi-clin-funding__meter-values">
|
{% else %}
|
||||||
<div class="jedi-clin-funding__meta">
|
{% set invoiced_width = (JEDI_clin.invoiced, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
|
||||||
<p class="jedi-clin-funding__meta-header">Funds expended:</p>
|
{% if invoiced_width %}
|
||||||
<p class="h3 jedi-clin-funding__meta-value">{{ funds["expended_funds"] | dollars }}</p>
|
<span style="width:{{ invoiced_width }}%"
|
||||||
|
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--invoiced">
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set estimated_width = (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
|
||||||
|
{% if estimated_width %}
|
||||||
|
<span style="width:{{ (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
|
||||||
|
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--estimated">
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<span style="width:{{ (JEDI_clin.remaining, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
|
||||||
|
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--remaining">
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="jedi-clin-funding__meta jedi-clin-funding__meta--remaining">
|
<div class="jedi-clin-funding__graph-values">
|
||||||
<p class="jedi-clin-funding__meta-header">Remaining funds:</p>
|
<div class="jedi-clin-funding__meta">
|
||||||
<p class="h3 jedi-clin-funding__meta-value">{{ remaining_funds | dollars }}</p>
|
<p class="jedi-clin-funding__meta-header">
|
||||||
|
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--invoiced"></span>
|
||||||
|
Invoiced expended funds:
|
||||||
|
</p>
|
||||||
|
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.invoiced | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="jedi-clin-funding__meta">
|
||||||
|
<p class="jedi-clin-funding__meta-header">
|
||||||
|
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--estimated"></span>
|
||||||
|
Estimated expended funds:
|
||||||
|
</p>
|
||||||
|
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.estimated | dollars }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="jedi-clin-funding__meta">
|
||||||
|
<p class="jedi-clin-funding__meta-header">
|
||||||
|
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--{{"remaining" if JEDI_clin.remaining > 0 else "insufficient"}}"></span>
|
||||||
|
Remaining funds:
|
||||||
|
</p>
|
||||||
|
<p class="h3 jedi-clin-funding__meta-value {% if JEDI_clin.remaining < 0 %}text-danger{% endif %}">{{ JEDI_clin.remaining | dollars }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
{% from "components/accordion.html" import Accordion %}
|
||||||
|
{% from "components/accordion_list.html" import AccordionList %}
|
||||||
{% from "components/empty_state.html" import EmptyState %}
|
{% from "components/empty_state.html" import EmptyState %}
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||||
@ -6,96 +8,68 @@
|
|||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
|
||||||
{% macro TaskOrderButton(task_order, route, text="Edit", secondary=False) %}
|
|
||||||
<a href="{{ url_for(route, task_order_id=task_order.id) }}" class="usa-button {{ 'usa-button-secondary' if secondary else '' }}">
|
|
||||||
{{ text }}
|
|
||||||
</a>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro TaskOrderDateTime(dt, className="") %}
|
{% macro TaskOrderDateTime(dt, className="") %}
|
||||||
<local-datetime timestamp="{{ dt }}" format="MMMM D, YYYY" class="{{ className }}"></local-datetime>
|
<local-datetime timestamp="{{ dt }}" format="MMMM D, YYYY" class="{{ className }}"></local-datetime>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro TaskOrderDate(task_order) %}
|
|
||||||
<span class="datetime">
|
|
||||||
<!-- Draft: {Begins, Began} start_date -->
|
|
||||||
<!-- Everything else: {Starts, Started} start_date | {Ends, Ended} end_date -->
|
|
||||||
|
|
||||||
{% if task_order.is_draft %}
|
{% macro TaskOrderList(task_orders, status) %}
|
||||||
{% if task_order.has_begun %}
|
<div class="accordion">
|
||||||
Started on
|
{% call Accordion(title=("task_orders.status_list_title"|translate({'status': status})), id=status, heading_tag="h4") %}
|
||||||
{% else %}
|
{% if task_orders|length > 0 %}
|
||||||
Starts on
|
|
||||||
{% endif %}
|
|
||||||
{{ TaskOrderDateTime(task_order.time_created) }}
|
|
||||||
{% else %}
|
|
||||||
{% if task_order.has_begun %}
|
|
||||||
Began
|
|
||||||
{% else %}
|
|
||||||
Begins
|
|
||||||
{% endif %}
|
|
||||||
{{ TaskOrderDateTime(task_order.start_date) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not task_order.is_draft %}
|
|
||||||
|
|
|
||||||
|
|
||||||
{% if task_order.has_ended %}
|
|
||||||
Ended
|
|
||||||
{% else %}
|
|
||||||
Ends
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{ TaskOrderDateTime(task_order.end_date) }}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro TaskOrderActions(task_order) %}
|
|
||||||
<div class="task-order-card__buttons">
|
|
||||||
{% if task_order.is_draft and user_can(permissions.EDIT_TASK_ORDER_DETAILS) %}
|
|
||||||
{{ TaskOrderButton(task_order, "task_orders.edit")}}
|
|
||||||
{% elif task_order.is_expired %}
|
|
||||||
{{ TaskOrderButton(task_order, "task_orders.review_task_order", text="View") }}
|
|
||||||
{% elif task_order.is_unsigned %}
|
|
||||||
{% if user_can(permissions.EDIT_TASK_ORDER_DETAILS) %}
|
|
||||||
{{ TaskOrderButton(task_order, "task_orders.form_step_four_review", text="Sign", secondary=True) }}
|
|
||||||
{% endif %}
|
|
||||||
{{ TaskOrderButton(task_order, "task_orders.review_task_order", text="View") }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro TaskOrderList(task_orders, label='success') %}
|
|
||||||
<div class="task-order-list">
|
|
||||||
{% for task_order in task_orders %}
|
{% for task_order in task_orders %}
|
||||||
<div class="card task-order-card">
|
{% set to_number %}
|
||||||
<div class="card__status">
|
{% if task_order.number != None %}
|
||||||
<span class='label label--{{ label_colors[task_order.status] }}'>{{ task_order.display_status }}</span>
|
Task Order #{{ task_order.number }}
|
||||||
{{ TaskOrderDate(task_order) }}
|
{% else %}
|
||||||
<span class="card__status-spacer"></span>
|
New Task Order
|
||||||
<span class="card__button">
|
{% endif %}
|
||||||
{{ TaskOrderActions(task_order) }}
|
{% endset %}
|
||||||
</span>
|
<div class="accordion__content--list-item">
|
||||||
|
<h4><a href="{{ url_for('task_orders.review_task_order', task_order_id=task_order.id) }}">{{ to_number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}</a></h4>
|
||||||
|
{% if status != 'Expired' -%}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col--grow">
|
||||||
|
<h5>
|
||||||
|
Current Period of Performance
|
||||||
|
</h5>
|
||||||
|
<p>
|
||||||
|
{{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }}
|
||||||
|
-
|
||||||
|
{{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__header">
|
<div class="col col--grow">
|
||||||
<h3>Task Order #{{ task_order.number }}</h3>
|
<h5>Total Value</h5>
|
||||||
|
<p>{{ task_order.total_contract_amount | dollars }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="col col--grow">
|
||||||
<b>Total amount: </b>{{ task_order.total_contract_amount | dollars }}
|
<h5>Total Obligated</h5>
|
||||||
|
<p>{{ task_order.total_obligated_funds | dollars }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__body">
|
<div class="col col--grow">
|
||||||
<b>Obligated amount: </b>{{ task_order.total_obligated_funds | dollars }}
|
<h5>Total Expended</h5>
|
||||||
|
<p>{{ task_order.invoiced_funds | dollars }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="accordion__content--empty">
|
||||||
|
{{ "task_orders.status_empty_state" | translate({ 'status': status }) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endcall %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% call StickyCTA(text="Funding") %}
|
{% call StickyCTA(text="common.task_orders"|translate) %}
|
||||||
{% if user_can(permissions.CREATE_TASK_ORDER) %}
|
{% if user_can(permissions.CREATE_TASK_ORDER) and task_orders %}
|
||||||
<a href="{{ url_for("task_orders.form_step_one_add_pdf", portfolio_id=portfolio.id) }}" class="usa-button usa-button-primary" type="submit">Start a new task order</a>
|
<a href="{{ url_for("task_orders.form_step_one_add_pdf", portfolio_id=portfolio.id) }}" class="usa-button usa-button-primary">
|
||||||
|
{{ "task_orders.add_new_button" | translate }}
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
@ -103,15 +77,20 @@
|
|||||||
|
|
||||||
<div class="portfolio-funding">
|
<div class="portfolio-funding">
|
||||||
|
|
||||||
{% if task_orders %}
|
{% if to_count > 0 %}
|
||||||
{{ TaskOrderList(task_orders) }}
|
{% call AccordionList() %}
|
||||||
|
{% for status, to_list in task_orders.items() %}
|
||||||
|
{{ TaskOrderList(to_list, status) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endcall %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ EmptyState(
|
{{ EmptyState(
|
||||||
'This portfolio doesn’t have any active or pending task orders.',
|
header="task_orders.empty_state.header"|translate,
|
||||||
action_label='Add a New Task Order',
|
message="task_orders.empty_state.message"|translate,
|
||||||
action_href=url_for('task_orders.form_step_one_add_pdf', portfolio_id=portfolio.id),
|
button_link=url_for('task_orders.form_step_one_add_pdf', portfolio_id=portfolio.id),
|
||||||
icon='cloud',
|
button_text="task_orders.empty_state.button_text"|translate,
|
||||||
add_perms=user_can(permissions.CREATE_TASK_ORDER)
|
view_only_text="task_orders.empty_state.view_only_text"|translate,
|
||||||
|
user_can_create=user_can(permissions.CREATE_TASK_ORDER),
|
||||||
) }}
|
) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
1
terraform/.gitignore
vendored
Normal file
1
terraform/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.terraform
|
35
terraform/modules/k8s/main.tf
Normal file
35
terraform/modules/k8s/main.tf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
resource "azurerm_resource_group" "k8s" {
|
||||||
|
name = "${var.name}-${var.environment}-vpc"
|
||||||
|
location = var.region
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_kubernetes_cluster" "k8s" {
|
||||||
|
name = "${var.name}-${var.environment}-k8s"
|
||||||
|
location = azurerm_resource_group.k8s.location
|
||||||
|
resource_group_name = azurerm_resource_group.k8s.name
|
||||||
|
dns_prefix = var.k8s_dns_prefix
|
||||||
|
|
||||||
|
service_principal {
|
||||||
|
client_id = "f05a4457-bd5e-4c63-98e1-89aab42645d0"
|
||||||
|
client_secret = "19b69e2c-9f55-4850-87cb-88c67a8dc811"
|
||||||
|
}
|
||||||
|
|
||||||
|
default_node_pool {
|
||||||
|
name = "default"
|
||||||
|
vm_size = "Standard_D1_v2"
|
||||||
|
os_disk_size_gb = 30
|
||||||
|
vnet_subnet_id = var.vnet_subnet_id
|
||||||
|
node_count = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
ignore_changes = [
|
||||||
|
default_node_pool.0.node_count
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
environment = var.environment
|
||||||
|
owner = var.owner
|
||||||
|
}
|
||||||
|
}
|
0
terraform/modules/k8s/outputs.tf
Normal file
0
terraform/modules/k8s/outputs.tf
Normal file
35
terraform/modules/k8s/variables.tf
Normal file
35
terraform/modules/k8s/variables.tf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
variable "region" {
|
||||||
|
type = string
|
||||||
|
description = "Region this module and resources will be created in"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
description = "Unique name for the services in this module"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "environment" {
|
||||||
|
type = string
|
||||||
|
description = "Environment these resources reside (prod, dev, staging, etc)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "owner" {
|
||||||
|
type = string
|
||||||
|
description = "Owner of the environment and resources created in this module"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "k8s_dns_prefix" {
|
||||||
|
type = string
|
||||||
|
description = "A DNS prefix"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "k8s_node_size" {
|
||||||
|
type = string
|
||||||
|
description = "The size of the instance to use in the node pools for k8s"
|
||||||
|
default = "Standard_A1_v2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vnet_subnet_id" {
|
||||||
|
description = "Subnet to use for the default k8s pool"
|
||||||
|
type = string
|
||||||
|
}
|
40
terraform/modules/keyvault/main.tf
Normal file
40
terraform/modules/keyvault/main.tf
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
data "azurerm_client_config" "current" {}
|
||||||
|
|
||||||
|
resource "azurerm_resource_group" "keyvault" {
|
||||||
|
name = "${var.name}-${var.environment}-rg"
|
||||||
|
location = var.region
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_key_vault" "keyvault" {
|
||||||
|
name = "${var.name}-${var.environment}-keyvault"
|
||||||
|
location = azurerm_resource_group.keyvault.location
|
||||||
|
resource_group_name = azurerm_resource_group.keyvault.name
|
||||||
|
tenant_id = data.azurerm_client_config.current.tenant_id
|
||||||
|
|
||||||
|
sku_name = "premium"
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
environment = var.environment
|
||||||
|
owner = var.owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_key_vault_access_policy" "keyvault" {
|
||||||
|
key_vault_id = azurerm_key_vault.keyvault.id
|
||||||
|
|
||||||
|
tenant_id = "b5ab0e1e-09f8-4258-afb7-fb17654bc5b3"
|
||||||
|
object_id = "2ca63d41-d058-4e06-aef6-eb517a53b631"
|
||||||
|
|
||||||
|
key_permissions = [
|
||||||
|
"get",
|
||||||
|
"list",
|
||||||
|
"create",
|
||||||
|
]
|
||||||
|
|
||||||
|
secret_permissions = [
|
||||||
|
"get",
|
||||||
|
"list",
|
||||||
|
"set",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
24
terraform/modules/keyvault/variables.tf
Normal file
24
terraform/modules/keyvault/variables.tf
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
variable "region" {
|
||||||
|
type = string
|
||||||
|
description = "Region this module and resources will be created in"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
description = "Unique name for the services in this module"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "environment" {
|
||||||
|
type = string
|
||||||
|
description = "Environment these resources reside (prod, dev, staging, etc)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "owner" {
|
||||||
|
type = string
|
||||||
|
description = "Owner of this environment"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tenant_id" {
|
||||||
|
type = string
|
||||||
|
description = "The Tenant ID"
|
||||||
|
}
|
37
terraform/modules/postgres/main.tf
Normal file
37
terraform/modules/postgres/main.tf
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
resource "azurerm_resource_group" "sql" {
|
||||||
|
name = "${var.name}-${var.environment}-postgres"
|
||||||
|
location = var.region
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_postgresql_server" "sql" {
|
||||||
|
name = "${var.name}-${var.environment}-sql"
|
||||||
|
location = azurerm_resource_group.sql.location
|
||||||
|
resource_group_name = azurerm_resource_group.sql.name
|
||||||
|
|
||||||
|
sku {
|
||||||
|
name = var.sku_name
|
||||||
|
capacity = var.sku_capacity
|
||||||
|
tier = var.sku_tier
|
||||||
|
family = var.sku_family
|
||||||
|
}
|
||||||
|
|
||||||
|
storage_profile {
|
||||||
|
storage_mb = var.storage_mb
|
||||||
|
backup_retention_days = var.storage_backup_retention_days
|
||||||
|
geo_redundant_backup = var.storage_geo_redundant_backup
|
||||||
|
auto_grow = var.storage_auto_grow
|
||||||
|
}
|
||||||
|
|
||||||
|
administrator_login = var.administrator_login
|
||||||
|
administrator_login_password = var.administrator_login_password
|
||||||
|
version = var.postgres_version
|
||||||
|
ssl_enforcement = var.ssl_enforcement
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_postgresql_virtual_network_rule" "sql" {
|
||||||
|
name = "${var.name}-${var.environment}-rule"
|
||||||
|
resource_group_name = azurerm_resource_group.sql.name
|
||||||
|
server_name = azurerm_postgresql_server.sql.name
|
||||||
|
subnet_id = var.subnet_id
|
||||||
|
ignore_missing_vnet_service_endpoint = true
|
||||||
|
}
|
0
terraform/modules/postgres/outputs.tf
Normal file
0
terraform/modules/postgres/outputs.tf
Normal file
100
terraform/modules/postgres/variables.tf
Normal file
100
terraform/modules/postgres/variables.tf
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
variable "region" {
|
||||||
|
type = string
|
||||||
|
description = "Region this module and resources will be created in"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
type = string
|
||||||
|
description = "Unique name for the services in this module"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "environment" {
|
||||||
|
type = string
|
||||||
|
description = "Environment these resources reside (prod, dev, staging, etc)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "owner" {
|
||||||
|
type = string
|
||||||
|
description = "Owner of the environment and resources created in this module"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "subnet_id" {
|
||||||
|
type = string
|
||||||
|
description = "Subnet the SQL server should run"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sku_name" {
|
||||||
|
type = string
|
||||||
|
description = "SKU name"
|
||||||
|
default = "GP_Gen5_2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sku_capacity" {
|
||||||
|
type = string
|
||||||
|
description = "SKU Capacity"
|
||||||
|
default = "2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sku_tier" {
|
||||||
|
type = string
|
||||||
|
description = "SKU Tier"
|
||||||
|
default = "GeneralPurpose"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sku_family" {
|
||||||
|
type = string
|
||||||
|
description = "SKU Family"
|
||||||
|
default = "Gen5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "storage_mb" {
|
||||||
|
type = string
|
||||||
|
description = "Size in MB of the storage used for the sql server"
|
||||||
|
default = "5120"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
variable "storage_backup_retention_days" {
|
||||||
|
type = string
|
||||||
|
description = "Storage backup retention (days)"
|
||||||
|
default = "7"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "storage_geo_redundant_backup" {
|
||||||
|
type = string
|
||||||
|
description = "Geographic redundant backup (Enabled/Disabled)"
|
||||||
|
default = "Disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "storage_auto_grow" {
|
||||||
|
type = string
|
||||||
|
description = "Auto Grow? (Enabled/Disabled)"
|
||||||
|
default = "Enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "administrator_login" {
|
||||||
|
type = string
|
||||||
|
description = "Administrator login"
|
||||||
|
default = "sqladmindude" # FIXME - Remove with wrapper using KeyVault
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "administrator_login_password" {
|
||||||
|
type = string
|
||||||
|
description = "Administrator password"
|
||||||
|
default = "eI0l7yswwtuhHpwzoVjwRKdAcuGNsg" # FIXME - Remove with wrapper using KeyVault
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
variable "postgres_version" {
|
||||||
|
type = string
|
||||||
|
description = "Postgres version to use"
|
||||||
|
default = "11"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssl_enforcement" {
|
||||||
|
type = string
|
||||||
|
description = "Enforce SSL (Enabled/Disable)"
|
||||||
|
default = "Enabled"
|
||||||
|
}
|
||||||
|
|
72
terraform/modules/vpc/main.tf
Normal file
72
terraform/modules/vpc/main.tf
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
resource "azurerm_resource_group" "vpc" {
|
||||||
|
name = "${var.name}-${var.environment}-vpc"
|
||||||
|
location = var.region
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
environment = var.environment
|
||||||
|
owner = var.owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_network_ddos_protection_plan" "vpc" {
|
||||||
|
count = var.ddos_enabled
|
||||||
|
name = "${var.name}-${var.environment}-ddos"
|
||||||
|
location = azurerm_resource_group.vpc.location
|
||||||
|
resource_group_name = azurerm_resource_group.vpc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_virtual_network" "vpc" {
|
||||||
|
name = "${var.name}-${var.environment}-network"
|
||||||
|
location = azurerm_resource_group.vpc.location
|
||||||
|
resource_group_name = azurerm_resource_group.vpc.name
|
||||||
|
address_space = ["${var.virtual_network}"]
|
||||||
|
dns_servers = var.dns_servers
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
environment = var.environment
|
||||||
|
owner = var.owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_subnet" "subnet" {
|
||||||
|
for_each = var.networks
|
||||||
|
name = "${var.name}-${var.environment}-${each.key}"
|
||||||
|
resource_group_name = azurerm_resource_group.vpc.name
|
||||||
|
virtual_network_name = azurerm_virtual_network.vpc.name
|
||||||
|
address_prefix = element(split(",", each.value), 0)
|
||||||
|
|
||||||
|
# See https://github.com/terraform-providers/terraform-provider-azurerm/issues/3471
|
||||||
|
lifecycle {
|
||||||
|
ignore_changes = [route_table_id]
|
||||||
|
}
|
||||||
|
#delegation {
|
||||||
|
# name = "acctestdelegation"
|
||||||
|
#
|
||||||
|
# service_delegation {
|
||||||
|
# name = "Microsoft.ContainerInstance/containerGroups"
|
||||||
|
# actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
|
||||||
|
# }
|
||||||
|
#}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_route_table" "route_table" {
|
||||||
|
for_each = var.route_tables
|
||||||
|
name = "${var.name}-${var.environment}-${each.key}"
|
||||||
|
location = azurerm_resource_group.vpc.location
|
||||||
|
resource_group_name = azurerm_resource_group.vpc.name
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_subnet_route_table_association" "route_table" {
|
||||||
|
for_each = var.networks
|
||||||
|
subnet_id = azurerm_subnet.subnet[each.key].id
|
||||||
|
route_table_id = azurerm_route_table.route_table[each.key].id
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_route" "route" {
|
||||||
|
for_each = var.route_tables
|
||||||
|
name = "${var.name}-${var.environment}-default"
|
||||||
|
resource_group_name = azurerm_resource_group.vpc.name
|
||||||
|
route_table_name = azurerm_route_table.route_table[each.key].name
|
||||||
|
address_prefix = "0.0.0.0/0"
|
||||||
|
next_hop_type = each.value
|
||||||
|
}
|
3
terraform/modules/vpc/outputs.tf
Normal file
3
terraform/modules/vpc/outputs.tf
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
output "subnets" {
|
||||||
|
value = azurerm_subnet.subnet["private"].id #FIXME - output should be a map
|
||||||
|
}
|
43
terraform/modules/vpc/variables.tf
Normal file
43
terraform/modules/vpc/variables.tf
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
variable "environment" {
|
||||||
|
description = "Environment (Prod,Dev,etc)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
description = "Region (useast2, etc)"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
description = "Name or prefix to use for all resources created by this module"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "owner" {
|
||||||
|
description = "Owner of these resources"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ddos_enabled" {
|
||||||
|
description = "Enable or disable DDoS Protection (1,0)"
|
||||||
|
default = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "virtual_network" {
|
||||||
|
description = "The supernet used for this VPC a.k.a Virtual Network"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "networks" {
|
||||||
|
description = "A map of lists describing the network topology"
|
||||||
|
type = map
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "dns_servers" {
|
||||||
|
description = "DNS Server IPs for internal and public DNS lookups (must be on a defined subnet)"
|
||||||
|
type = list
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "route_tables" {
|
||||||
|
type = map
|
||||||
|
description = "A map with the route tables to create"
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user