Switch to Poetry and Black (#254)
* Pipenv -> Poetry * poetry and black
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
*
|
||||
!Pipfile*
|
||||
!pip*
|
||||
!pyproject.toml
|
||||
!poetry.lock
|
||||
!kube_ops_view
|
||||
|
||||
3
.flake8
Normal file
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
max-line-length=240
|
||||
ignore=E722,W503,E402,E203
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ scm-source.json
|
||||
.cache/
|
||||
.coverage
|
||||
.pytest_cache/
|
||||
.mypy_cache
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
sudo: yes
|
||||
language: python
|
||||
python:
|
||||
@@ -6,9 +6,7 @@ python:
|
||||
services:
|
||||
- docker
|
||||
install:
|
||||
- pip install tox tox-travis coveralls
|
||||
- pip install pipenv
|
||||
- pipenv install --dev
|
||||
- pip install poetry
|
||||
- nvm install 7.4
|
||||
- npm install -g eslint
|
||||
script:
|
||||
|
||||
27
Dockerfile
27
Dockerfile
@@ -1,27 +1,30 @@
|
||||
FROM python:3.7-alpine3.10
|
||||
FROM python:3.8-slim
|
||||
|
||||
WORKDIR /
|
||||
|
||||
RUN apk add --no-cache python3 python3-dev gcc musl-dev zlib-dev libffi-dev openssl-dev ca-certificates
|
||||
RUN apt-get update && apt-get install --yes gcc
|
||||
|
||||
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 && \
|
||||
apk del python3-dev gcc musl-dev zlib-dev libffi-dev openssl-dev && \
|
||||
rm -rf /var/cache/apk/* /root/.cache /tmp/*
|
||||
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 pre-built packages to this image
|
||||
COPY --from=0 /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages
|
||||
|
||||
# now copy the actual code we will execute (poetry install above was just for dependencies)
|
||||
COPY kube_ops_view /kube_ops_view
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
RUN sed -i "s/__version__ = .*/__version__ = '${VERSION}'/" /kube_ops_view/__init__.py
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/python", "-m", "kube_ops_view"]
|
||||
ENTRYPOINT ["python3", "-m", "kube_ops_view"]
|
||||
|
||||
14
Makefile
14
Makefile
@@ -7,13 +7,19 @@ TTYFLAGS = $(shell test -t 0 && echo "-it")
|
||||
|
||||
default: docker
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
poetry install
|
||||
|
||||
clean:
|
||||
rm -fr kube_ops_view/static/build
|
||||
|
||||
test:
|
||||
pipenv run flake8
|
||||
pipenv run coverage run --source=kube_ops_view -m py.test
|
||||
pipenv run coverage report
|
||||
test: install
|
||||
poetry run flake8
|
||||
poetry run black --check kube_ops_view
|
||||
# poetry run mypy --ignore-missing-imports kube_ops_view
|
||||
poetry run coverage run --source=kube_ops_view -m py.test -v
|
||||
poetry run coverage report
|
||||
|
||||
appjs:
|
||||
docker run $(TTYFLAGS) -u $$(id -u) -v $$(pwd):/workdir -w /workdir/app -e NPM_CONFIG_CACHE=/tmp node:11.10-alpine npm install
|
||||
|
||||
25
Pipfile
25
Pipfile
@@ -1,25 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
click = "*"
|
||||
gevent = "*"
|
||||
requests = "*"
|
||||
stups-tokens = ">=1.1.19"
|
||||
redlock-py = "*"
|
||||
json-delta = ">=2.0"
|
||||
flask = "*"
|
||||
pykube-ng = "*"
|
||||
flask-dance = "*"
|
||||
|
||||
[dev-packages]
|
||||
"flake8" = "*"
|
||||
pytest = "*"
|
||||
pipenv = "*"
|
||||
pytest-cov = "*"
|
||||
coveralls = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
513
Pipfile.lock
generated
513
Pipfile.lock
generated
@@ -1,513 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b08b64b6ea15f966d4c4cdb6f96e61a7b5239bdcc5ea27953c16bd76d9572d34"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
||||
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
||||
],
|
||||
"version": "==2019.9.11"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
|
||||
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"flask-dance": {
|
||||
"hashes": [
|
||||
"sha256:83d6f8d684150ac8fe7d4f2ad8d71170c3233831a09601eb0e5b40d0c28e337d",
|
||||
"sha256:c3fd1da1c93ada28092e83a5eb843def82701d6d56bd03a512a2e008a25ec106"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"gevent": {
|
||||
"hashes": [
|
||||
"sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64",
|
||||
"sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea",
|
||||
"sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c",
|
||||
"sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51",
|
||||
"sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e",
|
||||
"sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917",
|
||||
"sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1",
|
||||
"sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c",
|
||||
"sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909",
|
||||
"sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12",
|
||||
"sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8",
|
||||
"sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942",
|
||||
"sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950",
|
||||
"sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8",
|
||||
"sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee",
|
||||
"sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922",
|
||||
"sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e",
|
||||
"sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0",
|
||||
"sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad",
|
||||
"sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51",
|
||||
"sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1",
|
||||
"sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05",
|
||||
"sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"greenlet": {
|
||||
"hashes": [
|
||||
"sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0",
|
||||
"sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28",
|
||||
"sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8",
|
||||
"sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304",
|
||||
"sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0",
|
||||
"sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214",
|
||||
"sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043",
|
||||
"sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6",
|
||||
"sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625",
|
||||
"sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc",
|
||||
"sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638",
|
||||
"sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163",
|
||||
"sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4",
|
||||
"sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490",
|
||||
"sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248",
|
||||
"sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939",
|
||||
"sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87",
|
||||
"sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720",
|
||||
"sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython'",
|
||||
"version": "==0.4.15"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
||||
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
||||
],
|
||||
"version": "==2.10.1"
|
||||
},
|
||||
"json-delta": {
|
||||
"hashes": [
|
||||
"sha256:462a73672f7527517d863930bb442ed1986c35dfb6960e0fb1cb84393deea652",
|
||||
"sha256:cdbb8d38b69b5bef9ac54863aff2df8ae95ab897d165448f8d72cc3501fd32f6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"pykube-ng": {
|
||||
"hashes": [
|
||||
"sha256:22e595aede4caf8eeb4fe5289e22785a986b4ff641b454d18c88347e71b8fe9d",
|
||||
"sha256:eeac0cb0104d7167ad0c8532cf30de86b2691d1368638e624b86ed36d6f77638"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.9.2"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
|
||||
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
|
||||
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
|
||||
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
|
||||
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
|
||||
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
|
||||
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
|
||||
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
|
||||
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
|
||||
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
|
||||
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
|
||||
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
|
||||
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
|
||||
],
|
||||
"version": "==5.1.2"
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:98a22fb750c9b9bb46e75e945dc3f61d0ab30d06117cbb21ff9cd1d315fedd3b",
|
||||
"sha256:c504251769031b0dd7dd5cf786050a6050197c6de0d37778c80c08cb04ae8275"
|
||||
],
|
||||
"version": "==3.3.8"
|
||||
},
|
||||
"redlock-py": {
|
||||
"hashes": [
|
||||
"sha256:0b8722c4843ddeabc2fc1dd37c05859e0da29fbce3bd1f6ecc73c98396f139ac"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.8"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57",
|
||||
"sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140"
|
||||
],
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"stups-tokens": {
|
||||
"hashes": [
|
||||
"sha256:317f4386763bac9dd5c0a4c8b0f9f0238dc3fa81de3c6fd1971b6b01662b5750",
|
||||
"sha256:7830ad83ccbfd52a9734608ffcefcca917137ce9480cc91a4fbd321a4aca3160"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.19"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
],
|
||||
"version": "==1.25.3"
|
||||
},
|
||||
"urlobject": {
|
||||
"hashes": [
|
||||
"sha256:47b2e20e6ab9c8366b2f4a3566b6ff4053025dad311c4bb71279bbcfa2430caa"
|
||||
],
|
||||
"version": "==2.4.3"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
"sha256:00d32beac38fcd48d329566f80d39f10ec2ed994efbecfb8dd4b320062d05902",
|
||||
"sha256:0a24d43be6a7dce81bae05292356176d6c46d63e42a0dd3f9504b210a9cfaa43"
|
||||
],
|
||||
"version": "==0.15.6"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
|
||||
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
|
||||
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
|
||||
],
|
||||
"version": "==19.1.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
|
||||
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
|
||||
],
|
||||
"version": "==2019.9.11"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"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"
|
||||
],
|
||||
"version": "==4.5.4"
|
||||
},
|
||||
"coveralls": {
|
||||
"hashes": [
|
||||
"sha256:9bc5a1f92682eef59f688a8f280207190d9a6afb84cef8f567fa47631a784060",
|
||||
"sha256:fb51cddef4bc458de347274116df15d641a735d3f0a580a9472174e2e62f408c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.8.2"
|
||||
},
|
||||
"docopt": {
|
||||
"hashes": [
|
||||
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
|
||||
],
|
||||
"version": "==0.6.2"
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
|
||||
],
|
||||
"version": "==0.3"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548",
|
||||
"sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.7.8"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:652234b6ab8f2506ae58e528b6fbcc668831d3cc758e1bc01ef438d328b68cdb",
|
||||
"sha256:6f264986fb88042bc1f0535fa9a557e6a376cfe5679dc77caac7fe8b5d43d05f"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==0.22"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
|
||||
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
|
||||
],
|
||||
"version": "==7.2.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9",
|
||||
"sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"
|
||||
],
|
||||
"version": "==19.1"
|
||||
},
|
||||
"pipenv": {
|
||||
"hashes": [
|
||||
"sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330",
|
||||
"sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e",
|
||||
"sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2018.11.26"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
|
||||
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
|
||||
],
|
||||
"version": "==0.13.0"
|
||||
},
|
||||
"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:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
|
||||
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
|
||||
],
|
||||
"version": "==2.4.2"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210",
|
||||
"sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.1.2"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6",
|
||||
"sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||
],
|
||||
"version": "==1.12.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||
],
|
||||
"version": "==1.25.3"
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30",
|
||||
"sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2"
|
||||
],
|
||||
"version": "==16.7.5"
|
||||
},
|
||||
"virtualenv-clone": {
|
||||
"hashes": [
|
||||
"sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5",
|
||||
"sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557"
|
||||
],
|
||||
"version": "==0.5.3"
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
|
||||
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
|
||||
],
|
||||
"version": "==0.1.7"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
# This version is replaced during release process.
|
||||
__version__ = '2017.0.dev1'
|
||||
__version__ = "2017.0.dev1"
|
||||
|
||||
@@ -9,9 +9,10 @@ import tokens
|
||||
from requests.auth import AuthBase
|
||||
|
||||
from pykube import HTTPClient, KubeConfig
|
||||
|
||||
# default URL points to kubectl proxy
|
||||
DEFAULT_CLUSTERS = 'http://localhost:8001/'
|
||||
CLUSTER_ID_INVALID_CHARS = re.compile('[^a-z0-9:-]')
|
||||
DEFAULT_CLUSTERS = "http://localhost:8001/"
|
||||
CLUSTER_ID_INVALID_CHARS = re.compile("[^a-z0-9:-]")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,27 +20,27 @@ tokens.configure(from_file_only=True)
|
||||
|
||||
|
||||
def generate_cluster_id(url: str):
|
||||
'''Generate some "cluster ID" from given API server URL'''
|
||||
for prefix in ('https://', 'http://'):
|
||||
"""Generate some "cluster ID" from given API server URL"""
|
||||
for prefix in ("https://", "http://"):
|
||||
if url.startswith(prefix):
|
||||
url = url[len(prefix):]
|
||||
return CLUSTER_ID_INVALID_CHARS.sub('-', url.lower()).strip('-')
|
||||
url = url[len(prefix) :]
|
||||
return CLUSTER_ID_INVALID_CHARS.sub("-", url.lower()).strip("-")
|
||||
|
||||
|
||||
class StaticAuthorizationHeaderAuth(AuthBase):
|
||||
'''Static authentication with given "Authorization" header'''
|
||||
"""Static authentication with given "Authorization" header"""
|
||||
|
||||
def __init__(self, authorization):
|
||||
self.authorization = authorization
|
||||
|
||||
def __call__(self, request):
|
||||
request.headers['Authorization'] = self.authorization
|
||||
request.headers["Authorization"] = self.authorization
|
||||
return request
|
||||
|
||||
|
||||
class OAuthTokenAuth(AuthBase):
|
||||
'''Dynamic authentication using the "tokens" library to load OAuth tokens from file
|
||||
(potentially mounted from a Kubernetes secret)'''
|
||||
"""Dynamic authentication using the "tokens" library to load OAuth tokens from file
|
||||
(potentially mounted from a Kubernetes secret)"""
|
||||
|
||||
def __init__(self, token_name):
|
||||
self.token_name = token_name
|
||||
@@ -47,18 +48,12 @@ class OAuthTokenAuth(AuthBase):
|
||||
|
||||
def __call__(self, request):
|
||||
token = tokens.get(self.token_name)
|
||||
request.headers['Authorization'] = f'Bearer {token}'
|
||||
request.headers["Authorization"] = f"Bearer {token}"
|
||||
return 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
|
||||
@@ -66,7 +61,6 @@ class Cluster:
|
||||
|
||||
|
||||
class StaticClusterDiscoverer:
|
||||
|
||||
def __init__(self, api_server_urls: list):
|
||||
self._clusters = []
|
||||
|
||||
@@ -79,15 +73,18 @@ class StaticClusterDiscoverer:
|
||||
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:
|
||||
@@ -95,47 +92,43 @@ class StaticClusterDiscoverer:
|
||||
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
|
||||
|
||||
|
||||
class ClusterRegistryDiscoverer:
|
||||
|
||||
def __init__(self, cluster_registry_url: str, cache_lifetime=60):
|
||||
self._url = cluster_registry_url
|
||||
self._cache_lifetime = cache_lifetime
|
||||
self._last_cache_refresh = 0
|
||||
self._clusters = []
|
||||
self._session = requests.Session()
|
||||
self._session.auth = OAuthTokenAuth('read-only')
|
||||
self._session.auth = OAuthTokenAuth("read-only")
|
||||
|
||||
def refresh(self):
|
||||
try:
|
||||
response = self._session.get(urljoin(self._url, '/kubernetes-clusters'), timeout=10)
|
||||
response = self._session.get(
|
||||
urljoin(self._url, "/kubernetes-clusters"), timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
clusters = []
|
||||
for row in response.json()['items']:
|
||||
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()
|
||||
@@ -145,7 +138,6 @@ class ClusterRegistryDiscoverer:
|
||||
|
||||
|
||||
class KubeconfigDiscoverer:
|
||||
|
||||
def __init__(self, kubeconfig_path: Path, contexts: set):
|
||||
self._path = kubeconfig_path
|
||||
self._contexts = contexts
|
||||
@@ -162,21 +154,17 @@ class KubeconfigDiscoverer:
|
||||
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
|
||||
|
||||
|
||||
class MockDiscoverer:
|
||||
|
||||
def get_clusters(self):
|
||||
for i in range(3):
|
||||
yield Cluster(
|
||||
f"mock-cluster-{i}",
|
||||
f"mock-cluster-{i}",
|
||||
api_server_url=f"https://kube-{i}.example.org",
|
||||
client=None
|
||||
client=None,
|
||||
)
|
||||
|
||||
@@ -19,57 +19,67 @@ session = requests.Session()
|
||||
# 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 map_node_status(status: dict):
|
||||
return {
|
||||
'addresses': status.get('addresses'),
|
||||
'capacity': status.get('capacity'),
|
||||
'allocatable': status.get('allocatable')
|
||||
"addresses": status.get("addresses"),
|
||||
"capacity": status.get("capacity"),
|
||||
"allocatable": status.get("allocatable"),
|
||||
}
|
||||
|
||||
|
||||
def map_node(node: dict):
|
||||
return {
|
||||
'name': node['metadata']['name'],
|
||||
'labels': node['metadata']['labels'],
|
||||
'status': map_node_status(node['status']),
|
||||
'pods': {}
|
||||
"name": node["metadata"]["name"],
|
||||
"labels": node["metadata"]["labels"],
|
||||
"status": map_node_status(node["status"]),
|
||||
"pods": {},
|
||||
}
|
||||
|
||||
|
||||
def map_pod(pod: dict):
|
||||
return {
|
||||
'name': pod['metadata']['name'],
|
||||
'namespace': pod['metadata']['namespace'],
|
||||
'labels': pod['metadata'].get('labels', {}),
|
||||
'phase': pod['status'].get('phase'),
|
||||
'startTime': pod['status']['startTime'] if 'startTime' in pod['status'] else '',
|
||||
'containers': []
|
||||
"name": pod["metadata"]["name"],
|
||||
"namespace": pod["metadata"]["namespace"],
|
||||
"labels": pod["metadata"].get("labels", {}),
|
||||
"phase": pod["status"].get("phase"),
|
||||
"startTime": pod["status"]["startTime"] if "startTime" in pod["status"] else "",
|
||||
"containers": [],
|
||||
}
|
||||
|
||||
|
||||
def map_container(cont: dict, pod: dict):
|
||||
obj = {'name': cont['name'], 'image': cont['image'], 'resources': cont['resources']}
|
||||
status = list([s for s in pod.get('status', {}).get('containerStatuses', []) if s['name'] == cont['name']])
|
||||
obj = {"name": cont["name"], "image": cont["image"], "resources": cont["resources"]}
|
||||
status = list(
|
||||
[
|
||||
s
|
||||
for s in pod.get("status", {}).get("containerStatuses", [])
|
||||
if s["name"] == cont["name"]
|
||||
]
|
||||
)
|
||||
if status:
|
||||
obj.update(**status[0])
|
||||
return obj
|
||||
|
||||
|
||||
def parse_time(s: str):
|
||||
return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=datetime.timezone.utc).timestamp()
|
||||
return (
|
||||
datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ")
|
||||
.replace(tzinfo=datetime.timezone.utc)
|
||||
.timestamp()
|
||||
)
|
||||
|
||||
|
||||
def query_kubernetes_cluster(cluster):
|
||||
@@ -80,54 +90,66 @@ def query_kubernetes_cluster(cluster):
|
||||
unassigned_pods = {}
|
||||
for node in Node.objects(cluster.client):
|
||||
obj = map_node(node.obj)
|
||||
nodes[obj['name']] = obj
|
||||
nodes[obj["name"]] = obj
|
||||
now = time.time()
|
||||
for pod in Pod.objects(cluster.client, namespace=pykube.all):
|
||||
obj = map_pod(pod.obj)
|
||||
if 'deletionTimestamp' in pod.metadata:
|
||||
obj['deleted'] = parse_time(pod.metadata['deletionTimestamp'])
|
||||
for cont in pod.obj['spec']['containers']:
|
||||
obj['containers'].append(map_container(cont, pod.obj))
|
||||
if obj['phase'] in ('Succeeded', 'Failed'):
|
||||
if "deletionTimestamp" in pod.metadata:
|
||||
obj["deleted"] = parse_time(pod.metadata["deletionTimestamp"])
|
||||
for cont in pod.obj["spec"]["containers"]:
|
||||
obj["containers"].append(map_container(cont, pod.obj))
|
||||
if obj["phase"] in ("Succeeded", "Failed"):
|
||||
last_termination_time = 0
|
||||
for container in obj['containers']:
|
||||
termination_time = container.get('state', {}).get('terminated', {}).get('finishedAt')
|
||||
for container in obj["containers"]:
|
||||
termination_time = (
|
||||
container.get("state", {}).get("terminated", {}).get("finishedAt")
|
||||
)
|
||||
if termination_time:
|
||||
termination_time = parse_time(termination_time)
|
||||
if termination_time > last_termination_time:
|
||||
last_termination_time = termination_time
|
||||
if (last_termination_time and last_termination_time < now - 3600) or (obj.get('reason') == 'Evicted'):
|
||||
if (last_termination_time and last_termination_time < now - 3600) or (
|
||||
obj.get("reason") == "Evicted"
|
||||
):
|
||||
# the job/pod finished more than an hour ago or if it is evicted by cgroup limits
|
||||
# => filter out
|
||||
continue
|
||||
pods_by_namespace_name[(pod.namespace, pod.name)] = obj
|
||||
pod_key = f'{pod.namespace}/{pod.name}'
|
||||
node_name = pod.obj['spec'].get('nodeName')
|
||||
pod_key = f"{pod.namespace}/{pod.name}"
|
||||
node_name = pod.obj["spec"].get("nodeName")
|
||||
if node_name in nodes:
|
||||
nodes[node_name]['pods'][pod_key] = obj
|
||||
nodes[node_name]["pods"][pod_key] = obj
|
||||
else:
|
||||
unassigned_pods[pod_key] = obj
|
||||
|
||||
try:
|
||||
for node_metrics in NodeMetrics.objects(cluster.client):
|
||||
key = node_metrics.name
|
||||
nodes[key]['usage'] = node_metrics.obj.get('usage', {})
|
||||
nodes[key]["usage"] = node_metrics.obj.get("usage", {})
|
||||
except Exception as e:
|
||||
logger.warning('Failed to query node metrics {}: {}'.format(cluster.id, get_short_error_message(e)))
|
||||
logger.warning(
|
||||
"Failed to query node metrics {}: {}".format(
|
||||
cluster.id, get_short_error_message(e)
|
||||
)
|
||||
)
|
||||
try:
|
||||
for pod_metrics in PodMetrics.objects(cluster.client, namespace=pykube.all):
|
||||
key = (pod_metrics.namespace, pod_metrics.name)
|
||||
pod = pods_by_namespace_name.get(key)
|
||||
if pod:
|
||||
for container in pod['containers']:
|
||||
for container_metrics in pod_metrics.obj.get('containers', []):
|
||||
if container['name'] == container_metrics['name']:
|
||||
container['resources']['usage'] = container_metrics['usage']
|
||||
for container in pod["containers"]:
|
||||
for container_metrics in pod_metrics.obj.get("containers", []):
|
||||
if container["name"] == container_metrics["name"]:
|
||||
container["resources"]["usage"] = container_metrics["usage"]
|
||||
except Exception as e:
|
||||
logger.warning('Failed to query pod metrics for cluster {}: {}'.format(cluster.id, get_short_error_message(e)))
|
||||
logger.warning(
|
||||
"Failed to query pod metrics for cluster {}: {}".format(
|
||||
cluster.id, get_short_error_message(e)
|
||||
)
|
||||
)
|
||||
return {
|
||||
'id': cluster_id,
|
||||
'api_server_url': api_server_url,
|
||||
'nodes': nodes,
|
||||
'unassigned_pods': unassigned_pods
|
||||
"id": cluster_id,
|
||||
"api_server_url": api_server_url,
|
||||
"nodes": nodes,
|
||||
"unassigned_pods": unassigned_pods,
|
||||
}
|
||||
|
||||
@@ -24,25 +24,32 @@ from urllib.parse import urljoin
|
||||
from .mock import query_mock_cluster
|
||||
from .kubernetes import query_kubernetes_cluster
|
||||
from .stores import MemoryStore, RedisStore
|
||||
from .cluster_discovery import DEFAULT_CLUSTERS, StaticClusterDiscoverer, ClusterRegistryDiscoverer, KubeconfigDiscoverer, MockDiscoverer
|
||||
from .cluster_discovery import (
|
||||
DEFAULT_CLUSTERS,
|
||||
StaticClusterDiscoverer,
|
||||
ClusterRegistryDiscoverer,
|
||||
KubeconfigDiscoverer,
|
||||
MockDiscoverer,
|
||||
)
|
||||
from .update import update_clusters
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SERVER_STATUS = {'shutdown': False}
|
||||
AUTHORIZE_URL = os.getenv('AUTHORIZE_URL')
|
||||
ACCESS_TOKEN_URL = os.getenv('ACCESS_TOKEN_URL')
|
||||
APP_URL = os.getenv('APP_URL')
|
||||
SCOPE = os.getenv('SCOPE')
|
||||
SERVER_STATUS = {"shutdown": False}
|
||||
AUTHORIZE_URL = os.getenv("AUTHORIZE_URL")
|
||||
ACCESS_TOKEN_URL = os.getenv("ACCESS_TOKEN_URL")
|
||||
APP_URL = os.getenv("APP_URL")
|
||||
SCOPE = os.getenv("SCOPE")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
oauth_blueprint = OAuth2ConsumerBlueprintWithClientRefresh(
|
||||
"oauth", __name__,
|
||||
"oauth",
|
||||
__name__,
|
||||
authorization_url=AUTHORIZE_URL,
|
||||
token_url=ACCESS_TOKEN_URL,
|
||||
scope=SCOPE
|
||||
scope=SCOPE,
|
||||
)
|
||||
app.register_blueprint(oauth_blueprint, url_prefix="/login")
|
||||
|
||||
@@ -50,35 +57,48 @@ app.register_blueprint(oauth_blueprint, url_prefix="/login")
|
||||
def authorize(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
if AUTHORIZE_URL and 'auth_token' not in flask.session and not oauth_blueprint.session.authorized:
|
||||
return redirect(url_for('oauth.login'))
|
||||
if (
|
||||
AUTHORIZE_URL
|
||||
and "auth_token" not in flask.session
|
||||
and not oauth_blueprint.session.authorized
|
||||
):
|
||||
return redirect(url_for("oauth.login"))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
@app.route("/health")
|
||||
def health():
|
||||
if SERVER_STATUS['shutdown']:
|
||||
if SERVER_STATUS["shutdown"]:
|
||||
flask.abort(503)
|
||||
else:
|
||||
return 'OK'
|
||||
return "OK"
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
@authorize
|
||||
def index():
|
||||
static_build_path = Path(__file__).parent / 'static' / 'build'
|
||||
candidates = sorted(static_build_path.glob('app*.js'))
|
||||
static_build_path = Path(__file__).parent / "static" / "build"
|
||||
candidates = sorted(static_build_path.glob("app*.js"))
|
||||
if candidates:
|
||||
app_js = candidates[0].name
|
||||
if app.debug:
|
||||
# cache busting for local development
|
||||
app_js += '?_={}'.format(time.time())
|
||||
app_js += "?_={}".format(time.time())
|
||||
else:
|
||||
logger.error('Could not find JavaScript application bundle app*.js in {}'.format(static_build_path))
|
||||
flask.abort(503, 'JavaScript application bundle not found (missing build)')
|
||||
return flask.render_template('index.html', app_js=app_js, version=kube_ops_view.__version__, app_config_json=json.dumps(app.app_config))
|
||||
logger.error(
|
||||
"Could not find JavaScript application bundle app*.js in {}".format(
|
||||
static_build_path
|
||||
)
|
||||
)
|
||||
flask.abort(503, "JavaScript application bundle not found (missing build)")
|
||||
return flask.render_template(
|
||||
"index.html",
|
||||
app_js=app_js,
|
||||
version=kube_ops_view.__version__,
|
||||
app_config_json=json.dumps(app.app_config),
|
||||
)
|
||||
|
||||
|
||||
def event(cluster_ids: set):
|
||||
@@ -89,55 +109,72 @@ def event(cluster_ids: set):
|
||||
if status:
|
||||
# send the cluster status including last_query_time BEFORE the cluster data
|
||||
# so the UI knows how to render correctly from the start
|
||||
yield 'event: clusterstatus\ndata: ' + json.dumps({'cluster_id': cluster_id, 'status': status}, separators=(',', ':')) + '\n\n'
|
||||
yield "event: clusterstatus\ndata: " + json.dumps(
|
||||
{"cluster_id": cluster_id, "status": status}, separators=(",", ":")
|
||||
) + "\n\n"
|
||||
cluster = app.store.get_cluster_data(cluster_id)
|
||||
if cluster:
|
||||
yield 'event: clusterupdate\ndata: ' + json.dumps(cluster, separators=(',', ':')) + '\n\n'
|
||||
yield 'event: bootstrapend\ndata: \n\n'
|
||||
yield "event: clusterupdate\ndata: " + json.dumps(
|
||||
cluster, separators=(",", ":")
|
||||
) + "\n\n"
|
||||
yield "event: bootstrapend\ndata: \n\n"
|
||||
|
||||
while True:
|
||||
for event_type, event_data in app.store.listen():
|
||||
# hacky, event_data can be delta or full cluster object
|
||||
if not cluster_ids or event_data.get('cluster_id', event_data.get('id')) in cluster_ids:
|
||||
yield 'event: ' + event_type + '\ndata: ' + json.dumps(event_data, separators=(',', ':')) + '\n\n'
|
||||
if (
|
||||
not cluster_ids
|
||||
or event_data.get("cluster_id", event_data.get("id")) in cluster_ids
|
||||
):
|
||||
yield "event: " + event_type + "\ndata: " + json.dumps(
|
||||
event_data, separators=(",", ":")
|
||||
) + "\n\n"
|
||||
|
||||
|
||||
@app.route('/events')
|
||||
@app.route("/events")
|
||||
@authorize
|
||||
def get_events():
|
||||
'''SSE (Server Side Events), for an EventSource'''
|
||||
"""SSE (Server Side Events), for an EventSource"""
|
||||
cluster_ids = set()
|
||||
for _id in flask.request.args.get('cluster_ids', '').split():
|
||||
for _id in flask.request.args.get("cluster_ids", "").split():
|
||||
if _id:
|
||||
cluster_ids.add(_id)
|
||||
return flask.Response(event(cluster_ids), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||
return flask.Response(
|
||||
event(cluster_ids),
|
||||
mimetype="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
@app.route('/screen-tokens', methods=['GET', 'POST'])
|
||||
@app.route("/screen-tokens", methods=["GET", "POST"])
|
||||
@authorize
|
||||
def screen_tokens():
|
||||
new_token = None
|
||||
if flask.request.method == 'POST':
|
||||
if flask.request.method == "POST":
|
||||
new_token = app.store.create_screen_token()
|
||||
return flask.render_template('screen-tokens.html', new_token=new_token)
|
||||
return flask.render_template("screen-tokens.html", new_token=new_token)
|
||||
|
||||
|
||||
@app.route('/screen/<token>')
|
||||
@app.route("/screen/<token>")
|
||||
def redeem_screen_token(token: str):
|
||||
remote_addr = flask.request.headers.get('X-Forwarded-For') or flask.request.remote_addr
|
||||
logger.info('Trying to redeem screen token "{}" for IP {}..'.format(token, remote_addr))
|
||||
remote_addr = (
|
||||
flask.request.headers.get("X-Forwarded-For") or flask.request.remote_addr
|
||||
)
|
||||
logger.info(
|
||||
'Trying to redeem screen token "{}" for IP {}..'.format(token, remote_addr)
|
||||
)
|
||||
try:
|
||||
app.store.redeem_screen_token(token, remote_addr)
|
||||
except:
|
||||
flask.abort(401)
|
||||
flask.session['auth_token'] = (token, '')
|
||||
return redirect(urljoin(APP_URL, '/'))
|
||||
flask.session["auth_token"] = (token, "")
|
||||
return redirect(urljoin(APP_URL, "/"))
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
flask.session.pop('auth_token', None)
|
||||
return redirect(urljoin(APP_URL, '/'))
|
||||
flask.session.pop("auth_token", None)
|
||||
return redirect(urljoin(APP_URL, "/"))
|
||||
|
||||
|
||||
def shutdown():
|
||||
@@ -150,48 +187,120 @@ def shutdown():
|
||||
|
||||
|
||||
def exit_gracefully(signum, frame):
|
||||
logger.info('Received TERM signal, shutting down..')
|
||||
SERVER_STATUS['shutdown'] = True
|
||||
logger.info("Received TERM signal, shutting down..")
|
||||
SERVER_STATUS["shutdown"] = True
|
||||
gevent.spawn(shutdown)
|
||||
|
||||
|
||||
def print_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
click.echo('Kubernetes Operational View {}'.format(kube_ops_view.__version__))
|
||||
click.echo("Kubernetes Operational View {}".format(kube_ops_view.__version__))
|
||||
ctx.exit()
|
||||
|
||||
|
||||
class CommaSeparatedValues(click.ParamType):
|
||||
name = 'comma_separated_values'
|
||||
name = "comma_separated_values"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if isinstance(value, str):
|
||||
values = filter(None, value.split(','))
|
||||
values = filter(None, value.split(","))
|
||||
else:
|
||||
values = value
|
||||
return values
|
||||
|
||||
|
||||
@click.command(context_settings={'help_option_names': ['-h', '--help']})
|
||||
@click.option('-V', '--version', is_flag=True, callback=print_version, expose_value=False, is_eager=True,
|
||||
help='Print the current version number and exit.')
|
||||
@click.option('-p', '--port', type=int, help='HTTP port to listen on (default: 8080)', envvar='SERVER_PORT', default=8080)
|
||||
@click.option('-d', '--debug', is_flag=True, help='Run in debugging mode', envvar='DEBUG')
|
||||
@click.option('-m', '--mock', is_flag=True, help='Mock Kubernetes clusters', envvar='MOCK')
|
||||
@click.option('--secret-key', help='Secret key for session cookies', envvar='SECRET_KEY', default='development')
|
||||
@click.option('--redis-url', help='Redis URL to use for pub/sub and job locking', envvar='REDIS_URL')
|
||||
@click.option('--clusters', type=CommaSeparatedValues(),
|
||||
help='Comma separated list of Kubernetes API server URLs (default: {})'.format(DEFAULT_CLUSTERS), envvar='CLUSTERS')
|
||||
@click.option('--cluster-registry-url', help='URL to cluster registry', envvar='CLUSTER_REGISTRY_URL')
|
||||
@click.option('--kubeconfig-path', type=click.Path(exists=True), help='Path to kubeconfig file', envvar='KUBECONFIG_PATH')
|
||||
@click.option('--kubeconfig-contexts', type=CommaSeparatedValues(),
|
||||
help='List of kubeconfig contexts to use (default: use all defined contexts)', envvar='KUBECONFIG_CONTEXTS')
|
||||
@click.option('--query-interval', type=float, help='Interval in seconds for querying clusters (default: 5)', envvar='QUERY_INTERVAL', default=5)
|
||||
@click.option('--node-link-url-template', help='Template for target URL when clicking on a Node', envvar='NODE_LINK_URL_TEMPLATE')
|
||||
@click.option('--pod-link-url-template', help='Template for target URL when clicking on a Pod', envvar='POD_LINK_URL_TEMPLATE')
|
||||
def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_registry_url, kubeconfig_path, kubeconfig_contexts: list, query_interval,
|
||||
node_link_url_template: str, pod_link_url_template: str):
|
||||
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
|
||||
@click.option(
|
||||
"-V",
|
||||
"--version",
|
||||
is_flag=True,
|
||||
callback=print_version,
|
||||
expose_value=False,
|
||||
is_eager=True,
|
||||
help="Print the current version number and exit.",
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--port",
|
||||
type=int,
|
||||
help="HTTP port to listen on (default: 8080)",
|
||||
envvar="SERVER_PORT",
|
||||
default=8080,
|
||||
)
|
||||
@click.option(
|
||||
"-d", "--debug", is_flag=True, help="Run in debugging mode", envvar="DEBUG"
|
||||
)
|
||||
@click.option(
|
||||
"-m", "--mock", is_flag=True, help="Mock Kubernetes clusters", envvar="MOCK"
|
||||
)
|
||||
@click.option(
|
||||
"--secret-key",
|
||||
help="Secret key for session cookies",
|
||||
envvar="SECRET_KEY",
|
||||
default="development",
|
||||
)
|
||||
@click.option(
|
||||
"--redis-url",
|
||||
help="Redis URL to use for pub/sub and job locking",
|
||||
envvar="REDIS_URL",
|
||||
)
|
||||
@click.option(
|
||||
"--clusters",
|
||||
type=CommaSeparatedValues(),
|
||||
help="Comma separated list of Kubernetes API server URLs (default: {})".format(
|
||||
DEFAULT_CLUSTERS
|
||||
),
|
||||
envvar="CLUSTERS",
|
||||
)
|
||||
@click.option(
|
||||
"--cluster-registry-url",
|
||||
help="URL to cluster registry",
|
||||
envvar="CLUSTER_REGISTRY_URL",
|
||||
)
|
||||
@click.option(
|
||||
"--kubeconfig-path",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to kubeconfig file",
|
||||
envvar="KUBECONFIG_PATH",
|
||||
)
|
||||
@click.option(
|
||||
"--kubeconfig-contexts",
|
||||
type=CommaSeparatedValues(),
|
||||
help="List of kubeconfig contexts to use (default: use all defined contexts)",
|
||||
envvar="KUBECONFIG_CONTEXTS",
|
||||
)
|
||||
@click.option(
|
||||
"--query-interval",
|
||||
type=float,
|
||||
help="Interval in seconds for querying clusters (default: 5)",
|
||||
envvar="QUERY_INTERVAL",
|
||||
default=5,
|
||||
)
|
||||
@click.option(
|
||||
"--node-link-url-template",
|
||||
help="Template for target URL when clicking on a Node",
|
||||
envvar="NODE_LINK_URL_TEMPLATE",
|
||||
)
|
||||
@click.option(
|
||||
"--pod-link-url-template",
|
||||
help="Template for target URL when clicking on a Pod",
|
||||
envvar="POD_LINK_URL_TEMPLATE",
|
||||
)
|
||||
def main(
|
||||
port,
|
||||
debug,
|
||||
mock,
|
||||
secret_key,
|
||||
redis_url,
|
||||
clusters: list,
|
||||
cluster_registry_url,
|
||||
kubeconfig_path,
|
||||
kubeconfig_contexts: list,
|
||||
query_interval,
|
||||
node_link_url_template: str,
|
||||
pod_link_url_template: str,
|
||||
):
|
||||
logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
|
||||
|
||||
store = RedisStore(redis_url) if redis_url else MemoryStore()
|
||||
@@ -199,7 +308,10 @@ def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_regis
|
||||
app.debug = debug
|
||||
app.secret_key = secret_key
|
||||
app.store = store
|
||||
app.app_config = {'node_link_url_template': node_link_url_template, 'pod_link_url_template': pod_link_url_template}
|
||||
app.app_config = {
|
||||
"node_link_url_template": node_link_url_template,
|
||||
"pod_link_url_template": pod_link_url_template,
|
||||
}
|
||||
|
||||
if mock:
|
||||
cluster_query = query_mock_cluster
|
||||
@@ -209,14 +321,23 @@ def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_regis
|
||||
if cluster_registry_url:
|
||||
discoverer = ClusterRegistryDiscoverer(cluster_registry_url)
|
||||
elif kubeconfig_path:
|
||||
discoverer = KubeconfigDiscoverer(Path(kubeconfig_path), set(kubeconfig_contexts or []))
|
||||
discoverer = KubeconfigDiscoverer(
|
||||
Path(kubeconfig_path), set(kubeconfig_contexts or [])
|
||||
)
|
||||
else:
|
||||
api_server_urls = clusters or []
|
||||
discoverer = StaticClusterDiscoverer(api_server_urls)
|
||||
|
||||
gevent.spawn(update_clusters, cluster_discoverer=discoverer, query_cluster=cluster_query, store=store, query_interval=query_interval, debug=debug)
|
||||
gevent.spawn(
|
||||
update_clusters,
|
||||
cluster_discoverer=discoverer,
|
||||
query_cluster=cluster_query,
|
||||
store=store,
|
||||
query_interval=query_interval,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
signal.signal(signal.SIGTERM, exit_gracefully)
|
||||
http_server = gevent.pywsgi.WSGIServer(('0.0.0.0', port), app)
|
||||
logger.info('Listening on :{}..'.format(port))
|
||||
http_server = gevent.pywsgi.WSGIServer(("0.0.0.0", port), app)
|
||||
logger.info("Listening on :{}..".format(port))
|
||||
http_server.serve_forever()
|
||||
|
||||
@@ -4,34 +4,33 @@ import string
|
||||
|
||||
|
||||
def hash_int(x: int):
|
||||
x = ((x >> 16) ^ x) * 0x45d9f3b
|
||||
x = ((x >> 16) ^ x) * 0x45d9f3b
|
||||
x = ((x >> 16) ^ x) * 0x45D9F3B
|
||||
x = ((x >> 16) ^ x) * 0x45D9F3B
|
||||
x = (x >> 16) ^ x
|
||||
return x
|
||||
|
||||
|
||||
def generate_mock_pod(index: int, i: int, j: int):
|
||||
names = [
|
||||
'agent-cooper',
|
||||
'black-lodge',
|
||||
'bob',
|
||||
'bobby-briggs',
|
||||
'laura-palmer',
|
||||
'leland-palmer',
|
||||
'log-lady',
|
||||
'sheriff-truman',
|
||||
"agent-cooper",
|
||||
"black-lodge",
|
||||
"bob",
|
||||
"bobby-briggs",
|
||||
"laura-palmer",
|
||||
"leland-palmer",
|
||||
"log-lady",
|
||||
"sheriff-truman",
|
||||
]
|
||||
labels = {
|
||||
'env': ['prod', 'dev'],
|
||||
'owner': ['x-wing', 'iris']
|
||||
}
|
||||
pod_phases = ['Pending', 'Running', 'Running', 'Failed']
|
||||
labels = {"env": ["prod", "dev"], "owner": ["x-wing", "iris"]}
|
||||
pod_phases = ["Pending", "Running", "Running", "Failed"]
|
||||
|
||||
pod_labels = {}
|
||||
for li, k in enumerate(labels):
|
||||
v = labels[k]
|
||||
label_choice = hash_int((index + 1) * (i + 1) * (j + 1) * (li + 1)) % (len(v) + 1)
|
||||
if(label_choice != 0):
|
||||
label_choice = hash_int((index + 1) * (i + 1) * (j + 1) * (li + 1)) % (
|
||||
len(v) + 1
|
||||
)
|
||||
if label_choice != 0:
|
||||
pod_labels[k] = v[label_choice - 1]
|
||||
|
||||
phase = pod_phases[hash_int((index + 1) * (i + 1) * (j + 1)) % len(pod_phases)]
|
||||
@@ -44,41 +43,53 @@ def generate_mock_pod(index: int, i: int, j: int):
|
||||
usage_cpu = max(requests_cpu + random.randint(-30, 30), 1)
|
||||
usage_memory = max(requests_memory + random.randint(-64, 128), 1)
|
||||
container = {
|
||||
'name': 'myapp',
|
||||
'image': 'foo/bar/{}'.format(j),
|
||||
'resources': {
|
||||
'requests': {'cpu': f'{requests_cpu}m', 'memory': f'{requests_memory}Mi'},
|
||||
'limits': {},
|
||||
'usage': {'cpu': f'{usage_cpu}m', 'memory': f'{usage_memory}Mi'},
|
||||
"name": "myapp",
|
||||
"image": "foo/bar/{}".format(j),
|
||||
"resources": {
|
||||
"requests": {
|
||||
"cpu": f"{requests_cpu}m",
|
||||
"memory": f"{requests_memory}Mi",
|
||||
},
|
||||
'ready': True,
|
||||
'state': {'running': {}}
|
||||
"limits": {},
|
||||
"usage": {"cpu": f"{usage_cpu}m", "memory": f"{usage_memory}Mi"},
|
||||
},
|
||||
"ready": True,
|
||||
"state": {"running": {}},
|
||||
}
|
||||
if phase == 'Running':
|
||||
if phase == "Running":
|
||||
if j % 13 == 0:
|
||||
container.update(**{'ready': False, 'state': {'waiting': {'reason': 'CrashLoopBackOff'}}})
|
||||
container.update(
|
||||
**{
|
||||
"ready": False,
|
||||
"state": {"waiting": {"reason": "CrashLoopBackOff"}},
|
||||
}
|
||||
)
|
||||
elif j % 7 == 0:
|
||||
container.update(**{'ready': False, 'state': {'running': {}}, 'restartCount': 3})
|
||||
elif phase == 'Failed':
|
||||
del container['state']
|
||||
del container['ready']
|
||||
container.update(
|
||||
**{"ready": False, "state": {"running": {}}, "restartCount": 3}
|
||||
)
|
||||
elif phase == "Failed":
|
||||
del container["state"]
|
||||
del container["ready"]
|
||||
containers.append(container)
|
||||
pod = {
|
||||
'name': '{}-{}-{}'.format(names[hash_int((i + 1) * (j + 1)) % len(names)], i, j),
|
||||
'namespace': 'kube-system' if j < 3 else 'default',
|
||||
'labels': pod_labels,
|
||||
'phase': phase,
|
||||
'containers': containers
|
||||
"name": "{}-{}-{}".format(
|
||||
names[hash_int((i + 1) * (j + 1)) % len(names)], i, j
|
||||
),
|
||||
"namespace": "kube-system" if j < 3 else "default",
|
||||
"labels": pod_labels,
|
||||
"phase": phase,
|
||||
"containers": containers,
|
||||
}
|
||||
if phase == 'Running' and j % 17 == 0:
|
||||
pod['deleted'] = 123
|
||||
if phase == "Running" and j % 17 == 0:
|
||||
pod["deleted"] = 123
|
||||
|
||||
return pod
|
||||
|
||||
|
||||
def query_mock_cluster(cluster):
|
||||
'''Generate deterministic (no randomness!) mock data'''
|
||||
index = int(cluster.id.split('-')[-1])
|
||||
"""Generate deterministic (no randomness!) mock data"""
|
||||
index = int(cluster.id.split("-")[-1])
|
||||
nodes = {}
|
||||
for i in range(10):
|
||||
# add/remove the second to last node every 13 seconds
|
||||
@@ -88,11 +99,11 @@ def query_mock_cluster(cluster):
|
||||
# only the first two clusters have master nodes
|
||||
if i < 2 and index < 2:
|
||||
if index == 0:
|
||||
labels['kubernetes.io/role'] = 'master'
|
||||
labels["kubernetes.io/role"] = "master"
|
||||
elif index == 1:
|
||||
labels['node-role.kubernetes.io/master'] = ''
|
||||
labels["node-role.kubernetes.io/master"] = ""
|
||||
else:
|
||||
labels['master'] = 'true'
|
||||
labels["master"] = "true"
|
||||
pods = {}
|
||||
for j in range(hash_int((index + 1) * (i + 1)) % 32):
|
||||
# add/remove some pods every 7 seconds
|
||||
@@ -100,7 +111,7 @@ def query_mock_cluster(cluster):
|
||||
pass
|
||||
else:
|
||||
pod = generate_mock_pod(index, i, j)
|
||||
pods['{}/{}'.format(pod['namespace'], pod['name'])] = pod
|
||||
pods["{}/{}".format(pod["namespace"], pod["name"])] = pod
|
||||
|
||||
# use data from containers (usage)
|
||||
usage_cpu = 0
|
||||
@@ -111,27 +122,27 @@ def query_mock_cluster(cluster):
|
||||
usage_memory += int(c["resources"]["usage"]["memory"].split("Mi")[0])
|
||||
|
||||
# generate longer name for a node
|
||||
suffix = ''.join(
|
||||
suffix = "".join(
|
||||
[random.choice(string.ascii_letters) for n in range(random.randint(1, 20))]
|
||||
)
|
||||
|
||||
node = {
|
||||
'name': f'node-{i}-{suffix}',
|
||||
'labels': labels,
|
||||
'status': {
|
||||
'capacity': {'cpu': '8', 'memory': '64Gi', 'pods': '110'},
|
||||
'allocatable': {'cpu': '7800m', 'memory': '62Gi'}
|
||||
"name": f"node-{i}-{suffix}",
|
||||
"labels": labels,
|
||||
"status": {
|
||||
"capacity": {"cpu": "8", "memory": "64Gi", "pods": "110"},
|
||||
"allocatable": {"cpu": "7800m", "memory": "62Gi"},
|
||||
},
|
||||
'pods': pods,
|
||||
"pods": pods,
|
||||
# get data from containers (usage)
|
||||
'usage': {'cpu': f'{usage_cpu}m', 'memory': f'{usage_memory}Mi'}
|
||||
"usage": {"cpu": f"{usage_cpu}m", "memory": f"{usage_memory}Mi"},
|
||||
}
|
||||
nodes[node['name']] = node
|
||||
nodes[node["name"]] = node
|
||||
pod = generate_mock_pod(index, 11, index)
|
||||
unassigned_pods = {'{}/{}'.format(pod['namespace'], pod['name']): pod}
|
||||
unassigned_pods = {"{}/{}".format(pod["namespace"], pod["name"]): pod}
|
||||
return {
|
||||
'id': 'mock-cluster-{}'.format(index),
|
||||
'api_server_url': 'https://kube-{}.example.org'.format(index),
|
||||
'nodes': nodes,
|
||||
'unassigned_pods': unassigned_pods
|
||||
"id": "mock-cluster-{}".format(index),
|
||||
"api_server_url": "https://kube-{}.example.org".format(index),
|
||||
"nodes": nodes,
|
||||
"unassigned_pods": unassigned_pods,
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ import os
|
||||
from flask_dance.consumer import OAuth2ConsumerBlueprint
|
||||
|
||||
|
||||
CREDENTIALS_DIR = os.getenv('CREDENTIALS_DIR', '')
|
||||
CREDENTIALS_DIR = os.getenv("CREDENTIALS_DIR", "")
|
||||
|
||||
|
||||
class OAuth2ConsumerBlueprintWithClientRefresh(OAuth2ConsumerBlueprint):
|
||||
'''Same as flask_dance.consumer.OAuth2ConsumerBlueprint, but loads client credentials from file'''
|
||||
"""Same as flask_dance.consumer.OAuth2ConsumerBlueprint, but loads client credentials from file"""
|
||||
|
||||
def refresh_credentials(self):
|
||||
with open(os.path.join(CREDENTIALS_DIR, 'authcode-client-id')) as fd:
|
||||
with open(os.path.join(CREDENTIALS_DIR, "authcode-client-id")) as fd:
|
||||
# note that we need to set two attributes because of how OAuth2ConsumerBlueprint works :-/
|
||||
self._client_id = self.client_id = fd.read().strip()
|
||||
with open(os.path.join(CREDENTIALS_DIR, 'authcode-client-secret')) as fd:
|
||||
with open(os.path.join(CREDENTIALS_DIR, "authcode-client-secret")) as fd:
|
||||
self.client_secret = fd.read().strip()
|
||||
|
||||
def login(self):
|
||||
|
||||
@@ -14,52 +14,55 @@ ONE_YEAR = 3600 * 24 * 365
|
||||
|
||||
|
||||
def generate_token(n: int):
|
||||
'''Generate a random ASCII token of length n'''
|
||||
"""Generate a random ASCII token of length n"""
|
||||
# uses os.urandom()
|
||||
rng = random.SystemRandom()
|
||||
return ''.join([rng.choice(string.ascii_letters + string.digits) for i in range(n)])
|
||||
return "".join([rng.choice(string.ascii_letters + string.digits) for i in range(n)])
|
||||
|
||||
|
||||
def generate_token_data():
|
||||
'''Generate screen token data for storing'''
|
||||
"""Generate screen token data for storing"""
|
||||
token = generate_token(10)
|
||||
now = time.time()
|
||||
return {'token': token, 'created': now, 'expires': now + ONE_YEAR}
|
||||
return {"token": token, "created": now, "expires": now + ONE_YEAR}
|
||||
|
||||
|
||||
def check_token(token: str, remote_addr: str, data: dict):
|
||||
'''Check whether the given screen token is valid, raises exception if not'''
|
||||
"""Check whether the given screen token is valid, raises exception if not"""
|
||||
now = time.time()
|
||||
if data and now < data['expires'] and data.get('remote_addr', remote_addr) == remote_addr:
|
||||
data['remote_addr'] = remote_addr
|
||||
if (
|
||||
data
|
||||
and now < data["expires"]
|
||||
and data.get("remote_addr", remote_addr) == remote_addr
|
||||
):
|
||||
data["remote_addr"] = remote_addr
|
||||
return data
|
||||
else:
|
||||
raise ValueError('Invalid token')
|
||||
raise ValueError("Invalid token")
|
||||
|
||||
|
||||
class AbstractStore:
|
||||
|
||||
def get_cluster_ids(self):
|
||||
return self.get('cluster-ids') or []
|
||||
return self.get("cluster-ids") or []
|
||||
|
||||
def set_cluster_ids(self, cluster_ids: set):
|
||||
self.set('cluster-ids', list(sorted(cluster_ids)))
|
||||
self.set("cluster-ids", list(sorted(cluster_ids)))
|
||||
|
||||
def get_cluster_status(self, cluster_id: str) -> dict:
|
||||
return self.get('clusters:{}:status'.format(cluster_id)) or {}
|
||||
return self.get("clusters:{}:status".format(cluster_id)) or {}
|
||||
|
||||
def set_cluster_status(self, cluster_id: str, status: dict):
|
||||
self.set('clusters:{}:status'.format(cluster_id), status)
|
||||
self.set("clusters:{}:status".format(cluster_id), status)
|
||||
|
||||
def get_cluster_data(self, cluster_id: str) -> dict:
|
||||
return self.get('clusters:{}:data'.format(cluster_id)) or {}
|
||||
return self.get("clusters:{}:data".format(cluster_id)) or {}
|
||||
|
||||
def set_cluster_data(self, cluster_id: str, data: dict):
|
||||
self.set('clusters:{}:data'.format(cluster_id), data)
|
||||
self.set("clusters:{}:data".format(cluster_id), data)
|
||||
|
||||
|
||||
class MemoryStore(AbstractStore):
|
||||
'''Memory-only backend, mostly useful for local debugging'''
|
||||
"""Memory-only backend, mostly useful for local debugging"""
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
@@ -74,7 +77,7 @@ class MemoryStore(AbstractStore):
|
||||
|
||||
def acquire_lock(self):
|
||||
# no-op for memory store
|
||||
return 'fake-lock'
|
||||
return "fake-lock"
|
||||
|
||||
def release_lock(self, lock):
|
||||
# no op for memory store
|
||||
@@ -96,7 +99,7 @@ class MemoryStore(AbstractStore):
|
||||
|
||||
def create_screen_token(self):
|
||||
data = generate_token_data()
|
||||
token = data['token']
|
||||
token = data["token"]
|
||||
self._screen_tokens[token] = data
|
||||
return token
|
||||
|
||||
@@ -107,51 +110,54 @@ class MemoryStore(AbstractStore):
|
||||
|
||||
|
||||
class RedisStore(AbstractStore):
|
||||
'''Redis-based backend for deployments with replicas > 1'''
|
||||
"""Redis-based backend for deployments with replicas > 1"""
|
||||
|
||||
def __init__(self, url: str):
|
||||
logger.info('Connecting to Redis on {}..'.format(url))
|
||||
logger.info("Connecting to Redis on {}..".format(url))
|
||||
self._redis = redis.StrictRedis.from_url(url)
|
||||
self._redlock = Redlock([url])
|
||||
|
||||
def set(self, key, value):
|
||||
self._redis.set(key, json.dumps(value, separators=(',', ':')))
|
||||
self._redis.set(key, json.dumps(value, separators=(",", ":")))
|
||||
|
||||
def get(self, key):
|
||||
value = self._redis.get(key)
|
||||
if value:
|
||||
return json.loads(value.decode('utf-8'))
|
||||
return json.loads(value.decode("utf-8"))
|
||||
|
||||
def acquire_lock(self):
|
||||
return self._redlock.lock('update', 10000)
|
||||
return self._redlock.lock("update", 10000)
|
||||
|
||||
def release_lock(self, lock):
|
||||
self._redlock.unlock(lock)
|
||||
|
||||
def publish(self, event_type, event_data):
|
||||
self._redis.publish('default', '{}:{}'.format(event_type, json.dumps(event_data, separators=(',', ':'))))
|
||||
self._redis.publish(
|
||||
"default",
|
||||
"{}:{}".format(event_type, json.dumps(event_data, separators=(",", ":"))),
|
||||
)
|
||||
|
||||
def listen(self):
|
||||
p = self._redis.pubsub()
|
||||
p.subscribe('default')
|
||||
p.subscribe("default")
|
||||
for message in p.listen():
|
||||
if message['type'] == 'message':
|
||||
event_type, data = message['data'].decode('utf-8').split(':', 1)
|
||||
if message["type"] == "message":
|
||||
event_type, data = message["data"].decode("utf-8").split(":", 1)
|
||||
yield (event_type, json.loads(data))
|
||||
|
||||
def create_screen_token(self):
|
||||
'''Generate a new screen token and store it in Redis'''
|
||||
"""Generate a new screen token and store it in Redis"""
|
||||
data = generate_token_data()
|
||||
token = data['token']
|
||||
self._redis.set('screen-tokens:{}'.format(token), json.dumps(data))
|
||||
token = data["token"]
|
||||
self._redis.set("screen-tokens:{}".format(token), json.dumps(data))
|
||||
return token
|
||||
|
||||
def redeem_screen_token(self, token: str, remote_addr: str):
|
||||
'''Validate the given token and bind it to the IP'''
|
||||
redis_key = 'screen-tokens:{}'.format(token)
|
||||
"""Validate the given token and bind it to the IP"""
|
||||
redis_key = "screen-tokens:{}".format(token)
|
||||
data = self._redis.get(redis_key)
|
||||
if not data:
|
||||
raise ValueError('Invalid token')
|
||||
data = json.loads(data.decode('utf-8'))
|
||||
raise ValueError("Invalid token")
|
||||
data = json.loads(data.decode("utf-8"))
|
||||
data = check_token(token, remote_addr, data)
|
||||
self._redis.set(redis_key, json.dumps(data))
|
||||
|
||||
@@ -18,21 +18,30 @@ def calculate_backoff(tries: int):
|
||||
def handle_query_failure(e: Exception, cluster, backoff: dict):
|
||||
if not backoff:
|
||||
backoff = {}
|
||||
tries = backoff.get('tries', 0) + 1
|
||||
backoff['tries'] = tries
|
||||
tries = backoff.get("tries", 0) + 1
|
||||
backoff["tries"] = tries
|
||||
wait_seconds = calculate_backoff(tries)
|
||||
backoff['next_try'] = time.time() + wait_seconds
|
||||
backoff["next_try"] = time.time() + wait_seconds
|
||||
message = get_short_error_message(e)
|
||||
if isinstance(e, requests.exceptions.RequestException):
|
||||
log = logger.error
|
||||
else:
|
||||
log = logger.exception
|
||||
log('Failed to query cluster {} ({}): {} (try {}, wait {} seconds)'.format(
|
||||
cluster.id, cluster.api_server_url, message, tries, round(wait_seconds)))
|
||||
log(
|
||||
"Failed to query cluster {} ({}): {} (try {}, wait {} seconds)".format(
|
||||
cluster.id, cluster.api_server_url, message, tries, round(wait_seconds)
|
||||
)
|
||||
)
|
||||
return backoff
|
||||
|
||||
|
||||
def update_clusters(cluster_discoverer, query_cluster: callable, store, query_interval: float=5, debug: bool=False):
|
||||
def update_clusters(
|
||||
cluster_discoverer,
|
||||
query_cluster: callable,
|
||||
store,
|
||||
query_interval: float = 5,
|
||||
debug: bool = False,
|
||||
):
|
||||
while True:
|
||||
lock = store.acquire_lock()
|
||||
if lock:
|
||||
@@ -43,42 +52,65 @@ def update_clusters(cluster_discoverer, query_cluster: callable, store, query_in
|
||||
cluster_ids.add(cluster.id)
|
||||
status = store.get_cluster_status(cluster.id)
|
||||
now = time.time()
|
||||
if now < status.get('last_query_time', 0) + query_interval:
|
||||
if now < status.get("last_query_time", 0) + query_interval:
|
||||
continue
|
||||
backoff = status.get('backoff')
|
||||
if backoff and now < backoff['next_try']:
|
||||
backoff = status.get("backoff")
|
||||
if backoff and now < backoff["next_try"]:
|
||||
# cluster is still in backoff, skip
|
||||
continue
|
||||
try:
|
||||
logger.debug('Querying cluster {} ({})..'.format(cluster.id, cluster.api_server_url))
|
||||
logger.debug(
|
||||
"Querying cluster {} ({})..".format(
|
||||
cluster.id, cluster.api_server_url
|
||||
)
|
||||
)
|
||||
data = query_cluster(cluster)
|
||||
except Exception as e:
|
||||
backoff = handle_query_failure(e, cluster, backoff)
|
||||
status['backoff'] = backoff
|
||||
store.publish('clusterstatus', {'cluster_id': cluster.id, 'status': status})
|
||||
status["backoff"] = backoff
|
||||
store.publish(
|
||||
"clusterstatus",
|
||||
{"cluster_id": cluster.id, "status": status},
|
||||
)
|
||||
else:
|
||||
status['last_query_time'] = now
|
||||
status["last_query_time"] = now
|
||||
if backoff:
|
||||
logger.info('Cluster {} ({}) recovered after {} tries.'.format(cluster.id, cluster.api_server_url, backoff['tries']))
|
||||
del status['backoff']
|
||||
old_data = store.get_cluster_data(data['id'])
|
||||
logger.info(
|
||||
"Cluster {} ({}) recovered after {} tries.".format(
|
||||
cluster.id, cluster.api_server_url, backoff["tries"]
|
||||
)
|
||||
)
|
||||
del status["backoff"]
|
||||
old_data = store.get_cluster_data(data["id"])
|
||||
if old_data:
|
||||
# https://pikacode.com/phijaro/json_delta/ticket/11/
|
||||
# diff is extremely slow without array_align=False
|
||||
delta = json_delta.diff(old_data, data, verbose=debug, array_align=False)
|
||||
store.publish('clusterdelta', {'cluster_id': cluster.id, 'delta': delta})
|
||||
delta = json_delta.diff(
|
||||
old_data, data, verbose=debug, array_align=False
|
||||
)
|
||||
store.publish(
|
||||
"clusterdelta",
|
||||
{"cluster_id": cluster.id, "delta": delta},
|
||||
)
|
||||
if delta:
|
||||
store.set_cluster_data(cluster.id, data)
|
||||
else:
|
||||
logger.info('Discovered new cluster {} ({}).'.format(cluster.id, cluster.api_server_url))
|
||||
logger.info(
|
||||
"Discovered new cluster {} ({}).".format(
|
||||
cluster.id, cluster.api_server_url
|
||||
)
|
||||
)
|
||||
# first send status with last_query_time!
|
||||
store.publish('clusterstatus', {'cluster_id': cluster.id, 'status': status})
|
||||
store.publish('clusterupdate', data)
|
||||
store.publish(
|
||||
"clusterstatus",
|
||||
{"cluster_id": cluster.id, "status": status},
|
||||
)
|
||||
store.publish("clusterupdate", data)
|
||||
store.set_cluster_data(cluster.id, data)
|
||||
store.set_cluster_status(cluster.id, status)
|
||||
store.set_cluster_ids(cluster_ids)
|
||||
except:
|
||||
logger.exception('Failed to update')
|
||||
logger.exception("Failed to update")
|
||||
finally:
|
||||
store.release_lock(lock)
|
||||
# sleep 1-2 seconds
|
||||
|
||||
@@ -2,11 +2,11 @@ import requests.exceptions
|
||||
|
||||
|
||||
def get_short_error_message(e: Exception):
|
||||
'''Generate a reasonable short message why the HTTP request failed'''
|
||||
"""Generate a reasonable short message why the HTTP request failed"""
|
||||
|
||||
if isinstance(e, requests.exceptions.RequestException) and e.response is not None:
|
||||
# e.g. "401 Unauthorized"
|
||||
return '{} {}'.format(e.response.status_code, e.response.reason)
|
||||
return "{} {}".format(e.response.status_code, e.response.reason)
|
||||
elif isinstance(e, requests.exceptions.ConnectionError):
|
||||
# e.g. "ConnectionError" or "ConnectTimeout"
|
||||
return e.__class__.__name__
|
||||
|
||||
@@ -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)
|
||||
1056
poetry.lock
generated
Normal file
1056
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
pyproject.toml
Normal file
26
pyproject.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[tool]
|
||||
[tool.poetry]
|
||||
name = "kube-ops-view"
|
||||
version = "2020.0.dev1"
|
||||
description = "Kubernetes Operational View - read-only system dashboard for multiple K8s clusters"
|
||||
authors = ["Henning Jacobs <henning@jacobs1.de>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.7"
|
||||
click = "*"
|
||||
flask = "*"
|
||||
flask-dance = "*"
|
||||
gevent = "*"
|
||||
json-delta = ">=2.0"
|
||||
pykube-ng = "*"
|
||||
redlock-py = "*"
|
||||
requests = "*"
|
||||
stups-tokens = ">=1.1.19"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
coveralls = "*"
|
||||
flake8 = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
black = "^19.10b0"
|
||||
mypy = "^0.761"
|
||||
78
setup.py
78
setup.py
@@ -1,78 +0,0 @@
|
||||
import sys
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_version(package):
|
||||
with (Path(package) / '__init__.py').open() as fd:
|
||||
for line in fd:
|
||||
if line.startswith('__version__ = '):
|
||||
return line.split()[-1].strip().strip("'")
|
||||
|
||||
|
||||
version = read_version('kube_ops_view')
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
|
||||
user_options = [('cov-html=', None, 'Generate junit html report')]
|
||||
|
||||
def initialize_options(self):
|
||||
TestCommand.initialize_options(self)
|
||||
self.cov = None
|
||||
self.pytest_args = ['--cov', 'kube_ops_view', '--cov-report', 'term-missing', '-v']
|
||||
self.cov_html = False
|
||||
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
if self.cov_html:
|
||||
self.pytest_args.extend(['--cov-report', 'html'])
|
||||
self.pytest_args.extend(['tests'])
|
||||
|
||||
def run_tests(self):
|
||||
import pytest
|
||||
|
||||
errno = pytest.main(self.pytest_args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
def readme():
|
||||
return open('README.rst', encoding='utf-8').read()
|
||||
|
||||
|
||||
tests_require = [
|
||||
'pytest',
|
||||
'pytest-cov'
|
||||
]
|
||||
|
||||
setup(
|
||||
name='kube-ops-view',
|
||||
packages=find_packages(),
|
||||
version=version,
|
||||
description='Kubernetes Operational View - read-only system dashboard for multiple K8s clusters',
|
||||
long_description=readme(),
|
||||
author='Henning Jacobs',
|
||||
url='https://github.com/hjacobs/kube-ops-view',
|
||||
keywords='kubernetes operations dashboard view k8s',
|
||||
license='GNU General Public License v3 (GPLv3)',
|
||||
tests_require=tests_require,
|
||||
extras_require={'tests': tests_require},
|
||||
cmdclass={'test': PyTest},
|
||||
test_suite='tests',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Topic :: System :: Clustering',
|
||||
'Topic :: System :: Monitoring',
|
||||
],
|
||||
include_package_data=True, # needed to include JavaScript (see MANIFEST.in)
|
||||
entry_points={'console_scripts': ['kube-ops-view = kube_ops_view.main:main']}
|
||||
)
|
||||
26
tox.ini
26
tox.ini
@@ -1,26 +0,0 @@
|
||||
[tox]
|
||||
envlist=py36,flake8,eslint
|
||||
|
||||
[tox:travis]
|
||||
3.6=py36,flake8,eslint
|
||||
|
||||
[testenv]
|
||||
deps=pipenv
|
||||
commands=
|
||||
pipenv install --dev
|
||||
pipenv run python setup.py test
|
||||
|
||||
[testenv:flake8]
|
||||
deps=pipenv
|
||||
commands=
|
||||
pipenv install --dev
|
||||
pipenv run flake8
|
||||
|
||||
[testenv:eslint]
|
||||
whitelist_externals=eslint
|
||||
changedir=app
|
||||
commands=eslint src
|
||||
|
||||
[flake8]
|
||||
max-line-length=160
|
||||
ignore=E402,E722,E252
|
||||
Reference in New Issue
Block a user