diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 000000000..288064a77 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,87 @@ +name: Deploy to production environment + +on: + push: + branches: [ production ] + +permissions: + contents: read + +# deploy-prod and deploy-demo will run in parallel now. +# TODO: Research if we want to serialize them +# by moving the jobs into a single file similar to +# https://github.com/GSA/usnotify-ssb/blob/main/.github/workflows/apply.yml +jobs: + deploy: + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Check for changes to Terraform + id: changed-terraform-files + uses: tj-actions/changed-files@v34 + with: + files: | + terraform/production + terraform/shared + .github/workflows/deploy-prod.yml + - name: Terraform init + if: steps.changed-terraform-files.outputs.any_changed == 'true' + working-directory: terraform/production + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }} + run: terraform init + - name: Terraform apply + if: steps.changed-terraform-files.outputs.any_changed == 'true' + working-directory: terraform/production + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }} + TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }} + TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} + run: terraform apply -auto-approve -input=false + + - uses: ./.github/actions/setup-project + - name: Install application dependencies + run: make bootstrap + + - name: Create requirements.txt because Cloud Foundry does a weird pipenv thing + run: pipenv requirements > requirements.txt + + - name: Deploy to cloud.gov + uses: 18f/cg-deploy-action@main + env: + DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} + with: + cf_username: ${{ secrets.CLOUDGOV_USERNAME }} + cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} + cf_org: gsa-tts-benefits-studio-prototyping + cf_space: notify-production + push_arguments: >- + --vars-file deploy-config/production.yml + --var DANGEROUS_SALT="$DANGEROUS_SALT" + --var SECRET_KEY="$SECRET_KEY" + --var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET" + --var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY" + + - name: Check for changes to egress config + id: changed-egress-config + uses: tj-actions/changed-files@v34 + with: + files: | + deploy-config/egress_proxy/notify-api-production.*.acl + .github/actions/deploy-proxy/action.yml + .github/workflows/deploy-prod.yml + - name: Deploy egress proxy + if: steps.changed-egress-config.outputs.any_changed == 'true' + uses: ./.github/actions/deploy-proxy + with: + cf_space: notify-production + app: notify-api-production diff --git a/.github/workflows/drift.yml b/.github/workflows/drift.yml index 412290a49..616e72689 100644 --- a/.github/workflows/drift.yml +++ b/.github/workflows/drift.yml @@ -45,22 +45,22 @@ jobs: with: path: terraform/demo - # check_prod_drift: - # runs-on: ubuntu-latest - # name: Check for drift of production terraform configuration - # environment: production - # steps: - # - name: Checkout - # uses: actions/checkout@v3 - # with: - # ref: 'production' + check_prod_drift: + runs-on: ubuntu-latest + name: Check for drift of production terraform configuration + environment: production + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: 'production' - # - name: Check for drift - # uses: dflook/terraform-check@v1 - # env: - # AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }} - # AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }} - # TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }} - # TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} - # with: - # path: terraform/production + - name: Check for drift + uses: dflook/terraform-check@v1 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }} + TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }} + TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} + with: + path: terraform/production diff --git a/.github/workflows/terraform-production.yml b/.github/workflows/terraform-production.yml index e48000438..afb10dcfb 100644 --- a/.github/workflows/terraform-production.yml +++ b/.github/workflows/terraform-production.yml @@ -2,7 +2,7 @@ name: Run Terraform plan in production on: pull_request: - branches: [ production-disabled-for-now ] + branches: [ production ] paths: [ 'terraform/**' ] defaults: diff --git a/Makefile b/Makefile index 02cd34162..c2c83ca1f 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,10 @@ run-celery-beat: ## Run celery beat -A run_celery.notify_celery beat \ --loglevel=INFO +.PHONY: cloudgov-user-report +cloudgov-user-report: + @pipenv run python -m terraform.ops.cloudgov_user_report + .PHONY: help help: @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/Pipfile b/Pipfile index a4c395a85..e31164f97 100644 --- a/Pipfile +++ b/Pipfile @@ -79,6 +79,7 @@ jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} pip-audit = "*" bandit = "*" honcho = "*" +cloudfoundry-client = "*" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index 12d87b09e..37571da2c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cec76bad3c666e613f7b6ef7da68232b923ae8855fa9d302446ac5f9843b634f" + "sha256": "a88c2c97eeb749b0acba8d801aa89ae4544574aac78ee413eaaf1dd009dab0a6" }, "pipfile-spec": 6, "requires": { @@ -158,11 +158,11 @@ }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" ], "index": "pypi", - "version": "==2022.12.7" + "version": "==2023.5.7" }, "cffi": { "hashes": [ @@ -769,7 +769,7 @@ "notifications-utils": { "editable": true, "git": "https://github.com/GSA/notifications-utils.git", - "ref": "8a7db23114779d71d2eb5344670d38413d188c1d" + "ref": "4c0c7c7767f04bbf7dda03df0d80ff1d1861baa9" }, "numpy": { "hashes": [ @@ -1055,10 +1055,10 @@ }, "redis": { "hashes": [ - "sha256:2c19e6767c474f2e85167909061d525ed65bea9301c0770bb151e041b7ac89a2", - "sha256:73ec35da4da267d6847e47f68730fdd5f62e2ca69e3ef5885c6a78a9374c3893" + "sha256:77929bc7f5dab9adf3acba2d3bb7d7658f1e0c2f1cafe7eb36434e751c471119", + "sha256:dc87a0bdef6c8bfe1ef1e1c40be7034390c2ae02d92dcd0c7ca1729443899880" ], - "version": "==4.5.4" + "version": "==4.5.5" }, "requests": { "hashes": [ @@ -1268,11 +1268,11 @@ }, "werkzeug": { "hashes": [ - "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a", - "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091" + "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76", + "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f" ], "index": "pypi", - "version": "==2.3.3" + "version": "==2.3.4" }, "wrapt": { "hashes": [ @@ -1365,6 +1365,115 @@ } }, "develop": { + "aiohttp": { + "hashes": [ + "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14", + "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391", + "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2", + "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e", + "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9", + "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd", + "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4", + "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b", + "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41", + "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567", + "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275", + "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54", + "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a", + "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef", + "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99", + "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da", + "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4", + "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e", + "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699", + "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04", + "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719", + "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131", + "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e", + "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f", + "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd", + "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f", + "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e", + "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1", + "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed", + "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4", + "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1", + "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777", + "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531", + "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b", + "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab", + "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8", + "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074", + "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc", + "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643", + "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01", + "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36", + "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24", + "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654", + "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d", + "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241", + "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51", + "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f", + "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2", + "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15", + "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf", + "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b", + "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71", + "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05", + "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52", + "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3", + "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6", + "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a", + "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519", + "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a", + "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333", + "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6", + "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d", + "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57", + "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c", + "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9", + "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea", + "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332", + "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5", + "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622", + "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71", + "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb", + "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a", + "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff", + "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945", + "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480", + "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6", + "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9", + "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd", + "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f", + "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a", + "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a", + "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949", + "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc", + "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75", + "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f", + "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10", + "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f" + ], + "markers": "python_version >= '3.6'", + "version": "==3.8.4" + }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "async-timeout": { + "hashes": [ + "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", + "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" + ], + "index": "pypi", + "version": "==4.0.2" + }, "attrs": { "hashes": [ "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", @@ -1410,11 +1519,11 @@ }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", + "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" ], "index": "pypi", - "version": "==2022.12.7" + "version": "==2023.5.7" }, "cffi": { "hashes": [ @@ -1480,6 +1589,14 @@ "index": "pypi", "version": "==2.0.12" }, + "cloudfoundry-client": { + "hashes": [ + "sha256:1261ff57c7309406b8e8720991d861dcede23c8ee612c80f87330815623c8753", + "sha256:8293d8027e5ad5a902806603286cbab78f9639b92229fc216f798a15023c484a" + ], + "index": "pypi", + "version": "==1.34.2" + }, "coverage": { "extras": [ "toml" @@ -1613,6 +1730,86 @@ "index": "pypi", "version": "==1.2.1" }, + "frozenlist": { + "hashes": [ + "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c", + "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f", + "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a", + "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784", + "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27", + "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d", + "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3", + "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678", + "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a", + "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483", + "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8", + "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf", + "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99", + "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c", + "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48", + "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5", + "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56", + "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e", + "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1", + "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401", + "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4", + "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e", + "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649", + "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a", + "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d", + "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0", + "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6", + "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d", + "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b", + "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6", + "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf", + "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef", + "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7", + "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842", + "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba", + "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420", + "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b", + "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d", + "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332", + "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936", + "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816", + "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91", + "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420", + "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448", + "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411", + "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4", + "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32", + "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b", + "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0", + "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530", + "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669", + "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7", + "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1", + "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5", + "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce", + "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4", + "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e", + "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2", + "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d", + "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9", + "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642", + "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0", + "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703", + "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb", + "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1", + "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13", + "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab", + "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38", + "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb", + "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb", + "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81", + "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8", + "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd", + "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.3" + }, "gitdb": { "hashes": [ "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a", @@ -1857,6 +2054,93 @@ ], "version": "==1.0.5" }, + "multidict": { + "hashes": [ + "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", + "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", + "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", + "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", + "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", + "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", + "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", + "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", + "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", + "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", + "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", + "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", + "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", + "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", + "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", + "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", + "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", + "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", + "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", + "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", + "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", + "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", + "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", + "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", + "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", + "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", + "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", + "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", + "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", + "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", + "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", + "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", + "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", + "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", + "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", + "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", + "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", + "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", + "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", + "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", + "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", + "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", + "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", + "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", + "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", + "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", + "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", + "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", + "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", + "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", + "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", + "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", + "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", + "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", + "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", + "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", + "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", + "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", + "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", + "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", + "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", + "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", + "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", + "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", + "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", + "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", + "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", + "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", + "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", + "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", + "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", + "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", + "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", + "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.4" + }, + "oauth2-client": { + "hashes": [ + "sha256:5381900448ff1ae762eb7c65c501002eac46bb5ca2f49477fdfeaf9e9969f284", + "sha256:7b938ba8166128a3c4c15ad23ca0c95a2468f8e8b6069d019ebc73360c15c7ca" + ], + "version": "==1.4.2" + }, "packageurl-python": { "hashes": [ "sha256:4bad1d3ea4feb5e7a1db5ca8fb690ac9c82ab18e08d500755947b853df68817d", @@ -1921,6 +2205,32 @@ "markers": "python_version >= '3.6'", "version": "==1.0.0" }, + "polling2": { + "hashes": [ + "sha256:90b7da82cf7adbb48029724d3546af93f21ab6e592ec37c8c4619aedd010e342", + "sha256:ad86d56fbd7502f0856cac2d0109d595c18fa6c7fb12c88cee5e5d16c17286c1" + ], + "version": "==0.5.0" + }, + "protobuf": { + "hashes": [ + "sha256:03eee35b60317112a72d19c54d0bff7bc58ff12fea4cd7b018232bd99758ffdf", + "sha256:2b94bd6df92d71bd1234a2ffe7ce96ddf6d10cf637a18d6b55ad0a89fbb7fc21", + "sha256:36f5370a930cb77c8ad2f4135590c672d0d2c72d4a707c7d0058dce4b4b4a598", + "sha256:5f1eba1da2a2f3f7df469fccddef3cc060b8a16cfe3cc65961ad36b4dbcf59c5", + "sha256:6c16657d6717a0c62d5d740cb354fbad1b0d8cb811669e06fc1caa0ff4799ddd", + "sha256:6fe180b56e1169d72ecc4acbd39186339aed20af5384531b8e8979b02bbee159", + "sha256:7cb5b9a05ce52c6a782bb97de52679bd3438ff2b7460eff5da348db65650f227", + "sha256:9744e934ea5855d12191040ea198eaf704ac78665d365a89d9572e3b627c2688", + "sha256:9f5a0fbfcdcc364f3986f9ed9f8bb1328fb84114fd790423ff3d7fdb0f85c2d1", + "sha256:baca40d067dddd62141a129f244703160d278648b569e90bb0e3753067644711", + "sha256:d5a35ff54e3f62e8fc7be02bb0d2fbc212bba1a5a9cc2748090690093996f07b", + "sha256:e62fb869762b4ba18666370e2f8a18f17f8ab92dd4467295c6d38be6f8fef60b", + "sha256:ebde3a023b8e11bfa6c890ef34cd6a8b47d586f26135e86c21344fe433daf2e2" + ], + "markers": "python_version >= '3.7'", + "version": "==4.23.0" + }, "py": { "hashes": [ "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", @@ -2189,13 +2499,21 @@ ], "version": "==0.5.1" }, + "websocket-client": { + "hashes": [ + "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40", + "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e" + ], + "markers": "python_version >= '3.7'", + "version": "==1.5.1" + }, "werkzeug": { "hashes": [ - "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a", - "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091" + "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76", + "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f" ], "index": "pypi", - "version": "==2.3.3" + "version": "==2.3.4" }, "xmltodict": { "hashes": [ @@ -2204,6 +2522,86 @@ ], "markers": "python_version >= '3.4'", "version": "==0.13.0" + }, + "yarl": { + "hashes": [ + "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571", + "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3", + "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3", + "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c", + "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7", + "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04", + "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191", + "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea", + "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4", + "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4", + "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095", + "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e", + "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74", + "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef", + "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33", + "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde", + "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45", + "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf", + "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b", + "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac", + "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0", + "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528", + "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716", + "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb", + "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18", + "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72", + "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6", + "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582", + "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5", + "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368", + "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc", + "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9", + "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be", + "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a", + "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80", + "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8", + "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6", + "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417", + "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574", + "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59", + "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608", + "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82", + "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1", + "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3", + "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d", + "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8", + "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc", + "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac", + "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8", + "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955", + "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0", + "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367", + "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb", + "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a", + "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623", + "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2", + "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6", + "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7", + "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4", + "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051", + "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938", + "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8", + "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9", + "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3", + "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5", + "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9", + "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333", + "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185", + "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3", + "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560", + "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b", + "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7", + "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78", + "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.2" } } } diff --git a/app/__init__.py b/app/__init__.py index abadaa315..81e5c055a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -29,6 +29,7 @@ from werkzeug.exceptions import HTTPException as WerkzeugHTTPException from werkzeug.local import LocalProxy from app.clients import NotificationProviderClients +from app.clients.cloudwatch.aws_cloudwatch import AwsCloudwatchClient from app.clients.document_download import DocumentDownloadClient from app.clients.email.aws_ses import AwsSesClient from app.clients.email.aws_ses_stub import AwsSesStubClient @@ -55,6 +56,7 @@ notify_celery = NotifyCelery() aws_ses_client = AwsSesClient() aws_ses_stub_client = AwsSesStubClient() aws_sns_client = AwsSnsClient() +aws_cloudwatch_client = AwsCloudwatchClient() encryption = Encryption() zendesk_client = ZendeskClient() redis_store = RedisClient() @@ -96,6 +98,7 @@ def create_app(application): aws_ses_stub_client.init_app( stub_url=application.config['SES_STUB_URL'] ) + aws_cloudwatch_client.init_app(application) # If a stub url is provided for SES, then use the stub client rather than the real SES boto client email_clients = [aws_ses_stub_client] if application.config['SES_STUB_URL'] else [aws_ses_client] notification_provider_clients.init_app( diff --git a/app/celery/provider_tasks.py b/app/celery/provider_tasks.py index a274635ce..01d826ba6 100644 --- a/app/celery/provider_tasks.py +++ b/app/celery/provider_tasks.py @@ -1,7 +1,11 @@ +from datetime import datetime, timedelta +from time import time +from zoneinfo import ZoneInfo + from flask import current_app from sqlalchemy.orm.exc import NoResultFound -from app import notify_celery +from app import aws_cloudwatch_client, notify_celery from app.clients.email import EmailClientNonRetryableException from app.clients.email.aws_ses import AwsSesClientThrottlingSendRateException from app.clients.sms import SmsClientResponseException @@ -10,17 +14,51 @@ from app.dao import notifications_dao from app.dao.notifications_dao import update_notification_status_by_id from app.delivery import send_to_providers from app.exceptions import NotificationTechnicalFailureException -from app.models import NOTIFICATION_TECHNICAL_FAILURE +from app.models import ( + NOTIFICATION_FAILED, + NOTIFICATION_SENT, + NOTIFICATION_TECHNICAL_FAILURE, +) + + +@notify_celery.task(bind=True, name="check_sms_delivery_receipt", max_retries=48, default_retry_delay=300) +def check_sms_delivery_receipt(self, message_id, notification_id, sent_at): + """ + This is called after deliver_sms to check the status of the message. This uses the same number of + retries and the same delay period as deliver_sms. In addition, this fires five minutes after + deliver_sms initially. So the idea is that most messages will succeed and show up in the logs quickly. + Other message will resolve successfully after a retry or to. A few will fail but it will take up to + 4 hours to know for sure. The call to check_sms will raise an exception if neither a success nor a + failure appears in the cloudwatch logs, so this should keep retrying until the log appears, or until + we run out of retries. + """ + status, provider_response = aws_cloudwatch_client.check_sms(message_id, notification_id, sent_at) + if status == 'success': + status = NOTIFICATION_SENT + else: + status = NOTIFICATION_FAILED + update_notification_status_by_id(notification_id, status, provider_response=provider_response) + current_app.logger.info(f"Updated notification {notification_id} with response '{provider_response}'") @notify_celery.task(bind=True, name="deliver_sms", max_retries=48, default_retry_delay=300) def deliver_sms(self, notification_id): try: + # Get the time we are doing the sending, to minimize the time period we need to check over for receipt + now = round(time() * 1000) current_app.logger.info("Start sending SMS for notification id: {}".format(notification_id)) notification = notifications_dao.get_notification_by_id(notification_id) if not notification: raise NoResultFound() - send_to_providers.send_sms_to_provider(notification) + message_id = send_to_providers.send_sms_to_provider(notification) + # We have to put it in the default US/Eastern timezone. From zones west of there, the delay + # will be ignored and it will fire immediately (although this probably only affects developer testing) + my_eta = datetime.now(ZoneInfo('US/Eastern')) + timedelta(seconds=300) + check_sms_delivery_receipt.apply_async( + [message_id, notification_id, now], + eta=my_eta, + queue=QueueNames.CHECK_SMS + ) except Exception as e: if isinstance(e, SmsClientResponseException): current_app.logger.warning( diff --git a/app/clients/cloudwatch/__init__.py b/app/clients/cloudwatch/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/clients/cloudwatch/aws_cloudwatch.py b/app/clients/cloudwatch/aws_cloudwatch.py new file mode 100644 index 000000000..97de58219 --- /dev/null +++ b/app/clients/cloudwatch/aws_cloudwatch.py @@ -0,0 +1,89 @@ +import json +import re +import time + +from boto3 import client + +from app.clients import Client +from app.cloudfoundry_config import cloud_config + + +class AwsCloudwatchClient(Client): + """ + This client is responsible for retrieving sms delivery receipts from cloudwatch. + """ + + def init_app(self, current_app, *args, **kwargs): + self._client = client( + "logs", + region_name=cloud_config.sns_region, + aws_access_key_id=cloud_config.sns_access_key, + aws_secret_access_key=cloud_config.sns_secret_key + ) + super(Client, self).__init__(*args, **kwargs) + self.current_app = current_app + self._valid_sender_regex = re.compile(r"^\+?\d{5,14}$") + + @property + def name(self): + return 'cloudwatch' + + def _get_log(self, my_filter, log_group_name, sent_at): + + # Check all cloudwatch logs from the time the notification was sent (currently 5 minutes previously) until now + now = round(time.time() * 1000) + beginning = sent_at + next_token = None + all_log_events = [] + while True: + if next_token: + response = self._client.filter_log_events( + logGroupName=log_group_name, + filterPattern=my_filter, + nextToken=next_token, + startTime=beginning, + endTime=now + ) + else: + response = self._client.filter_log_events( + logGroupName=log_group_name, + filterPattern=my_filter, + startTime=beginning, + endTime=now + ) + log_events = response.get('events', []) + all_log_events.extend(log_events) + if len(log_events) > 0: + # We found it + break + next_token = response.get('nextToken') + if not next_token: + break + return all_log_events + + def check_sms(self, message_id, notification_id, created_at): + + # TODO this clumsy approach to getting the account number will be fixed as part of notify-api #258 + account_number = cloud_config.ses_domain_arn + account_number = account_number.replace('arn:aws:ses:us-west-2:', '') + account_number = account_number.split(":") + account_number = account_number[0] + + log_group_name = f'sns/us-west-2/{account_number}/DirectPublishToPhoneNumber' + filter_pattern = '{$.notification.messageId="XXXXX"}' + filter_pattern = filter_pattern.replace("XXXXX", message_id) + all_log_events = self._get_log(filter_pattern, log_group_name, created_at) + + if all_log_events and len(all_log_events) > 0: + event = all_log_events[0] + message = json.loads(event['message']) + return "success", message['delivery']['providerResponse'] + + log_group_name = f'sns/us-west-2/{account_number}/DirectPublishToPhoneNumber/Failure' + all_failed_events = self._get_log(filter_pattern, log_group_name, created_at) + if all_failed_events and len(all_failed_events) > 0: + event = all_failed_events[0] + message = json.loads(event['message']) + return "fail", message['delivery']['providerResponse'] + + raise Exception(f'No event found for message_id {message_id} notification_id {notification_id}') diff --git a/app/cloudfoundry_config.py b/app/cloudfoundry_config.py index 7fda0184d..62527c797 100644 --- a/app/cloudfoundry_config.py +++ b/app/cloudfoundry_config.py @@ -39,6 +39,15 @@ class CloudfoundryConfig: domain_arn = getenv('SES_DOMAIN_ARN', 'dev.notify.gov') return domain_arn.split('/')[-1] + # TODO remove this after notifications-api #258 + @property + def ses_domain_arn(self): + try: + domain_arn = self._ses_credentials('domain_arn') + except KeyError: + domain_arn = getenv('SES_DOMAIN_ARN', 'dev.notify.gov') + return domain_arn + @property def ses_region(self): try: diff --git a/app/config.py b/app/config.py index 4fdbb0104..cb9a25eec 100644 --- a/app/config.py +++ b/app/config.py @@ -13,6 +13,7 @@ class QueueNames(object): PRIORITY = 'priority-tasks' DATABASE = 'database-tasks' SEND_SMS = 'send-sms-tasks' + CHECK_SMS = 'check-sms_tasks' SEND_EMAIL = 'send-email-tasks' RESEARCH_MODE = 'research-mode-tasks' REPORTING = 'reporting-tasks' @@ -33,6 +34,7 @@ class QueueNames(object): QueueNames.PERIODIC, QueueNames.DATABASE, QueueNames.SEND_SMS, + QueueNames.CHECK_SMS, QueueNames.SEND_EMAIL, QueueNames.RESEARCH_MODE, QueueNames.REPORTING, diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index d560e61eb..ae8405440 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -95,7 +95,7 @@ def _update_notification_status(notification, status, provider_response=None): @autocommit -def update_notification_status_by_id(notification_id, status, sent_by=None): +def update_notification_status_by_id(notification_id, status, sent_by=None, provider_response=None): notification = Notification.query.with_for_update().filter(Notification.id == notification_id).first() if not notification: @@ -121,6 +121,8 @@ def update_notification_status_by_id(notification_id, status, sent_by=None): and not country_records_delivery(notification.phone_prefix) ): return None + if provider_response: + notification.provider_response = provider_response if not notification.sent_by and sent_by: notification.sent_by = sent_by return _update_notification_status( diff --git a/app/delivery/send_to_providers.py b/app/delivery/send_to_providers.py index db331db43..380ec7b4d 100644 --- a/app/delivery/send_to_providers.py +++ b/app/delivery/send_to_providers.py @@ -38,7 +38,7 @@ from app.serialised_models import SerialisedService, SerialisedTemplate def send_sms_to_provider(notification): service = SerialisedService.from_id(notification.service_id) - + message_id = None if not service.active: technical_failure(notification=notification) return @@ -79,7 +79,7 @@ def send_sms_to_provider(notification): 'international': notification.international, } db.session.close() # no commit needed as no changes to objects have been made above - provider.send_sms(**send_sms_kwargs) + message_id = provider.send_sms(**send_sms_kwargs) except Exception as e: notification.billable_units = template.fragment_count dao_update_notification(notification) @@ -88,6 +88,7 @@ def send_sms_to_provider(notification): else: notification.billable_units = template.fragment_count update_notification_to_sending(notification, provider) + return message_id def send_email_to_provider(notification): @@ -98,7 +99,6 @@ def send_email_to_provider(notification): return if notification.status == 'created': provider = provider_to_use(EMAIL_TYPE, False) - template_dict = SerialisedTemplate.from_id_and_service_id( template_id=notification.template_id, service_id=service.id, version=notification.template_version ).__dict__ diff --git a/app/notifications/sns_cert_validator.py b/app/notifications/sns_cert_validator.py index 1b3f7ea3d..c06d06c49 100644 --- a/app/notifications/sns_cert_validator.py +++ b/app/notifications/sns_cert_validator.py @@ -16,7 +16,7 @@ VALID_SNS_TOPICS = Config.VALID_SNS_TOPICS _signing_cert_cache = {} _cert_url_re = re.compile( - r'sns\.([a-z]{1,3}-[a-z]+-[0-9]{1,2})\.amazonaws\.com', + r'sns\.([a-z]{1,3}(?:-gov)?-[a-z]+-[0-9]{1,2})\.amazonaws\.com', ) diff --git a/deploy-config/egress_proxy/notify-api-production.allow.acl b/deploy-config/egress_proxy/notify-api-production.allow.acl new file mode 100644 index 000000000..a6e4a2f65 --- /dev/null +++ b/deploy-config/egress_proxy/notify-api-production.allow.acl @@ -0,0 +1,4 @@ +email.us-gov-west-1.amazonaws.com +sns.us-gov-west-1.amazonaws.com +gov-collector.newrelic.com +egress-proxy-notify-api-production.apps.internal diff --git a/deploy-config/egress_proxy/notify-api-production.deny.acl b/deploy-config/egress_proxy/notify-api-production.deny.acl new file mode 100644 index 000000000..e69de29bb diff --git a/deploy-config/egress_proxy/notify-api-production.deploy.acl b/deploy-config/egress_proxy/notify-api-production.deploy.acl new file mode 100644 index 000000000..e5a3a541d --- /dev/null +++ b/deploy-config/egress_proxy/notify-api-production.deploy.acl @@ -0,0 +1 @@ +Update this file to force a re-deploy of the egress proxy even when notify-api-production..acl haven't changed diff --git a/docs/deploying.md b/docs/deploying.md index 2763a57a5..916a5279f 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -1,19 +1,20 @@ # Deploying -We deploy automatically to cloud.gov for demo and staging environments. +We deploy automatically to cloud.gov for production, demo, and staging environments. Deployment to staging runs via the [base deployment action](../.github/workflows/deploy.yml) on GitHub, which pulls credentials from GitHub's secrets store in the staging environment. Deployment to demo runs via the [demo deployment action](../.github/workflows/deploy-demo.yml) on GitHub, which pulls credentials from GitHub's secrets store in the demo environment. +Deployment to production runs via the [production deployment action](../.github/workflows/deploy-prod.yml) on GitHub, which pulls credentials from GitHub's secrets store in the production environment. + The [action that we use](https://github.com/18F/cg-deploy-action) deploys using [a rolling strategy](https://docs.cloudfoundry.org/devguide/deploy-apps/rolling-deploy.html), so all deployments should have zero downtime. -The API has 2 deployment environments: +The API has 3 deployment environments: - Staging, which deploys from `main` - Demo, which deploys from `production` - -In the future, we will add a Production deploy environment, which will deploy in parallel to Demo. +- Production, which deploys from `production` Configurations for these are located in [the `deploy-config` folder](../deploy-config/). diff --git a/docs/infra-overview.md b/docs/infra-overview.md index e7dd0768e..8707f9797 100644 --- a/docs/infra-overview.md +++ b/docs/infra-overview.md @@ -102,6 +102,24 @@ We are using [New Relic](https://one.newrelic.com/nr1-core?account=3389907) for These steps are required for new cloud.gov environments. Local development borrows SES & SNS infrastructure from the `notify-staging` cloud.gov space, so these steps are not required for new developers. +### Steps to do a clean prod deploy to cloud.gov + +Steps for deploying production from scratch. These can be updated for a new cloud.gov environment by subbing out `prod` or `production` for your desired environment within the steps. + +1. Deploy API app + 1. Update `terraform-production.yml` and `deploy-prod.yml` to point to the correct space and git branch. + 1. Ensure that the `domain` module is commented out in `terraform/production/main.tf` + 1. Run CI/CD pipeline on the `production` branch by opening a PR from `main` to `production` + 1. Create any necessary DNS records (check `notify-api-ses-production` service credentials for instructions) within https://github.com/18f/dns + 1. Follow the `Steps to prepare SES` below + 1. (Optional) if using a public API route, uncomment the `domain` module and re-trigger a deploy +1. Deploy Admin app + 1. Update `terraform-production.yml` and `deploy-prod.yml` to point to the correct space and git branch. + 1. Ensure that the `api_network_route` and `domain` modules are commented out in `terraform/production/main.tf` + 1. Run CI/CD pipeline on the `production` branch by opening a PR from `main` to `production` + 1. Uncomment the `api_network_route` and `domain` modules and re-trigger a deploy + 1. Create DNS records for `domain` module within https://github.com/18f/dns + ### Steps to prepare SES 1. After the first deploy of the application with the SSB-brokered SES service completes: diff --git a/docs/run-book.md b/docs/run-book.md index f93114a99..3619a377f 100644 --- a/docs/run-book.md +++ b/docs/run-book.md @@ -19,7 +19,7 @@ that the security of the system is maintained. Operational alerts are posted to the [#pb-notify-alerts](https://gsa-tts.slack.com/archives/C04U9BGHUDB) Slack channel. Please join this channel and enable push notifications for all messages whenever you are on call. -[NewRelic](https://one.newrelic.com/) is being used for monitoring the application. +[NewRelic](https://one.newrelic.com/) is being used for monitoring the application. [NewRelic Dashboard](https://onenr.io/08wokrnrvwx) can be filtered by environment and API, Admin, or Both. [Cloud.gov Logging](https://logs.fr.cloud.gov/) is used to view and search application and platform logs. @@ -153,6 +153,7 @@ Important policies: * All users must utilize `.gov` email addresses. * Users who leave the team or otherwise have role changes must have their accounts updated to reflect the new roles required (or disabled) within 14 days. * SpaceDeployer credentials must be rotated within 14 days of anyone with SpaceDeveloper cloud.gov access leaving the team. +* A user report must be created annually (See AC-2(j)). `make cloudgov-user-report` can be used to create a full report of all cloud.gov users. ### Types of Infrastructure Users diff --git a/newrelic.ini b/newrelic.ini index 394ae3c1a..5f5fec1e7 100644 --- a/newrelic.ini +++ b/newrelic.ini @@ -216,7 +216,7 @@ app_name = us-notify-api (Demo) monitor_mode = true [newrelic:production] -app_name = us-notify-api +app_name = us-notify-api (Production) monitor_mode = true # --------------------------------------------------------------------------- diff --git a/terraform/ops/cloudgov_user_report.py b/terraform/ops/cloudgov_user_report.py new file mode 100644 index 000000000..7a26fe28c --- /dev/null +++ b/terraform/ops/cloudgov_user_report.py @@ -0,0 +1,84 @@ +from subprocess import check_output + +from cloudfoundry_client.client import CloudFoundryClient + +ORG_NAME = "gsa-tts-benefits-studio-prototyping" + + +client = CloudFoundryClient.build_from_cf_config() +org_guid = check_output(f"cf org {ORG_NAME} --guid", shell=True).decode().strip() +space_guids = list(map(lambda item: item['guid'], client.v3.spaces.list(organization_guids=org_guid))) + + +class RoleCollector: + def __init__(self): + self._map = {} + + def add(self, role): + user = role.user + if self._map.get(user.guid) is None: + self._map[user.guid] = { + "user": user, + "roles": [role] + } + else: + self._map[user.guid]["roles"].append(role) + + def print(self): + for user_roles in self._map.values(): + user = user_roles['user'] + print(f"{user.type}: {user.username} has roles:") + for role in user_roles['roles']: + if role.space: + print(f" {role.type} in {role.space.name}") + else: + print(f" {role.type}") + + +role_collector = RoleCollector() + + +class User: + def __init__(self, entity): + self.guid = entity['guid'] + self._username = entity['username'] + self._is_service_account = entity['origin'] != 'gsa.gov' + self.type = 'Bot' if self._is_service_account else 'User' + + @property + def username(self): + if self._is_service_account: + return client.v3.service_credential_bindings.get( + self._username, include="service_instance" + ).service_instance()['name'] + else: + return self._username + + +class Space: + def __init__(self, entity): + self.name = entity['name'] + + +class Role: + def __init__(self, entity): + self._fields = entity + self.type = entity['type'] + self.user = User(entity.user()) + + @property + def space(self): + try: + return Space(self._fields.space()) + except AttributeError: + return None + + +for role in map(Role, client.v3.roles.list(organization_guids=org_guid, include="user")): + role_collector.add(role) +for role in map(Role, client.v3.roles.list(space_guids=space_guids, include="user")): + role_collector.add(role) + + +if __name__ == '__main__': + role_collector.print() diff --git a/terraform/production/main.tf b/terraform/production/main.tf index afe132ad4..574ae0741 100644 --- a/terraform/production/main.tf +++ b/terraform/production/main.tf @@ -13,7 +13,7 @@ module "database" { cf_space_name = local.cf_space_name name = "${local.app_name}-rds-${local.env}" recursive_delete = local.recursive_delete - rds_plan_name = "TKTK-production-rds-plan" + rds_plan_name = "small-psql-redundant" } module "redis" { @@ -23,7 +23,7 @@ module "redis" { cf_space_name = local.cf_space_name name = "${local.app_name}-redis-${local.env}" recursive_delete = local.recursive_delete - redis_plan_name = "TKTK-production-redis-plan" + redis_plan_name = "redis-3node-large" } module "csv_upload_bucket" { @@ -72,9 +72,10 @@ module "sns_sms" { ########################################################################### # The following lines need to be commented out for the initial `terraform apply` # It can be re-enabled after: +# TODO: decide on public API domain name # 1) the app has first been deployed # 2) the route has been manually created by an OrgManager: -# `cf create-domain TKTK-org-name TKTK-production-domain-name` +# `cf create-domain gsa-tts-benefits-studio-prototyping api.notify.gov` ########################################################################### # module "domain" { # source = "github.com/18f/terraform-cloudgov//domain?ref=v0.2.0" @@ -85,5 +86,5 @@ module "sns_sms" { # name = "${local.app_name}-domain-${local.env}" # recursive_delete = local.recursive_delete # cdn_plan_name = "domain" -# domain_name = "TKTK-production-domain-name" +# domain_name = "api.notify.gov" # } diff --git a/tests/app/celery/test_provider_tasks.py b/tests/app/celery/test_provider_tasks.py index 2f241bc24..d4a9070bf 100644 --- a/tests/app/celery/test_provider_tasks.py +++ b/tests/app/celery/test_provider_tasks.py @@ -23,6 +23,7 @@ def test_should_call_send_sms_to_provider_from_deliver_sms_task( sample_notification, mocker): mocker.patch('app.delivery.send_to_providers.send_sms_to_provider') + mocker.patch('app.celery.provider_tasks.check_sms_delivery_receipt') deliver_sms(sample_notification.id) app.delivery.send_to_providers.send_sms_to_provider.assert_called_with(sample_notification) diff --git a/tests/app/clients/test_aws_cloudwatch.py b/tests/app/clients/test_aws_cloudwatch.py new file mode 100644 index 000000000..5a54383b5 --- /dev/null +++ b/tests/app/clients/test_aws_cloudwatch.py @@ -0,0 +1,87 @@ +import pytest +from flask import current_app + +from app import aws_cloudwatch_client + + +def test_check_sms_no_event_error_condition(notify_api, mocker): + boto_mock = mocker.patch.object(aws_cloudwatch_client, '_client', create=True) + # TODO + # we do this to get the AWS account number, and it seems like unit tests locally have + # access to the env variables but when we push the PR they do not. Is there a better way to get it? + mocker.patch.dict('os.environ', {"SES_DOMAIN_ARN": "1111:"}) + message_id = 'aaa' + notification_id = 'bbb' + boto_mock.filter_log_events.return_value = [] + with notify_api.app_context(): + aws_cloudwatch_client.init_app(current_app) + with pytest.raises(Exception): + aws_cloudwatch_client.check_sms(message_id, notification_id) + + +def side_effect(filterPattern, logGroupName, startTime, endTime): + if "Failure" in logGroupName and 'fail' in filterPattern: + return { + "events": + [ + { + 'logStreamName': '89db9712-c6d1-49f9-be7c-4caa7ed9efb1', + 'message': '{"delivery":{"destination":"+1661","providerResponse":"Invalid phone number"}}', + 'eventId': '37535432778099870001723210579798865345508698025292922880' + } + ] + } + + elif 'succeed' in filterPattern: + return { + "events": + [ + { + 'logStreamName': '89db9712-c6d1-49f9-be7c-4caa7ed9efb1', + 'timestamp': 1683147017911, + 'message': '{"delivery":{"destination":"+1661","providerResponse":"Phone accepted msg"}}', + 'ingestionTime': 1683147018026, + 'eventId': '37535432778099870001723210579798865345508698025292922880' + } + ] + } + else: + return {"events": []} + + +def test_check_sms_success(notify_api, mocker): + aws_cloudwatch_client.init_app(current_app) + boto_mock = mocker.patch.object(aws_cloudwatch_client, '_client', create=True) + boto_mock.filter_log_events.side_effect = side_effect + mocker.patch.dict('os.environ', {"SES_DOMAIN_ARN": "1111:"}) + + message_id = 'succeed' + notification_id = 'ccc' + with notify_api.app_context(): + aws_cloudwatch_client.check_sms(message_id, notification_id, 1000000000000) + + # We check the 'success' log group first and if we find the message_id, we are done, so there is only 1 call + assert boto_mock.filter_log_events.call_count == 1 + mock_call = str(boto_mock.filter_log_events.mock_calls[0]) + assert 'Failure' not in mock_call + assert 'succeed' in mock_call + assert 'notification.messageId' in mock_call + + +def test_check_sms_failure(notify_api, mocker): + aws_cloudwatch_client.init_app(current_app) + boto_mock = mocker.patch.object(aws_cloudwatch_client, '_client', create=True) + boto_mock.filter_log_events.side_effect = side_effect + mocker.patch.dict('os.environ', {"SES_DOMAIN_ARN": "1111:"}) + + message_id = 'fail' + notification_id = 'bbb' + with notify_api.app_context(): + aws_cloudwatch_client.check_sms(message_id, notification_id, 1000000000000) + + # We check the 'success' log group and find nothing, so we then check the 'fail' log group -- two calls. + assert boto_mock.filter_log_events.call_count == 2 + mock_call = str(boto_mock.filter_log_events.mock_calls[1]) + assert 'Failure' in mock_call + assert 'fail' in mock_call + assert 'notification.messageId' in mock_call diff --git a/tests/app/test_config.py b/tests/app/test_config.py index fe2fef296..23d67aafa 100644 --- a/tests/app/test_config.py +++ b/tests/app/test_config.py @@ -4,12 +4,13 @@ from app.config import QueueNames def test_queue_names_all_queues_correct(): # Need to ensure that all_queues() only returns queue names used in API queues = QueueNames.all_queues() - assert len(queues) == 15 + assert len(queues) == 16 assert set([ QueueNames.PRIORITY, QueueNames.PERIODIC, QueueNames.DATABASE, QueueNames.SEND_SMS, + QueueNames.CHECK_SMS, QueueNames.SEND_EMAIL, QueueNames.RESEARCH_MODE, QueueNames.REPORTING,