From 70a6220b1c330d86b778d8346e8dcc08d7df79bd Mon Sep 17 00:00:00 2001 From: Henning Jacobs Date: Fri, 20 Dec 2019 16:06:55 +0100 Subject: [PATCH] poetry & black (#121) * use poetry instead of Pipenv * format code with black * install poetry on Travis * add flake8 config to ignore conflicts with black --- .flake8 | 2 + .travis.yml | 4 +- Dockerfile | 17 +- Makefile | 20 +- Pipfile | 29 - Pipfile.lock | 572 -------------- kube_resource_report/__init__.py | 2 +- kube_resource_report/cluster_discovery.py | 45 +- kube_resource_report/filters.py | 13 +- kube_resource_report/main.py | 8 +- kube_resource_report/output.py | 3 +- kube_resource_report/pricing.py | 109 ++- kube_resource_report/report.py | 379 ++++++--- pipenv-install.py | 15 - poetry.lock | 917 ++++++++++++++++++++++ pyproject.toml | 25 + tests/test_ema.py | 13 +- tests/test_report.py | 179 +++-- 18 files changed, 1452 insertions(+), 900 deletions(-) create mode 100644 .flake8 delete mode 100644 Pipfile delete mode 100644 Pipfile.lock delete mode 100755 pipenv-install.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c37a7bb --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore=E203,E722,W503 diff --git a/.travis.yml b/.travis.yml index 1dd6bb2..38ea826 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ python: services: - docker install: - - pip install pipenv - - pipenv install --dev + - pip3 install poetry + - make install script: - make test docker after_success: diff --git a/Dockerfile b/Dockerfile index 95f1947..a850090 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,20 @@ -FROM python:3.7-alpine3.10 +FROM python:3.8-slim WORKDIR / -COPY Pipfile.lock / -COPY pipenv-install.py / +RUN pip3 install poetry -RUN /pipenv-install.py && \ - rm -fr /usr/local/lib/python3.7/site-packages/pip && \ - rm -fr /usr/local/lib/python3.7/site-packages/setuptools +COPY poetry.lock / +COPY pyproject.toml / -FROM python:3.7-alpine3.10 +RUN poetry config virtualenvs.create false && \ + poetry install --no-interaction --no-dev --no-ansi + +FROM python:3.8-slim WORKDIR / -COPY --from=0 /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages +COPY --from=0 /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages COPY kube_resource_report /kube_resource_report diff --git a/Makefile b/Makefile index 9bfe4fd..5c2833c 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,20 @@ TAG ?= $(VERSION) default: docker -test: - pipenv run flake8 - pipenv run mypy --ignore-missing-imports kube_resource_report/ - pipenv run coverage run --source=kube_resource_report -m py.test - pipenv run coverage report +.PHONY: +install: + poetry install + +.PHONY: +lint: install + poetry run black --check kube_resource_report tests + poetry run flake8 + poetry run mypy --ignore-missing-imports kube_resource_report/ + +.PHONY: +test: install lint + poetry run coverage run --source=kube_resource_report -m py.test + poetry run coverage report docker: docker build --build-arg "VERSION=$(VERSION)" -t "$(IMAGE):$(TAG)" . @@ -23,6 +32,7 @@ push: docker .PHONY: version version: + poetry version $(VERSION) sed -i 's,$(IMAGE):[0-9.]*,$(IMAGE):$(TAG),g' README.rst deploy/*.yaml sed -i 's,version: v[0-9.]*,version: v$(VERSION),g' deploy/*.yaml sed -i 's,tag: "[0-9.]*",tag: "$(VERSION)",g' chart/*/values.yaml diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d233f8a..0000000 --- a/Pipfile +++ /dev/null @@ -1,29 +0,0 @@ -[[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -requests = "*" -click = "*" -stups-tokens = "*" -"jinja2" = "*" -requests-futures = "*" -pykube-ng = "*" - -[dev-packages] -flake8 = "*" -pytest = "*" -pytest-watch = "*" -black = "*" -"boto3" = "*" -pytest-cov = "*" -coveralls = "*" -coverage = "*" -mypy = "*" - -[requires] -python_version = "3.7" - -[pipenv] -allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index d46f831..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,572 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "003167fda258a9c54c419bae7dc094cf3a9f98ce775ccfc114d2e818221babc4" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" - ], - "version": "==2019.11.28" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "index": "pypi", - "version": "==7.0" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "jinja2": { - "hashes": [ - "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", - "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" - ], - "index": "pypi", - "version": "==2.10.3" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" - ], - "version": "==1.1.1" - }, - "pykube-ng": { - "hashes": [ - "sha256:440b4183719e673c11b7cd68669d3ba0b710c192834d16bd7766dfb6df9737b2", - "sha256:bd872f0e6ad4a58cc6cb005a9d15decaba1363efc7d52ee75a64d16a5e986b87" - ], - "index": "pypi", - "version": "==19.10.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" - ], - "version": "==5.2" - }, - "requests": { - "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" - ], - "index": "pypi", - "version": "==2.22.0" - }, - "requests-futures": { - "hashes": [ - "sha256:35547502bf1958044716a03a2f47092a89efe8f9789ab0c4c528d9c9c30bc148" - ], - "index": "pypi", - "version": "==1.0.0" - }, - "stups-tokens": { - "hashes": [ - "sha256:317f4386763bac9dd5c0a4c8b0f9f0238dc3fa81de3c6fd1971b6b01662b5750", - "sha256:7830ad83ccbfd52a9734608ffcefcca917137ce9480cc91a4fbd321a4aca3160" - ], - "index": "pypi", - "version": "==1.1.19" - }, - "urllib3": { - "hashes": [ - "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", - "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" - ], - "version": "==1.25.7" - } - }, - "develop": { - "appdirs": { - "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" - ], - "version": "==1.4.3" - }, - "argh": { - "hashes": [ - "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", - "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" - ], - "version": "==0.26.2" - }, - "attrs": { - "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" - ], - "version": "==19.3.0" - }, - "black": { - "hashes": [ - "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", - "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" - ], - "index": "pypi", - "version": "==19.10b0" - }, - "boto3": { - "hashes": [ - "sha256:5db4db12a017be2a0b07ec662584b7b9e8afa05894c8aaac145576a7c39a9886", - "sha256:7fb8bf70ff2403991c8ae7bc548333811be6e432c7665721364ea0c858eb824e" - ], - "index": "pypi", - "version": "==1.10.41" - }, - "botocore": { - "hashes": [ - "sha256:5bfffa38ebba26ab462bb40e858702390fbe3ae2093a2177a8cde050ad6cb7e3", - "sha256:62ddff63be904781f97ced737836a66f5b72579af788c905cfdab32d2970e15e" - ], - "version": "==1.13.41" - }, - "certifi": { - "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" - ], - "version": "==2019.11.28" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "index": "pypi", - "version": "==7.0" - }, - "colorama": { - "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" - ], - "version": "==0.4.3" - }, - "coverage": { - "hashes": [ - "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", - "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", - "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", - "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", - "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", - "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", - "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", - "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", - "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", - "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", - "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", - "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", - "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", - "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", - "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", - "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", - "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", - "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", - "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", - "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", - "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", - "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", - "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", - "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", - "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", - "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", - "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", - "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", - "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", - "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", - "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", - "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" - ], - "index": "pypi", - "version": "==4.5.4" - }, - "coveralls": { - "hashes": [ - "sha256:25522a50cdf720d956601ca6ef480786e655ae2f0c94270c77e1a23d742de558", - "sha256:8e3315e8620bb6b3c6f3179a75f498e7179c93b3ddc440352404f941b1f70524" - ], - "index": "pypi", - "version": "==1.9.2" - }, - "docopt": { - "hashes": [ - "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" - ], - "version": "==0.6.2" - }, - "docutils": { - "hashes": [ - "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", - "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", - "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" - ], - "version": "==0.15.2" - }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" - }, - "flake8": { - "hashes": [ - "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", - "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" - ], - "index": "pypi", - "version": "==3.7.9" - }, - "idna": { - "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" - ], - "version": "==2.8" - }, - "importlib-metadata": { - "hashes": [ - "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", - "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f" - ], - "markers": "python_version < '3.8'", - "version": "==1.3.0" - }, - "jmespath": { - "hashes": [ - "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", - "sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c" - ], - "version": "==0.9.4" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "more-itertools": { - "hashes": [ - "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", - "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564" - ], - "version": "==8.0.2" - }, - "mypy": { - "hashes": [ - "sha256:0308c35fd16c96a81b8dfc4d09ec63b8fa607cfec087acf5aafb44c2c45197de", - "sha256:39f7be2f89668d21b2bbab45ce5aa15e69bf8d6f3b46f9e1cc1a88e4fcc84f3d", - "sha256:4223f576813c79a10d0fd14192c86f1b85e3bd235c93792f22ed811a20b5ee4e", - "sha256:4c8f812a2fbefa96185933fbe05aa035e9cf791cf3a23bbdb6a219c80b60e0b1", - "sha256:4ea9ee847ea5bb38ea275441f3aea7eeba1b96187a3f968ee359d33d9dcc0eda", - "sha256:573c68df69f0e399fa57866a0b72989acf0a56c4008eee59c789c2ca5ea9df03", - "sha256:588c0e38466306aa7dbe6522ceacf37dde8b13cfa5edde90be2ce382f078875f", - "sha256:6d1bd2e675823a19e6bf72149540ab9851bfe698b796aea698fb926ab2bedd02", - "sha256:aa8e3bd1540dd5c39ef580ec2146a9c99c45f7c62af890095fec9e87b5ca19fb", - "sha256:b978ba1ea90d0abe2fc720ec9a41824b7d3a1304569bd58c9038d8d61dc4dfdb", - "sha256:c85c5367c2e8247e06cc0aba84e3633e90f48e8a0677bc51b351e138b5ff80b1", - "sha256:ce69577b424058bfa177df27213869f37c1e964c3e1ebd3b3d54f1d10b234c4d", - "sha256:ec6eaf98a57624d96d9916352a5bad2d73959f6358fabf43838f7d1a4d2f8389" - ], - "index": "pypi", - "version": "==0.760" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", - "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" - ], - "version": "==19.2" - }, - "pathspec": { - "hashes": [ - "sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c" - ], - "version": "==0.6.0" - }, - "pathtools": { - "hashes": [ - "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0" - ], - "version": "==0.1.2" - }, - "pluggy": { - "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" - ], - "version": "==0.13.1" - }, - "py": { - "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" - ], - "version": "==1.8.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" - ], - "version": "==2.5.0" - }, - "pyflakes": { - "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" - ], - "version": "==2.1.1" - }, - "pyparsing": { - "hashes": [ - "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f", - "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a" - ], - "version": "==2.4.5" - }, - "pytest": { - "hashes": [ - "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", - "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" - ], - "index": "pypi", - "version": "==5.3.2" - }, - "pytest-cov": { - "hashes": [ - "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", - "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626" - ], - "index": "pypi", - "version": "==2.8.1" - }, - "pytest-watch": { - "hashes": [ - "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9" - ], - "index": "pypi", - "version": "==4.2.0" - }, - "python-dateutil": { - "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" - ], - "markers": "python_version >= '2.7'", - "version": "==2.8.0" - }, - "pyyaml": { - "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" - ], - "version": "==5.2" - }, - "regex": { - "hashes": [ - "sha256:0472acc4b6319801c1bc681d838c88ba1446f9ae199e01f6e41091c701fb3d42", - "sha256:16709434c4e2332ee8ba26ae339aceb8ab0b24b8398ebd0f52ebc943f45c4fc2", - "sha256:223fb63ec8dcab20b3318e93dcec4aee89e98b062934090bf29ffc374d2000a2", - "sha256:23c3ebf05d1cd3adb26723fd598e75724e0cdb7d6a35185ac0caf061cc6edb49", - "sha256:2404a50fb48badaf214b700f08822b68d93d79200e0aefd9569d0332d21fbfcb", - "sha256:2af3a7a16fed6eff85c25da106effa36f61cbbe801d00ade349b53ce7619eb15", - "sha256:37e018d3746baf159aedfc9773c3cafacbd10d354ba15484f5cfc8ed9da5748b", - "sha256:3c9c2988d02a9238a1975c70e87c6ce94e6f36dd8e372b66f468990cfe077434", - "sha256:47298bc8b89d1c747f0f5974aa528fc0b6b17396f1694136a224d51461279d83", - "sha256:4eeb0fe936797ae00a085f99802642bfc722b3b4ea557e9e7849cb621ea10c91", - "sha256:6881be0218b47ed76db033f252bab3f912dfe7ed1fe7baa9daebf51de08546a0", - "sha256:7ac08cee5055f548eed3889e9aaef15fd00172d037949496f1f0b34acb8a7c3e", - "sha256:7c5e2efcf079c35ff266c3f3a6708834f88f9fd04a3c16b855e036b2b7b1b543", - "sha256:8355eaa64724a0fdb010a1654b77cb3e375dc08b7f592cc4a1c05ac606aa481c", - "sha256:999a885f7f5194464238ad5d74b05982acee54002f3aa775d8e0e8c5fb74c06c", - "sha256:9fd2f4813eaa3e421e82819d38e5b634d900faff7ae5a80cd89ccff407175e69", - "sha256:a2e1e53df7dd27943da2b512895125b33fb20f81862c9fed7b3bab2a1de684d1", - "sha256:ab43bc0836820b7900dfffc025b996784aec26ec87dc1df4f95a40398760223f", - "sha256:ba449b56fa419fb19bf2a2438adbd2433f27087a6fe115917eaf9cfca684d5b6", - "sha256:d3f632cefad2cf247bd845794002585e3772288bfcb0dbac59fdecd32cd38b67", - "sha256:d51311496061863caae2cfe120cf1ef37900019b86c89c2d75f0918e0b4b8bf3" - ], - "version": "==2019.12.19" - }, - "requests": { - "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" - ], - "index": "pypi", - "version": "==2.22.0" - }, - "s3transfer": { - "hashes": [ - "sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d", - "sha256:b780f2411b824cb541dbcd2c713d0cb61c7d1bcadae204cdddda2b35cef493ba" - ], - "version": "==0.2.1" - }, - "six": { - "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" - ], - "version": "==1.13.0" - }, - "toml": { - "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" - ], - "version": "==0.10.0" - }, - "typed-ast": { - "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" - ], - "version": "==1.4.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", - "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", - "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" - ], - "version": "==3.7.4.1" - }, - "urllib3": { - "hashes": [ - "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", - "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" - ], - "version": "==1.25.7" - }, - "watchdog": { - "hashes": [ - "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" - ], - "version": "==0.9.0" - }, - "wcwidth": { - "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" - ], - "version": "==0.1.7" - }, - "zipp": { - "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" - ], - "version": "==0.6.0" - } - } -} diff --git a/kube_resource_report/__init__.py b/kube_resource_report/__init__.py index 5ccfc47..5d8c6fb 100644 --- a/kube_resource_report/__init__.py +++ b/kube_resource_report/__init__.py @@ -1 +1 @@ -__version__ = '0.1-3-g00a7914-dirty' +__version__ = "0.1-3-g00a7914-dirty" diff --git a/kube_resource_report/cluster_discovery.py b/kube_resource_report/cluster_discovery.py index ab50533..c52aae7 100644 --- a/kube_resource_report/cluster_discovery.py +++ b/kube_resource_report/cluster_discovery.py @@ -25,7 +25,7 @@ def generate_cluster_id(url: str): """Generate some "cluster ID" from given API server URL""" for prefix in ("https://", "http://"): if url.startswith(prefix): - url = url[len(prefix):] + url = url[len(prefix) :] return CLUSTER_ID_INVALID_CHARS.sub("-", url.lower()).strip("-") @@ -44,13 +44,7 @@ def __call__(self, request): class Cluster: - def __init__( - self, - id: str, - name: str, - api_server_url: str, - client: HTTPClient - ): + def __init__(self, id: str, name: str, api_server_url: str, client: HTTPClient): self.id = id self.name = name self.api_server_url = api_server_url @@ -70,15 +64,18 @@ def __init__(self, api_server_urls: list): config = KubeConfig.from_url(DEFAULT_CLUSTERS) client = HTTPClient(config) cluster = Cluster( - generate_cluster_id(DEFAULT_CLUSTERS), "cluster", DEFAULT_CLUSTERS, client + generate_cluster_id(DEFAULT_CLUSTERS), + "cluster", + DEFAULT_CLUSTERS, + client, ) else: client = HTTPClient(config) cluster = Cluster( - generate_cluster_id(config.cluster['server']), + generate_cluster_id(config.cluster["server"]), "cluster", - config.cluster['server'], - client + config.cluster["server"], + client, ) self._clusters.append(cluster) else: @@ -86,7 +83,9 @@ def __init__(self, api_server_urls: list): config = KubeConfig.from_url(api_server_url) client = HTTPClient(config) generated_id = generate_cluster_id(api_server_url) - self._clusters.append(Cluster(generated_id, generated_id, api_server_url, client)) + self._clusters.append( + Cluster(generated_id, generated_id, api_server_url, client) + ) def get_clusters(self): return self._clusters @@ -111,23 +110,16 @@ def refresh(self): for row in response.json()["items"]: # only consider "ready" clusters if row.get("lifecycle_status", "ready") == "ready": - config = KubeConfig.from_url(row['api_server_url']) + config = KubeConfig.from_url(row["api_server_url"]) client = HTTPClient(config) client.session.auth = OAuthTokenAuth("read-only") clusters.append( - Cluster( - row["id"], - row["alias"], - row["api_server_url"], - client - ) + Cluster(row["id"], row["alias"], row["api_server_url"], client) ) self._clusters = clusters self._last_cache_refresh = time.time() except: - logger.exception( - f"Failed to refresh from cluster registry {self._url}" - ) + logger.exception(f"Failed to refresh from cluster registry {self._url}") def get_clusters(self): now = time.time() @@ -153,10 +145,7 @@ def get_clusters(self): context_config = KubeConfig(config.doc, context) client = HTTPClient(context_config) cluster = Cluster( - context, - context, - context_config.cluster['server'], - client + context, context, context_config.cluster["server"], client ) yield cluster @@ -168,5 +157,5 @@ def get_clusters(self): f"mock-cluster-{i}", f"mock-cluster-{i}", api_server_url=f"https://kube-{i}.example.org", - client=None + client=None, ) diff --git a/kube_resource_report/filters.py b/kube_resource_report/filters.py index f5a6c0c..7220374 100644 --- a/kube_resource_report/filters.py +++ b/kube_resource_report/filters.py @@ -1,16 +1,15 @@ - def money(value): - return '{:,.02f}'.format(value) + return "{:,.02f}".format(value) def cpu(value): - return '{:,.01f}'.format(value) + return "{:,.01f}".format(value) def memory(value, fmt): - if fmt == 'GiB': - return '{:,.01f}'.format(value / (1024**3)) - elif fmt == 'MiB': - return '{:,.0f}'.format(value / (1024**2)) + if fmt == "GiB": + return "{:,.01f}".format(value / (1024 ** 3)) + elif fmt == "MiB": + return "{:,.0f}".format(value / (1024 ** 2)) else: return value diff --git a/kube_resource_report/main.py b/kube_resource_report/main.py index b7ff70a..62c9971 100644 --- a/kube_resource_report/main.py +++ b/kube_resource_report/main.py @@ -100,12 +100,12 @@ def convert(self, value, param, ctx): @click.option( "--pricing-file", type=click.Path(exists=True), - help="Path to alternate pricing file" + help="Path to alternate pricing file", ) @click.option( "--links-file", type=click.Path(exists=True), - help="Path to YAML file defining custom links for resources" + help="Path to YAML file defining custom links for resources", ) @click.option( "--node-labels", @@ -113,9 +113,7 @@ def convert(self, value, param, ctx): help="Values for the kubernetes.io/role label (e.g. 'worker' if nodes are labeled kubernetes.io/role=worker)", default="worker", ) -@click.option( - "--debug", is_flag=True, help="Enable debug logging" -) +@click.option("--debug", is_flag=True, help="Enable debug logging") @click.argument("output_dir", type=click.Path(exists=True)) def main( clusters, diff --git a/kube_resource_report/output.py b/kube_resource_report/output.py index 7ef5bd5..3321660 100644 --- a/kube_resource_report/output.py +++ b/kube_resource_report/output.py @@ -12,7 +12,6 @@ class OutputManager: - def __init__(self, output_path: Path): self.output_path = output_path self.written_paths: set = set() @@ -21,7 +20,7 @@ def __init__(self, output_path: Path): loader=FileSystemLoader(str(TEMPLATES_PATH)), autoescape=select_autoescape(["html", "xml"]), trim_blocks=True, - lstrip_blocks=True + lstrip_blocks=True, ) env.filters["money"] = filters.money env.filters["cpu"] = filters.cpu diff --git a/kube_resource_report/pricing.py b/kube_resource_report/pricing.py index cadd0f1..437583e 100755 --- a/kube_resource_report/pricing.py +++ b/kube_resource_report/pricing.py @@ -58,9 +58,9 @@ def get_node_cost(region, instance_type, is_spot, cpu, memory): else: cost = NODE_COSTS_MONTHLY.get((region, instance_type)) - if cost is None and instance_type.startswith('custom-'): - per_cpu = NODE_COSTS_MONTHLY.get((region, 'custom-per-cpu-core')) - per_memory = NODE_COSTS_MONTHLY.get((region, 'custom-per-memory-gib')) + if cost is None and instance_type.startswith("custom-"): + per_cpu = NODE_COSTS_MONTHLY.get((region, "custom-per-cpu-core")) + per_memory = NODE_COSTS_MONTHLY.get((region, "custom-per-memory-gib")) if per_cpu and per_memory: cost = (cpu * per_cpu) + (memory / ONE_GIBI * per_memory) @@ -73,39 +73,51 @@ def get_node_cost(region, instance_type, is_spot, cpu, memory): def generate_gcp_price_list(): import requests - response = requests.get('https://cloudpricingcalculator.appspot.com/static/data/pricelist.json?v=1563953523378') - prefix = 'CP-COMPUTEENGINE-VMIMAGE-' - custom_prefix = 'CP-COMPUTEENGINE-CUSTOM-VM-' + response = requests.get( + "https://cloudpricingcalculator.appspot.com/static/data/pricelist.json?v=1563953523378" + ) + prefix = "CP-COMPUTEENGINE-VMIMAGE-" + custom_prefix = "CP-COMPUTEENGINE-CUSTOM-VM-" with _path_gcp.open("w") as fd: writer = csv.writer(fd, lineterminator="\n") - for product, data in sorted(response.json()['gcp_price_list'].items()): + for product, data in sorted(response.json()["gcp_price_list"].items()): if product.startswith(prefix): - instance_type = product[len(prefix):].lower() + instance_type = product[len(prefix) :].lower() for region, hourly_price in sorted(data.items()): - if '-' in region and isinstance(hourly_price, float): + if "-" in region and isinstance(hourly_price, float): monthly_price = hourly_price * 24 * AVG_DAYS_PER_MONTH - writer.writerow([region, instance_type, "{:.4f}".format(monthly_price), data.get('cores'), data.get('memory')]) + writer.writerow( + [ + region, + instance_type, + "{:.4f}".format(monthly_price), + data.get("cores"), + data.get("memory"), + ] + ) elif product.startswith(custom_prefix): - _type = product[len(custom_prefix):].lower() - if _type == 'core': - instance_type = 'custom-per-cpu-core' - elif _type == 'ram': + _type = product[len(custom_prefix) :].lower() + if _type == "core": + instance_type = "custom-per-cpu-core" + elif _type == "ram": # note: GCP prices are per GiB (2^30 bytes) # https://cloud.google.com/compute/all-pricing - instance_type = 'custom-per-memory-gib' - elif _type == 'core-preemptible': - instance_type = 'custom-preemptible-per-cpu-core' - elif _type == 'ram-preemptible': - instance_type = 'custom-preemptible-per-memory-gib' + instance_type = "custom-per-memory-gib" + elif _type == "core-preemptible": + instance_type = "custom-preemptible-per-cpu-core" + elif _type == "ram-preemptible": + instance_type = "custom-preemptible-per-memory-gib" else: instance_type = None if instance_type: for region, hourly_price in sorted(data.items()): - if '-' in region and isinstance(hourly_price, float): + if "-" in region and isinstance(hourly_price, float): monthly_price = hourly_price * 24 * AVG_DAYS_PER_MONTH - writer.writerow([region, instance_type, "{:.4f}".format(monthly_price)]) + writer.writerow( + [region, instance_type, "{:.4f}".format(monthly_price)] + ) def generate_ec2_price_list(): @@ -145,7 +157,14 @@ def generate_ec2_price_list(): max_price = {} for location in sorted(LOCATIONS.values()): # some regions are not available - if location in ("ap-northeast-3", "cn-north-1", "cn-northwest-1", "us-gov-west-1", "us-gov-east-1", "ap-east-1"): + if location in ( + "ap-northeast-3", + "cn-north-1", + "cn-northwest-1", + "us-gov-west-1", + "us-gov-east-1", + "ap-east-1", + ): continue print(location) ec2 = boto3.client("ec2", location) @@ -153,29 +172,39 @@ def generate_ec2_price_list(): today = datetime.date.today() start = today - datetime.timedelta(days=3) - instance_types_required = set([x[1] for x in NODE_COSTS_MONTHLY.keys() if x[0] == location]) + instance_types_required = set( + [x[1] for x in NODE_COSTS_MONTHLY.keys() if x[0] == location] + ) # instances not available as Spot.. - instance_types_required -= set(['hs1.8xlarge', 't2.nano']) + instance_types_required -= set(["hs1.8xlarge", "t2.nano"]) instance_types_seen = set() next_token = "" i = 0 while next_token is not None: - data = ec2.describe_spot_price_history(Filters=[{"Name": "product-description", "Values": ["Linux/UNIX"]}], - StartTime=start.isoformat(), EndTime=today.isoformat(), NextToken=next_token) - print('. ', end='') + data = ec2.describe_spot_price_history( + Filters=[{"Name": "product-description", "Values": ["Linux/UNIX"]}], + StartTime=start.isoformat(), + EndTime=today.isoformat(), + NextToken=next_token, + ) + print(". ", end="") for entry in data["SpotPriceHistory"]: print(entry) instance_type = entry["InstanceType"] instance_types_seen.add(instance_type) price = float(entry["SpotPrice"]) monthly_price = price * 24 * AVG_DAYS_PER_MONTH - max_price[(location, instance_type)] = max(max_price.get((location, instance_type), 0), monthly_price) + max_price[(location, instance_type)] = max( + max_price.get((location, instance_type), 0), monthly_price + ) i += 1 if instance_types_seen >= instance_types_required or i > 4: next_token = None else: - print(f"Waiting to see instance types {instance_types_required - instance_types_seen}") + print( + f"Waiting to see instance types {instance_types_required - instance_types_seen}" + ) next_token = data.get("NextToken") with _spot_path.open("w") as fd: @@ -198,7 +227,7 @@ def generate_ec2_price_list(): data = pricing.get_products( ServiceCode="AmazonEC2", Filters=filters, NextToken=next_token ) - print('. ', end='') + print(". ", end="") for entry in data["PriceList"]: entry = json.loads(entry) tenancy = entry["product"]["attributes"].get("tenancy") @@ -208,17 +237,31 @@ def generate_ec2_price_list(): sw = entry["product"]["attributes"].get("preInstalledSw", "") usagetype = entry["product"]["attributes"]["usagetype"] - if tenancy == "Shared" and os == "Linux" and sw == "NA" and "BoxUsage:" in usagetype: + if ( + tenancy == "Shared" + and os == "Linux" + and sw == "NA" + and "BoxUsage:" in usagetype + ): for k, v in entry["terms"]["OnDemand"].items(): for k_, v_ in v["priceDimensions"].items(): if v_["unit"] == "Hrs": price = float(v_["pricePerUnit"]["USD"]) monthly_price = price * 24 * AVG_DAYS_PER_MONTH - instance_type = entry["product"]["attributes"]["instanceType"] + instance_type = entry["product"]["attributes"][ + "instanceType" + ] key = (location, instance_type) if key in pricing_data: - raise Exception("Duplicate data for {}/{}: {:.4f} and {:.4f}".format(location, instance_type, pricing_data[key], monthly_price)) + raise Exception( + "Duplicate data for {}/{}: {:.4f} and {:.4f}".format( + location, + instance_type, + pricing_data[key], + monthly_price, + ) + ) pricing_data[key] = monthly_price next_token = data.get("NextToken") diff --git a/kube_resource_report/report.py b/kube_resource_report/report.py index 3b70e05..ebaca22 100755 --- a/kube_resource_report/report.py +++ b/kube_resource_report/report.py @@ -29,12 +29,20 @@ NODE_LABEL_SPOT = os.environ.get("NODE_LABEL_SPOT", "aws.amazon.com/spot") NODE_LABEL_ROLE = os.environ.get("NODE_LABEL_ROLE", "kubernetes.io/role") # the following labels are used by both AWS and GKE -NODE_LABEL_REGION = os.environ.get("NODE_LABEL_REGION", "failure-domain.beta.kubernetes.io/region") -NODE_LABEL_INSTANCE_TYPE = os.environ.get("NODE_LABEL_INSTANCE_TYPE", "beta.kubernetes.io/instance-type") +NODE_LABEL_REGION = os.environ.get( + "NODE_LABEL_REGION", "failure-domain.beta.kubernetes.io/region" +) +NODE_LABEL_INSTANCE_TYPE = os.environ.get( + "NODE_LABEL_INSTANCE_TYPE", "beta.kubernetes.io/instance-type" +) # https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels -OBJECT_LABEL_APPLICATION = os.environ.get("OBJECT_LABEL_APPLICATION", "application,app,app.kubernetes.io/name").split(",") -OBJECT_LABEL_COMPONENT = os.environ.get("OBJECT_LABEL_COMPONENT", "component,app.kubernetes.io/component").split(",") +OBJECT_LABEL_APPLICATION = os.environ.get( + "OBJECT_LABEL_APPLICATION", "application,app,app.kubernetes.io/name" +).split(",") +OBJECT_LABEL_COMPONENT = os.environ.get( + "OBJECT_LABEL_COMPONENT", "component,app.kubernetes.io/component" +).split(",") OBJECT_LABEL_TEAM = os.environ.get("OBJECT_LABEL_TEAM", "team,owner").split(",") ONE_MEBI = 1024 ** 2 @@ -129,17 +137,17 @@ def json_default(obj): # https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/resource-metrics-api.md class NodeMetrics(APIObject): - version = 'metrics.k8s.io/v1beta1' - endpoint = 'nodes' - kind = 'NodeMetrics' + version = "metrics.k8s.io/v1beta1" + endpoint = "nodes" + kind = "NodeMetrics" # https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/resource-metrics-api.md class PodMetrics(NamespacedAPIObject): - version = 'metrics.k8s.io/v1beta1' - endpoint = 'pods' - kind = 'PodMetrics' + version = "metrics.k8s.io/v1beta1" + endpoint = "pods" + kind = "PodMetrics" def get_ema(curr_value, prev_value, alpha=1.0): @@ -207,20 +215,24 @@ def get_pod_usage(cluster, pods: dict, prev_pods: dict, alpha_ema: float): def find_backend_application(client, ingress, rule): - ''' + """ The Ingress object might not have a "application" label, so let's try to find the application by looking at the backend service and its pods - ''' - paths = rule.get('http', {}).get('paths', []) + """ + paths = rule.get("http", {}).get("paths", []) selectors = [] for path in paths: - service_name = path.get('backend', {}).get('serviceName') + service_name = path.get("backend", {}).get("serviceName") if service_name: try: - service = Service.objects(client, namespace=ingress.namespace).get(name=service_name) + service = Service.objects(client, namespace=ingress.namespace).get( + name=service_name + ) except ObjectDoesNotExist: - logger.debug(f'Referenced service does not exist: {ingress.namespace}/{service_name}') + logger.debug( + f"Referenced service does not exist: {ingress.namespace}/{service_name}" + ) else: - selector = service.obj['spec'].get('selector', {}) + selector = service.obj["spec"].get("selector", {}) selectors.append(selector) application = get_application_from_labels(selector) if application: @@ -228,14 +240,16 @@ def find_backend_application(client, ingress, rule): # we still haven't found the application, let's look up pods by label selectors for selector in selectors: application_candidates = set() - for pod in Pod.objects(client).filter(namespace=ingress.namespace, selector=selector): + for pod in Pod.objects(client).filter( + namespace=ingress.namespace, selector=selector + ): application = get_application_from_labels(pod.labels) if application: application_candidates.add(application) if len(application_candidates) == 1: return application_candidates.pop() - return '' + return "" def pod_active(pod): @@ -253,8 +267,14 @@ def pod_active(pod): def query_cluster( - cluster, executor, system_namespaces, additional_cost_per_cluster, - alpha_ema, prev_cluster_summaries, no_ingress_status, node_labels + cluster, + executor, + system_namespaces, + additional_cost_per_cluster, + alpha_ema, + prev_cluster_summaries, + no_ingress_status, + node_labels, ): logger.info(f"Querying cluster {cluster.id} ({cluster.api_server_url})..") pods = {} @@ -262,9 +282,9 @@ def query_cluster( namespaces = {} for namespace in Namespace.objects(cluster.client): - email = namespace.annotations.get('email') + email = namespace.annotations.get("email") namespaces[namespace.name] = { - "status": namespace.obj['status']['phase'], + "status": namespace.obj["status"]["phase"], "email": email, } @@ -302,15 +322,20 @@ def query_cluster( ) node["role"] = role node["instance_type"] = instance_type - node["cost"] = pricing.get_node_cost(region, instance_type, is_spot, - cpu=node['capacity'].get('cpu'), memory=node['capacity'].get('memory')) + node["cost"] = pricing.get_node_cost( + region, + instance_type, + is_spot, + cpu=node["capacity"].get("cpu"), + memory=node["capacity"].get("memory"), + ) cluster_cost += node["cost"] get_node_usage(cluster, nodes, prev_cluster_summaries.get("nodes", {}), alpha_ema) cluster_usage = collections.defaultdict(float) for node in nodes.values(): - for k, v in node['usage'].items(): + for k, v in node["usage"].items(): cluster_usage[k] += v cost_per_cpu = cluster_cost / cluster_allocatable["cpu"] @@ -378,7 +403,9 @@ def query_cluster( "cost": cluster_cost, "cost_per_user_request_hour": { "cpu": 0.5 * hourly_cost / max(user_requests["cpu"], MIN_CPU_USER_REQUESTS), - "memory": 0.5 * hourly_cost / max(user_requests["memory"] / ONE_GIBI, MIN_MEMORY_USER_REQUESTS), + "memory": 0.5 + * hourly_cost + / max(user_requests["memory"] / ONE_GIBI, MIN_MEMORY_USER_REQUESTS), }, "ingresses": [], } @@ -403,13 +430,21 @@ def query_cluster( for _ingress in Ingress.objects(cluster.client, namespace=pykube.all): application = get_application_from_labels(_ingress.labels) for rule in _ingress.obj["spec"].get("rules", []): - host = rule.get('host', '') + host = rule.get("host", "") if not application: # find the application by getting labels from pods - backend_application = find_backend_application(cluster.client, _ingress, rule) + backend_application = find_backend_application( + cluster.client, _ingress, rule + ) else: backend_application = None - ingress = [_ingress.namespace, _ingress.name, application or backend_application, host, 0] + ingress = [ + _ingress.namespace, + _ingress.name, + application or backend_application, + host, + 0, + ] if host and not no_ingress_status: try: future = futures_by_host[host] @@ -420,7 +455,9 @@ def query_cluster( cluster_summary["ingresses"].append(ingress) if not no_ingress_status: - logger.info(f'Waiting for ingress status for {cluster.id} ({cluster.api_server_url})..') + logger.info( + f"Waiting for ingress status for {cluster.id} ({cluster.api_server_url}).." + ) for future in concurrent.futures.as_completed(futures): ingresses = futures[future] try: @@ -497,7 +534,9 @@ def get_cluster_summaries( ) logger.exception(e) - sorted_by_name = sorted(cluster_summaries.values(), key=lambda summary: summary["cluster"].name) + sorted_by_name = sorted( + cluster_summaries.values(), key=lambda summary: summary["cluster"].name + ) return {summary["cluster"].id: summary for summary in sorted_by_name} @@ -549,10 +588,8 @@ def aggregate_by_team(applications: dict, teams: dict): team["clusters"] |= app["clusters"] team["pods"] += app["pods"] for r in "cpu", "memory": - team["requests"][r] = team["requests"].get( - r, 0) + app["requests"][r] - team["usage"][r] = team["usage"].get(r, 0) + app.get( - "usage", {}).get(r, 0) + team["requests"][r] = team["requests"].get(r, 0) + app["requests"][r] + team["usage"][r] = team["usage"].get(r, 0) + app.get("usage", {}).get(r, 0) team["cost"] += app["cost"] team["slack_cost"] += app["slack_cost"] teams[team_id] = team @@ -611,7 +648,7 @@ def generate_report( pricing.regenerate_cost_dict(pricing_file) if links_file: - with open(links_file, 'rb') as fd: + with open(links_file, "rb") as fd: links = yaml.safe_load(fd) else: links = {} @@ -628,7 +665,7 @@ def generate_report( pickle_file_name = "dump.pickle" if use_cache and out.exists(pickle_file_name): - with out.open(pickle_file_name, 'rb') as fd: + with out.open(pickle_file_name, "rb") as fd: data = pickle.load(fd) cluster_summaries = data["cluster_summaries"] teams = data["teams"] @@ -698,10 +735,12 @@ def generate_report( }, ) for r in "cpu", "memory": - namespace["requests"][r] = namespace["requests"].get(r, 0) + pod["requests"][r] - namespace["usage"][r] = namespace["usage"].get(r, 0) + pod.get("usage", {}).get( - r, 0 + namespace["requests"][r] = ( + namespace["requests"].get(r, 0) + pod["requests"][r] ) + namespace["usage"][r] = namespace["usage"].get(r, 0) + pod.get( + "usage", {} + ).get(r, 0) namespace["cost"] += pod["cost"] namespace["slack_cost"] += pod.get("slack_cost", 0) namespace["pods"] += 1 @@ -714,11 +753,13 @@ def generate_report( aggregate_by_team(applications, teams) for team in teams.values(): + def cluster_name(cluster_id): try: return cluster_summaries[cluster_id]["cluster"].name except KeyError: return None + team["clusters"] = sorted(team["clusters"], key=cluster_name) for cluster_id, summary in sorted(cluster_summaries.items()): @@ -735,7 +776,7 @@ def cluster_name(cluster_id): if not use_cache: try: - with out.open(pickle_file_name, 'wb') as fd: + with out.open(pickle_file_name, "wb") as fd: pickle.dump( { "cluster_summaries": cluster_summaries, @@ -746,9 +787,20 @@ def cluster_name(cluster_id): fd, ) except Exception as e: - logger.error(f'Could not dump pickled cache data: {e}') - - write_report(out, start, notifications, cluster_summaries, namespace_usage, applications, teams, node_labels, links, alpha_ema) + logger.error(f"Could not dump pickled cache data: {e}") + + write_report( + out, + start, + notifications, + cluster_summaries, + namespace_usage, + applications, + teams, + node_labels, + links, + alpha_ema, + ) return cluster_summaries @@ -762,10 +814,21 @@ def write_loading_page(out): "now": now, "version": __version__, } - out.render_template('loading.html', context, file_name) + out.render_template("loading.html", context, file_name) -def write_report(out: OutputManager, start, notifications, cluster_summaries, namespace_usage, applications, teams, node_labels, links, alpha_ema: float): +def write_report( + out: OutputManager, + start, + notifications, + cluster_summaries, + namespace_usage, + applications, + teams, + node_labels, + links, + alpha_ema: float, +): total_allocatable: dict = collections.defaultdict(int) total_requests: dict = collections.defaultdict(int) total_user_requests: dict = collections.defaultdict(int) @@ -780,7 +843,14 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na with out.open("clusters.tsv") as csvfile: writer = csv.writer(csvfile, delimiter="\t") - headers = ["Cluster ID", "API Server URL", "Master Nodes", "Worker Nodes", "Worker Instance Type", "Kubelet Version"] + headers = [ + "Cluster ID", + "API Server URL", + "Master Nodes", + "Worker Nodes", + "Worker Instance Type", + "Kubelet Version", + ] for x in resource_categories: headers.extend([f"CPU {x.capitalize()}", f"Memory {x.capitalize()} [MiB]"]) headers.append("Cost [USD]") @@ -812,7 +882,17 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na with out.open("ingresses.tsv") as csvfile: writer = csv.writer(csvfile, delimiter="\t") - writer.writerow(["Cluster ID", "API Server URL", "Namespace", "Name", "Application", "Host", "Status"]) + writer.writerow( + [ + "Cluster ID", + "API Server URL", + "Namespace", + "Name", + "Application", + "Host", + "Status", + ] + ) for cluster_id, summary in sorted(cluster_summaries.items()): for ingress in summary["ingresses"]: writer.writerow( @@ -821,34 +901,84 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na with out.open("teams.tsv") as csvfile: writer = csv.writer(csvfile, delimiter="\t") - writer.writerow(["ID", "Clusters", "Applications", "Pods", - "CPU Requests", "Memory Requests", "CPU Usage", "Memory Usage", "Cost [USD]", "Slack Cost [USD]"]) + writer.writerow( + [ + "ID", + "Clusters", + "Applications", + "Pods", + "CPU Requests", + "Memory Requests", + "CPU Usage", + "Memory Usage", + "Cost [USD]", + "Slack Cost [USD]", + ] + ) for team_id, team in sorted(teams.items()): - writer.writerow([ - team_id, len(team["clusters"]), len(team["applications"]), team["pods"], - round(team["requests"]["cpu"], 2), - round(team["requests"]["memory"], 2), - round(team["usage"]["cpu"], 2), - round(team["usage"]["memory"], 2), - round(team["cost"], 2), - round(team["slack_cost"], 2)]) + writer.writerow( + [ + team_id, + len(team["clusters"]), + len(team["applications"]), + team["pods"], + round(team["requests"]["cpu"], 2), + round(team["requests"]["memory"], 2), + round(team["usage"]["cpu"], 2), + round(team["usage"]["memory"], 2), + round(team["cost"], 2), + round(team["slack_cost"], 2), + ] + ) with out.open("applications.tsv") as csvfile: writer = csv.writer(csvfile, delimiter="\t") - writer.writerow(["ID", "Team", "Clusters", "Pods", "CPU Requests", "Memory Requests", "CPU Usage", "Memory Usage", "Cost [USD]", "Slack Cost [USD]"]) + writer.writerow( + [ + "ID", + "Team", + "Clusters", + "Pods", + "CPU Requests", + "Memory Requests", + "CPU Usage", + "Memory Usage", + "Cost [USD]", + "Slack Cost [USD]", + ] + ) for app_id, app in sorted(applications.items()): - writer.writerow([ - app_id, app["team"], len(app["clusters"]), app["pods"], - round(app["requests"]["cpu"], 2), - round(app["requests"]["memory"], 2), - round(app["usage"]["cpu"], 2), - round(app["usage"]["memory"], 2), - round(app["cost"], 2), - round(app["slack_cost"], 2)]) + writer.writerow( + [ + app_id, + app["team"], + len(app["clusters"]), + app["pods"], + round(app["requests"]["cpu"], 2), + round(app["requests"]["memory"], 2), + round(app["usage"]["cpu"], 2), + round(app["usage"]["memory"], 2), + round(app["cost"], 2), + round(app["slack_cost"], 2), + ] + ) with out.open("namespaces.tsv") as csvfile: writer = csv.writer(csvfile, delimiter="\t") - writer.writerow(["Name", "Status", "Cluster", "Pods", "CPU Requests", "Memory Requests", "CPU Usage", "Memory Usage", "Cost [USD]", "Slack Cost [USD]"]) + writer.writerow( + [ + "Name", + "Status", + "Cluster", + "Pods", + "CPU Requests", + "Memory Requests", + "CPU Usage", + "Memory Usage", + "Cost [USD]", + "Slack Cost [USD]", + ] + ) for cluster_id, namespace_item in sorted(namespace_usage.items()): fields = [ namespace_item["id"], @@ -860,14 +990,28 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na round(namespace_item["usage"]["cpu"], 2), round(namespace_item["usage"]["memory"], 2), round(namespace_item["cost"], 2), - round(namespace_item["slack_cost"], 2) + round(namespace_item["slack_cost"], 2), ] writer.writerow(fields) with out.open("pods.tsv") as csvfile: writer = csv.writer(csvfile, delimiter="\t") - writer.writerow(["Cluster ID", "API Server URL", "Namespace", "Name", "Application", "Component", "Container Images", - "CPU Requests", "Memory Requests", "CPU Usage", "Memory Usage", "Cost [USD]"]) + writer.writerow( + [ + "Cluster ID", + "API Server URL", + "Namespace", + "Name", + "Application", + "Component", + "Container Images", + "CPU Requests", + "Memory Requests", + "CPU Usage", + "Memory Usage", + "Cost [USD]", + ] + ) with out.open("slack.tsv") as csvfile2: slackwriter = csv.writer(csvfile2, delimiter="\t") for cluster_id, summary in sorted(cluster_summaries.items()): @@ -897,7 +1041,7 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na requests["memory"], usage["cpu"], usage["memory"], - pod["cost"] + pod["cost"], ] ) cost_per_cpu = summary["cost"] / summary["allocatable"]["cpu"] @@ -956,9 +1100,12 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na "total_pods": sum([len(s["pods"]) for s in cluster_summaries.values()]), "total_cost": total_cost, "total_cost_per_user_request_hour": { - "cpu": 0.5 * total_hourly_cost / max(total_user_requests["cpu"], MIN_CPU_USER_REQUESTS), - "memory": 0.5 * total_hourly_cost / max( - total_user_requests["memory"] / ONE_GIBI, MIN_MEMORY_USER_REQUESTS), + "cpu": 0.5 + * total_hourly_cost + / max(total_user_requests["cpu"], MIN_CPU_USER_REQUESTS), + "memory": 0.5 + * total_hourly_cost + / max(total_user_requests["memory"] / ONE_GIBI, MIN_MEMORY_USER_REQUESTS), }, "total_slack_cost": sum([a["slack_cost"] for a in applications.values()]), "now": now, @@ -971,7 +1118,15 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na with out.open("metrics.json") as fd: json.dump(metrics, fd) - for page in ["index", "clusters", "ingresses", "teams", "applications", "namespaces", "pods"]: + for page in [ + "index", + "clusters", + "ingresses", + "teams", + "applications", + "namespaces", + "pods", + ]: file_name = f"{page}.html" context["page"] = page context["alpha_ema"] = alpha_ema @@ -983,23 +1138,25 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na context["page"] = page context["cluster_id"] = cluster_id context["summary"] = summary - out.render_template('cluster.html', context, file_name) + out.render_template("cluster.html", context, file_name) with out.open("cluster-metrics.json") as fd: json.dump( { cluster_id: { key: { - k if isinstance(k, str) else '/'.join(k): v + k if isinstance(k, str) else "/".join(k): v for k, v in value.items() - } if hasattr(value, 'items') else value + } + if hasattr(value, "items") + else value for key, value in summary.items() - if key != 'cluster' + if key != "cluster" } for cluster_id, summary in cluster_summaries.items() }, fd, - default=json_default + default=json_default, ) for team_id, team in teams.items(): @@ -1008,7 +1165,7 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na context["page"] = page context["team_id"] = team_id context["team"] = team - out.render_template('team.html', context, file_name) + out.render_template("team.html", context, file_name) with out.open("team-metrics.json") as fd: json.dump( @@ -1019,12 +1176,12 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na app_id: app for app_id, app in applications.items() if app["team"] == team_id - } + }, } for team_id, team in teams.items() }, fd, - default=json_default + default=json_default, ) with out.open("application-metrics.json") as fd: @@ -1032,24 +1189,30 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na ingresses_by_application: Dict[str, list] = collections.defaultdict(list) for cluster_id, summary in cluster_summaries.items(): - for ingress in summary['ingresses']: - ingresses_by_application[ingress[2]].append({ - 'cluster_id': cluster_id, 'cluster_summary': summary, - 'namespace': ingress[0], - 'name': ingress[1], - 'host': ingress[3], - 'status': ingress[4]} + for ingress in summary["ingresses"]: + ingresses_by_application[ingress[2]].append( + { + "cluster_id": cluster_id, + "cluster_summary": summary, + "namespace": ingress[0], + "name": ingress[1], + "host": ingress[3], + "status": ingress[4], + } ) pods_by_application: Dict[str, list] = collections.defaultdict(list) for cluster_id, summary in cluster_summaries.items(): - for namespace_name, pod in summary['pods'].items(): + for namespace_name, pod in summary["pods"].items(): namespace, name = namespace_name - pods_by_application[pod['application']].append({ - 'cluster_id': cluster_id, 'cluster_summary': summary, - 'namespace': namespace, - 'name': name, - 'pod': pod} + pods_by_application[pod["application"]].append( + { + "cluster_id": cluster_id, + "cluster_summary": summary, + "namespace": namespace, + "name": name, + "pod": pod, + } ) for app_id, application in applications.items(): @@ -1060,26 +1223,26 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na **application, "ingresses": [ { - "cluster": row['cluster_id'], - "namespace": row['namespace'], + "cluster": row["cluster_id"], + "namespace": row["namespace"], "name": row["name"], "host": row["host"], - "status": row["status"] + "status": row["status"], } for row in ingresses_by_application[app_id] ], "pods": [ { - **row['pod'], - "cluster": row['cluster_id'], - "namespace": row['namespace'], - "name": row["name"] + **row["pod"], + "cluster": row["cluster_id"], + "namespace": row["namespace"], + "name": row["name"], } for row in pods_by_application[app_id] - ] + ], }, fd, - default=json_default + default=json_default, ) for app_id, application in applications.items(): @@ -1089,6 +1252,6 @@ def write_report(out: OutputManager, start, notifications, cluster_summaries, na context["application"] = application context["ingresses_by_application"] = ingresses_by_application context["pods_by_application"] = pods_by_application - out.render_template('application.html', context, file_name) + out.render_template("application.html", context, file_name) out.clean_up_stale_files() diff --git a/pipenv-install.py b/pipenv-install.py deleted file mode 100755 index d790d61..0000000 --- a/pipenv-install.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -""" -Helper script for Docker build to install packages from Pipfile.lock without installing Pipenv -""" -import json -import subprocess - -with open("Pipfile.lock") as fd: - data = json.load(fd) - -packages = [] -for k, v in data["default"].items(): - packages.append(k + v["version"]) - -subprocess.run(["pip3", "install"] + packages, check=True) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..dd8ff23 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,917 @@ +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + +[[package]] +category = "dev" +description = "An unobtrusive argparse wrapper with natural syntax" +name = "argh" +optional = false +python-versions = "*" +version = "0.26.2" + +[[package]] +category = "dev" +description = "Atomic file writes." +marker = "sys_platform == \"win32\"" +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.10b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" +toml = ">=0.9.4" +typed-ast = ">=1.4.0" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "dev" +description = "The AWS SDK for Python" +name = "boto3" +optional = false +python-versions = "*" +version = "1.10.43" + +[package.dependencies] +botocore = ">=1.13.43,<1.14.0" +jmespath = ">=0.7.1,<1.0.0" +s3transfer = ">=0.2.0,<0.3.0" + +[[package]] +category = "dev" +description = "Low-level, data-driven core of boto 3." +name = "botocore" +optional = false +python-versions = "*" +version = "1.13.43" + +[package.dependencies] +docutils = ">=0.10,<0.16" +jmespath = ">=0.7.1,<1.0.0" + +[package.dependencies.python-dateutil] +python = ">=2.7" +version = ">=2.1,<2.8.1" + +[package.dependencies.urllib3] +python = ">=3.4" +version = ">=1.20,<1.26" + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.11.28" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +version = "4.5.4" + +[[package]] +category = "dev" +description = "Show coverage stats online via coveralls.io" +name = "coveralls" +optional = false +python-versions = "*" +version = "1.9.2" + +[package.dependencies] +coverage = ">=3.6,<5.0" +docopt = ">=0.6.1" +requests = ">=1.0.0" + +[package.extras] +yaml = ["PyYAML (>=3.10)"] + +[[package]] +category = "dev" +description = "Pythonic argument parser, that will make you smile" +name = "docopt" +optional = false +python-versions = "*" +version = "0.6.2" + +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.15.2" + +[[package]] +category = "dev" +description = "Discover and load entry points from installed packages." +name = "entrypoints" +optional = false +python-versions = ">=2.7" +version = "0.3" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8, pyflakes and co" +name = "flake8" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.7.9" + +[package.dependencies] +entrypoints = ">=0.3.0,<0.4.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.5.0,<2.6.0" +pyflakes = ">=2.1.0,<2.2.0" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.3.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[[package]] +category = "main" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10.3" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "dev" +description = "JSON Matching Expressions" +name = "jmespath" +optional = false +python-versions = "*" +version = "0.9.4" + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.0.2" + +[[package]] +category = "dev" +description = "Optional static typing for Python" +name = "mypy" +optional = false +python-versions = ">=3.5" +version = "0.761" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.2" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.6.0" + +[[package]] +category = "dev" +description = "File system general utilities" +name = "pathtools" +optional = false +python-versions = "*" +version = "0.1.2" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.5.0" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.1" + +[[package]] +category = "main" +description = "Python client library for Kubernetes" +name = "pykube-ng" +optional = false +python-versions = "*" +version = "19.10.0" + +[package.dependencies] +PyYAML = "*" +requests = ">=2.12" + +[package.extras] +gcp = ["google-auth", "jsonpath-ng"] + +[[package]] +category = "dev" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.5" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.3.2" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.1" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] + +[[package]] +category = "dev" +description = "Local continuous test runner with pytest and watchdog." +name = "pytest-watch" +optional = false +python-versions = "*" +version = "4.2.0" + +[package.dependencies] +colorama = ">=0.3.3" +docopt = ">=0.4.0" +pytest = ">=2.6.4" +watchdog = ">=0.6.0" + +[[package]] +category = "dev" +description = "Extensions to the standard Python datetime module" +marker = "python_version >= \"2.7\"" +name = "python-dateutil" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.8.0" + +[package.dependencies] +six = ">=1.5" + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.2" + +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2019.12.19" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "Asynchronous Python HTTP for Humans." +name = "requests-futures" +optional = false +python-versions = "*" +version = "1.0.0" + +[package.dependencies] +requests = ">=1.2.0" + +[[package]] +category = "dev" +description = "An Amazon S3 Transfer Manager" +name = "s3transfer" +optional = false +python-versions = "*" +version = "0.2.1" + +[package.dependencies] +botocore = ">=1.12.36,<2.0.0" + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.13.0" + +[[package]] +category = "main" +description = "Python library to manage OAuth access tokens" +name = "stups-tokens" +optional = false +python-versions = "*" +version = "1.1.19" + +[package.dependencies] +requests = "*" + +[package.extras] +tests = ["flake8"] + +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4.1" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +version = "1.25.7" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "dev" +description = "Filesystem events monitoring" +name = "watchdog" +optional = false +python-versions = "*" +version = "0.9.0" + +[package.dependencies] +PyYAML = ">=3.10" +argh = ">=0.24.1" +pathtools = ">=0.1.1" + +[[package]] +category = "dev" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.7" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "0.6.0" + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "contextlib2", "unittest2"] + +[metadata] +content-hash = "b3fd691ab91c700e00b1874ce718b9884d9224a65dc86ab142e0470e1ba6f2f7" +python-versions = "^3.7" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +argh = [ + {file = "argh-0.26.2-py2.py3-none-any.whl", hash = "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3"}, + {file = "argh-0.26.2.tar.gz", hash = "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65"}, +] +atomicwrites = [ + {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, + {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +black = [ + {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, +] +boto3 = [ + {file = "boto3-1.10.43-py2.py3-none-any.whl", hash = "sha256:06f5fd086dc4f7a09b1d36dd15f047878198ee87db5d1f85d7621596d163e254"}, + {file = "boto3-1.10.43.tar.gz", hash = "sha256:40620839d08152911f0fb285fa4a91f4879ec71e0498f4d1f6e2cc30fddb3fb5"}, +] +botocore = [ + {file = "botocore-1.13.43-py2.py3-none-any.whl", hash = "sha256:1c73eaf96a5f35741b72c382c5a19a97259f92271e919fd76a138991157610c4"}, + {file = "botocore-1.13.43.tar.gz", hash = "sha256:5ac2108e44976730a514218be381cc0d83141e398fd7c55483b6ea2b181febd2"}, +] +certifi = [ + {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, + {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, + {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, +] +colorama = [ + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, +] +coverage = [ + {file = "coverage-4.5.4-cp26-cp26m-macosx_10_12_x86_64.whl", hash = "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888"}, + {file = "coverage-4.5.4-cp27-cp27m-win32.whl", hash = "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc"}, + {file = "coverage-4.5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6"}, + {file = "coverage-4.5.4-cp33-cp33m-macosx_10_10_x86_64.whl", hash = "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5"}, + {file = "coverage-4.5.4-cp34-cp34m-macosx_10_12_x86_64.whl", hash = "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca"}, + {file = "coverage-4.5.4-cp34-cp34m-win32.whl", hash = "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0"}, + {file = "coverage-4.5.4-cp34-cp34m-win_amd64.whl", hash = "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1"}, + {file = "coverage-4.5.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"}, + {file = "coverage-4.5.4-cp35-cp35m-win32.whl", hash = "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e"}, + {file = "coverage-4.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d"}, + {file = "coverage-4.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9"}, + {file = "coverage-4.5.4-cp36-cp36m-win32.whl", hash = "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f"}, + {file = "coverage-4.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5"}, + {file = "coverage-4.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2"}, + {file = "coverage-4.5.4-cp37-cp37m-win32.whl", hash = "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5"}, + {file = "coverage-4.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351"}, + {file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"}, + {file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"}, +] +coveralls = [ + {file = "coveralls-1.9.2-py2.py3-none-any.whl", hash = "sha256:25522a50cdf720d956601ca6ef480786e655ae2f0c94270c77e1a23d742de558"}, + {file = "coveralls-1.9.2.tar.gz", hash = "sha256:8e3315e8620bb6b3c6f3179a75f498e7179c93b3ddc440352404f941b1f70524"}, +] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] +docutils = [ + {file = "docutils-0.15.2-py2-none-any.whl", hash = "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827"}, + {file = "docutils-0.15.2-py3-none-any.whl", hash = "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0"}, + {file = "docutils-0.15.2.tar.gz", hash = "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"}, +] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] +flake8 = [ + {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, + {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"}, +] +idna = [ + {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, + {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, +] +importlib-metadata = [ + {file = "importlib_metadata-1.3.0-py2.py3-none-any.whl", hash = "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"}, + {file = "importlib_metadata-1.3.0.tar.gz", hash = "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45"}, +] +jinja2 = [ + {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, + {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, +] +jmespath = [ + {file = "jmespath-0.9.4-py2.py3-none-any.whl", hash = "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6"}, + {file = "jmespath-0.9.4.tar.gz", hash = "sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +more-itertools = [ + {file = "more-itertools-8.0.2.tar.gz", hash = "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d"}, + {file = "more_itertools-8.0.2-py3-none-any.whl", hash = "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"}, +] +mypy = [ + {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, + {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, + {file = "mypy-0.761-cp35-cp35m-win_amd64.whl", hash = "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72"}, + {file = "mypy-0.761-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2"}, + {file = "mypy-0.761-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0"}, + {file = "mypy-0.761-cp36-cp36m-win_amd64.whl", hash = "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474"}, + {file = "mypy-0.761-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a"}, + {file = "mypy-0.761-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749"}, + {file = "mypy-0.761-cp37-cp37m-win_amd64.whl", hash = "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1"}, + {file = "mypy-0.761-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7"}, + {file = "mypy-0.761-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"}, + {file = "mypy-0.761-cp38-cp38-win_amd64.whl", hash = "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b"}, + {file = "mypy-0.761-py3-none-any.whl", hash = "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217"}, + {file = "mypy-0.761.tar.gz", hash = "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-19.2-py2.py3-none-any.whl", hash = "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"}, + {file = "packaging-19.2.tar.gz", hash = "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47"}, +] +pathspec = [ + {file = "pathspec-0.6.0.tar.gz", hash = "sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c"}, +] +pathtools = [ + {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.8.0-py2.py3-none-any.whl", hash = "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"}, + {file = "py-1.8.0.tar.gz", hash = "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"}, +] +pycodestyle = [ + {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, + {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, +] +pyflakes = [ + {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, + {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, +] +pykube-ng = [ + {file = "pykube-ng-19.10.0.tar.gz", hash = "sha256:440b4183719e673c11b7cd68669d3ba0b710c192834d16bd7766dfb6df9737b2"}, + {file = "pykube_ng-19.10.0-py2.py3-none-any.whl", hash = "sha256:bd872f0e6ad4a58cc6cb005a9d15decaba1363efc7d52ee75a64d16a5e986b87"}, +] +pyparsing = [ + {file = "pyparsing-2.4.5-py2.py3-none-any.whl", hash = "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f"}, + {file = "pyparsing-2.4.5.tar.gz", hash = "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"}, +] +pytest = [ + {file = "pytest-5.3.2-py3-none-any.whl", hash = "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"}, + {file = "pytest-5.3.2.tar.gz", hash = "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa"}, +] +pytest-cov = [ + {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, + {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, +] +pytest-watch = [ + {file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.0.tar.gz", hash = "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"}, + {file = "python_dateutil-2.8.0-py2.py3-none-any.whl", hash = "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb"}, +] +pyyaml = [ + {file = "PyYAML-5.2-cp27-cp27m-win32.whl", hash = "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc"}, + {file = "PyYAML-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"}, + {file = "PyYAML-5.2-cp35-cp35m-win32.whl", hash = "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15"}, + {file = "PyYAML-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075"}, + {file = "PyYAML-5.2-cp36-cp36m-win32.whl", hash = "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31"}, + {file = "PyYAML-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc"}, + {file = "PyYAML-5.2-cp37-cp37m-win32.whl", hash = "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04"}, + {file = "PyYAML-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd"}, + {file = "PyYAML-5.2-cp38-cp38-win32.whl", hash = "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f"}, + {file = "PyYAML-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803"}, + {file = "PyYAML-5.2.tar.gz", hash = "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c"}, +] +regex = [ + {file = "regex-2019.12.19-cp27-cp27m-win32.whl", hash = "sha256:ba449b56fa419fb19bf2a2438adbd2433f27087a6fe115917eaf9cfca684d5b6"}, + {file = "regex-2019.12.19-cp27-cp27m-win_amd64.whl", hash = "sha256:4eeb0fe936797ae00a085f99802642bfc722b3b4ea557e9e7849cb621ea10c91"}, + {file = "regex-2019.12.19-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47298bc8b89d1c747f0f5974aa528fc0b6b17396f1694136a224d51461279d83"}, + {file = "regex-2019.12.19-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7c5e2efcf079c35ff266c3f3a6708834f88f9fd04a3c16b855e036b2b7b1b543"}, + {file = "regex-2019.12.19-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:16709434c4e2332ee8ba26ae339aceb8ab0b24b8398ebd0f52ebc943f45c4fc2"}, + {file = "regex-2019.12.19-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d51311496061863caae2cfe120cf1ef37900019b86c89c2d75f0918e0b4b8bf3"}, + {file = "regex-2019.12.19-cp36-cp36m-win32.whl", hash = "sha256:2404a50fb48badaf214b700f08822b68d93d79200e0aefd9569d0332d21fbfcb"}, + {file = "regex-2019.12.19-cp36-cp36m-win_amd64.whl", hash = "sha256:999a885f7f5194464238ad5d74b05982acee54002f3aa775d8e0e8c5fb74c06c"}, + {file = "regex-2019.12.19-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:223fb63ec8dcab20b3318e93dcec4aee89e98b062934090bf29ffc374d2000a2"}, + {file = "regex-2019.12.19-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d3f632cefad2cf247bd845794002585e3772288bfcb0dbac59fdecd32cd38b67"}, + {file = "regex-2019.12.19-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:a2e1e53df7dd27943da2b512895125b33fb20f81862c9fed7b3bab2a1de684d1"}, + {file = "regex-2019.12.19-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ab43bc0836820b7900dfffc025b996784aec26ec87dc1df4f95a40398760223f"}, + {file = "regex-2019.12.19-cp37-cp37m-win32.whl", hash = "sha256:23c3ebf05d1cd3adb26723fd598e75724e0cdb7d6a35185ac0caf061cc6edb49"}, + {file = "regex-2019.12.19-cp37-cp37m-win_amd64.whl", hash = "sha256:37e018d3746baf159aedfc9773c3cafacbd10d354ba15484f5cfc8ed9da5748b"}, + {file = "regex-2019.12.19-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0472acc4b6319801c1bc681d838c88ba1446f9ae199e01f6e41091c701fb3d42"}, + {file = "regex-2019.12.19-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2af3a7a16fed6eff85c25da106effa36f61cbbe801d00ade349b53ce7619eb15"}, + {file = "regex-2019.12.19-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:3c9c2988d02a9238a1975c70e87c6ce94e6f36dd8e372b66f468990cfe077434"}, + {file = "regex-2019.12.19-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:9fd2f4813eaa3e421e82819d38e5b634d900faff7ae5a80cd89ccff407175e69"}, + {file = "regex-2019.12.19-cp38-cp38-win32.whl", hash = "sha256:7ac08cee5055f548eed3889e9aaef15fd00172d037949496f1f0b34acb8a7c3e"}, + {file = "regex-2019.12.19-cp38-cp38-win_amd64.whl", hash = "sha256:6881be0218b47ed76db033f252bab3f912dfe7ed1fe7baa9daebf51de08546a0"}, + {file = "regex-2019.12.19.tar.gz", hash = "sha256:8355eaa64724a0fdb010a1654b77cb3e375dc08b7f592cc4a1c05ac606aa481c"}, +] +requests = [ + {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, + {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, +] +requests-futures = [ + {file = "requests-futures-1.0.0.tar.gz", hash = "sha256:35547502bf1958044716a03a2f47092a89efe8f9789ab0c4c528d9c9c30bc148"}, +] +s3transfer = [ + {file = "s3transfer-0.2.1-py2.py3-none-any.whl", hash = "sha256:b780f2411b824cb541dbcd2c713d0cb61c7d1bcadae204cdddda2b35cef493ba"}, + {file = "s3transfer-0.2.1.tar.gz", hash = "sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d"}, +] +six = [ + {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, + {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, +] +stups-tokens = [ + {file = "stups-tokens-1.1.19.tar.gz", hash = "sha256:7830ad83ccbfd52a9734608ffcefcca917137ce9480cc91a4fbd321a4aca3160"}, + {file = "stups_tokens-1.1.19-py3-none-any.whl", hash = "sha256:317f4386763bac9dd5c0a4c8b0f9f0238dc3fa81de3c6fd1971b6b01662b5750"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +typed-ast = [ + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, + {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, + {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, + {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, + {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, + {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, + {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.1-py2-none-any.whl", hash = "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d"}, + {file = "typing_extensions-3.7.4.1-py3-none-any.whl", hash = "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"}, + {file = "typing_extensions-3.7.4.1.tar.gz", hash = "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2"}, +] +urllib3 = [ + {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, + {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, +] +watchdog = [ + {file = "watchdog-0.9.0.tar.gz", hash = "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d"}, +] +wcwidth = [ + {file = "wcwidth-0.1.7-py2.py3-none-any.whl", hash = "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"}, + {file = "wcwidth-0.1.7.tar.gz", hash = "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e"}, +] +zipp = [ + {file = "zipp-0.6.0-py2.py3-none-any.whl", hash = "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"}, + {file = "zipp-0.6.0.tar.gz", hash = "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b417b63 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "kube_resource_report" +version = "19.12.1" +description = "Report Kubernetes cluster and pod resource requests vs usage and generate static HTML" +authors = ["Henning Jacobs "] + +[tool.poetry.dependencies] +python = "^3.7" +click = "*" +jinja2 = "*" +pykube-ng = "*" +requests = "*" +requests-futures = "*" +stups-tokens = "*" + +[tool.poetry.dev-dependencies] +black = {version = "^19.10b0", allow-prereleases = true} +boto3 = "*" +coverage = "*" +coveralls = "*" +flake8 = "*" +mypy = "*" +pytest = "*" +pytest-cov = "*" +pytest-watch = "*" diff --git a/tests/test_ema.py b/tests/test_ema.py index dd16eaa..dfa5471 100644 --- a/tests/test_ema.py +++ b/tests/test_ema.py @@ -1,8 +1,6 @@ import pytest -from kube_resource_report.report import ( - get_ema, -) +from kube_resource_report.report import get_ema @pytest.mark.parametrize( @@ -20,7 +18,7 @@ (10, 2, 0.9, 9.2), # small alpha, discount older observations slower (10, 2, 0.1, 2.8), - ] + ], ) def test_ema_func(curr_value, prev_value, alpha, expected): assert get_ema(curr_value, prev_value, alpha) == expected @@ -34,8 +32,11 @@ def test_ema_func(curr_value, prev_value, alpha, expected): ([1, 1, 2, 2], 2 / (4 + 1)), ([1000, 1, 1, 1, 1, 1, 1], 2 / (7 + 1)), ([1, 1, 2, 2, 3, 3, 2, 2, 1, 1], 2 / (10 + 1)), - ([5, 5, 5, 6, 10, 15, 15, 20, 15, 15, 21, 22, 23, 30, 10, 8, 5, 5, 6, 6], 2 / (20 + 1)), - ] + ( + [5, 5, 5, 6, 10, 15, 15, 20, 15, 15, 21, 22, 23, 30, 10, 8, 5, 5, 6, 6], + 2 / (20 + 1), + ), + ], ) def test_ema_like_sma(values, alpha): """ diff --git a/tests/test_report.py b/tests/test_report.py index 0885997..777fa8a 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -12,7 +12,7 @@ get_node_usage, new_resources, aggregate_by_team, - NODE_LABEL_ROLE + NODE_LABEL_ROLE, ) from unittest.mock import MagicMock @@ -28,7 +28,7 @@ def fake_responses(): # 1/20 of 1 core (node capacity) "cpu": "50m", # 1/4 of 1Gi (node capacity) - "memory": "256Mi" + "memory": "256Mi", } } } @@ -36,7 +36,7 @@ def fake_responses(): } return { - ('v1', "nodes"): { + ("v1", "nodes"): { "items": [ { "metadata": {"name": "node-1", "labels": {}}, @@ -47,58 +47,62 @@ def fake_responses(): } ] }, - ('v1', "pods"): { + ("v1", "pods"): { "items": [ { - "metadata": {"name": "pod-1", "namespace": "default", "labels": {"app": "myapp"}}, + "metadata": { + "name": "pod-1", + "namespace": "default", + "labels": {"app": "myapp"}, + }, "spec": fake_pod_spec, - "status": { - "phase": "Running" - } + "status": {"phase": "Running"}, }, { - "metadata": {"name": "pod-failed", "namespace": "default", "labels": {"app": "myapp"}}, + "metadata": { + "name": "pod-failed", + "namespace": "default", + "labels": {"app": "myapp"}, + }, "spec": fake_pod_spec, - "status": { - "phase": "Failed" - } + "status": {"phase": "Failed"}, }, { - "metadata": {"name": "pod-pending-scheduled", "namespace": "default", "labels": {"app": "myapp"}}, + "metadata": { + "name": "pod-pending-scheduled", + "namespace": "default", + "labels": {"app": "myapp"}, + }, "spec": fake_pod_spec, "status": { "phase": "Pending", - "conditions": [ - { - "type": "PodScheduled", - "status": "True", - } - ] - } + "conditions": [{"type": "PodScheduled", "status": "True"}], + }, }, { - "metadata": {"name": "pod-pending-no-conditions", "namespace": "default", "labels": {"app": "myapp"}}, + "metadata": { + "name": "pod-pending-no-conditions", + "namespace": "default", + "labels": {"app": "myapp"}, + }, "spec": fake_pod_spec, - "status": { - "phase": "Pending" - } + "status": {"phase": "Pending"}, }, { - "metadata": {"name": "pod-pending-not-scheduled", "namespace": "default", "labels": {"app": "myapp"}}, + "metadata": { + "name": "pod-pending-not-scheduled", + "namespace": "default", + "labels": {"app": "myapp"}, + }, "spec": fake_pod_spec, "status": { "phase": "Pending", - "conditions": [ - { - "type": "PodScheduled", - "status": "False", - } - ] - } - } + "conditions": [{"type": "PodScheduled", "status": "False"}], + }, + }, ] }, - ('extensions/v1beta1', "ingresses"): { + ("extensions/v1beta1", "ingresses"): { "items": [ { "metadata": {"name": "ing-1", "namespace": "default"}, @@ -107,18 +111,17 @@ def fake_responses(): # no "host" field! {"http": {}} ] - } - + }, } ] }, - ('v1', "namespaces"): {"items": []}, + ("v1", "namespaces"): {"items": []}, } @pytest.fixture def fake_responses_with_two_different_nodes(fake_responses): - fake_responses[('v1', "nodes")] = { + fake_responses[("v1", "nodes")] = { "items": [ { "metadata": {"name": "node-1", "labels": {NODE_LABEL_ROLE: "worker"}}, @@ -151,7 +154,7 @@ def fake_pod_metric_responses(): # 50% of requested resources are used "usage": {"cpu": "50m", "memory": "256Mi"} } - ] + ], } ] } @@ -164,13 +167,8 @@ def fake_node_metric_responses(): ("metrics.k8s.io/v1beta1", "nodes"): { "items": [ { - "metadata": { - "name": "node-1", - }, - "usage": { - "cpu": "16", - "memory": "128Gi" - } + "metadata": {"name": "node-1"}, + "usage": {"cpu": "16", "memory": "128Gi"}, }, ] } @@ -187,10 +185,7 @@ def fake_applications(): "pods": 1, "cost": 40, "slack_cost": 10, - "requests": { - "cpu": 1, - "memory": 1024, - }, + "requests": {"cpu": 1, "memory": 1024}, }, "some-other-app": { "id": "some-other-app", @@ -199,10 +194,7 @@ def fake_applications(): "pods": 1, "cost": 10, "slack_cost": 5, - "requests": { - "cpu": 0.2, - "memory": 512, - }, + "requests": {"cpu": 0.2, "memory": 512}, }, } @@ -226,14 +218,23 @@ def mock_get(version, url, **kwargs): @pytest.fixture def fake_generate_report(output_dir, monkeypatch): - monkeypatch.setattr("kube_resource_report.cluster_discovery.tokens.get", lambda x: "mytok") + monkeypatch.setattr( + "kube_resource_report.cluster_discovery.tokens.get", lambda x: "mytok" + ) def wrapper(responses): mock_client = get_mock_client(responses) monkeypatch.setattr( "kube_resource_report.cluster_discovery.ClusterRegistryDiscoverer.get_clusters", - lambda x: [Cluster("test-cluster-1", "test-cluster-1", "https://test-cluster-1.example.org", mock_client)], + lambda x: [ + Cluster( + "test-cluster-1", + "test-cluster-1", + "https://test-cluster-1.example.org", + mock_client, + ) + ], ) cluster_summaries = generate_report( @@ -253,7 +254,7 @@ def wrapper(responses): {}, None, None, - ["worker", "node"] + ["worker", "node"], ) assert len(cluster_summaries) == 1 return cluster_summaries @@ -262,12 +263,12 @@ def wrapper(responses): def test_parse_resource(): - assert parse_resource('500m') == 0.5 + assert parse_resource("500m") == 0.5 def test_ingress_without_host(fake_generate_report, fake_responses): cluster_summaries = fake_generate_report(fake_responses) - assert len(cluster_summaries['test-cluster-1']['ingresses']) == 1 + assert len(cluster_summaries["test-cluster-1"]["ingresses"]) == 1 def test_cluster_cost(fake_generate_report, fake_responses): @@ -278,58 +279,78 @@ def test_cluster_cost(fake_generate_report, fake_responses): cost_per_user_request_hour_cpu = 10 * cost_per_hour / 2 cost_per_user_request_hour_memory = 2 * cost_per_hour / 2 - assert cluster_summaries['test-cluster-1']['cost'] == cluster_cost + assert cluster_summaries["test-cluster-1"]["cost"] == cluster_cost - assert cluster_summaries['test-cluster-1']['cost_per_user_request_hour']['cpu'] == cost_per_user_request_hour_cpu - assert cluster_summaries['test-cluster-1']['cost_per_user_request_hour']['memory'] == cost_per_user_request_hour_memory + assert ( + cluster_summaries["test-cluster-1"]["cost_per_user_request_hour"]["cpu"] + == cost_per_user_request_hour_cpu + ) + assert ( + cluster_summaries["test-cluster-1"]["cost_per_user_request_hour"]["memory"] + == cost_per_user_request_hour_memory + ) # assert cost_per_hour == cost_per_user_request_hour_cpu + cost_per_user_request_hour_memory -def test_application_report(output_dir, fake_generate_report, fake_responses, fake_pod_metric_responses): +def test_application_report( + output_dir, fake_generate_report, fake_responses, fake_pod_metric_responses +): # merge responses to get usage metrics and slack costs all_responses = {**fake_responses, **fake_pod_metric_responses} fake_generate_report(all_responses) - expected = set(['index.html', 'applications.html', 'application-metrics.json']) + expected = set(["index.html", "applications.html", "application-metrics.json"]) paths = set() for f in Path(str(output_dir)).iterdir(): paths.add(f.name) assert expected <= paths - with (Path(str(output_dir)) / 'application-metrics.json').open() as fd: + with (Path(str(output_dir)) / "application-metrics.json").open() as fd: data = json.load(fd) - assert data['myapp']['id'] == 'myapp' - assert data['myapp']['pods'] == 2 - assert data['myapp']['requests'] == {'cpu': 0.1, 'memory': 512 * 1024**2} + assert data["myapp"]["id"] == "myapp" + assert data["myapp"]["pods"] == 2 + assert data["myapp"]["requests"] == {"cpu": 0.1, "memory": 512 * 1024 ** 2} # the "myapp" pod consumes 1/2 of cluster capacity (512Mi of 1Gi memory) - assert data['myapp']['cost'] == 50.0 + assert data["myapp"]["cost"] == 50.0 # only 1/2 of requested resources are used => 50% of costs are slack - assert data['myapp']['slack_cost'] == 25.0 + assert data["myapp"]["slack_cost"] == 25.0 def test_get_pod_usage(monkeypatch, fake_pod_metric_responses): mock_client = get_mock_client(fake_pod_metric_responses) - cluster = Cluster("test-cluster-1", "test-cluster-1", "https://test-cluster-1.example.org", mock_client) - pods = {('default', 'pod-1'): {'usage': new_resources()}} + cluster = Cluster( + "test-cluster-1", + "test-cluster-1", + "https://test-cluster-1.example.org", + mock_client, + ) + pods = {("default", "pod-1"): {"usage": new_resources()}} get_pod_usage(cluster, pods, {}, 1.0) - assert pods[('default', 'pod-1')]['usage']['cpu'] == 0.05 + assert pods[("default", "pod-1")]["usage"]["cpu"] == 0.05 def test_get_node_usage(monkeypatch, fake_node_metric_responses): mock_client = get_mock_client(fake_node_metric_responses) - cluster = Cluster("test-cluster-1", "test-cluster-1", "https://test-cluster-1.example.org", mock_client) - nodes = {'node-1': {'usage': new_resources()}} + cluster = Cluster( + "test-cluster-1", + "test-cluster-1", + "https://test-cluster-1.example.org", + mock_client, + ) + nodes = {"node-1": {"usage": new_resources()}} get_node_usage(cluster, nodes, {}, 1.0) - assert nodes['node-1']['usage']['cpu'] == 16 + assert nodes["node-1"]["usage"]["cpu"] == 16 -def test_more_than_one_label(fake_generate_report, fake_responses_with_two_different_nodes): +def test_more_than_one_label( + fake_generate_report, fake_responses_with_two_different_nodes +): cluster_summaries = fake_generate_report(fake_responses_with_two_different_nodes) - assert cluster_summaries['test-cluster-1']['worker_nodes'] == 2 + assert cluster_summaries["test-cluster-1"]["worker_nodes"] == 2 def test_aggregate_by_team(fake_applications): @@ -340,5 +361,5 @@ def test_aggregate_by_team(fake_applications): assert team["cost"] == 50 assert team["slack_cost"] == 15 assert team["pods"] == 2 - assert team['requests'] == {'cpu': 1.2, 'memory': 512 + 1024} + assert team["requests"] == {"cpu": 1.2, "memory": 512 + 1024} assert team["clusters"] == {"some-cluster"}