diff --git a/.vscode/settings.json b/.vscode/settings.json index 0a4d1db..48551f8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "python.pythonPath": "C:\\Users\\me\\.virtualenvs\\bruce-server-_01-8y1n\\Scripts\\python.exe" + "python.pythonPath": "C:\\Users\\me\\.virtualenvs\\bruce-operator-UKafV52I\\Scripts\\python.exe", + "python.unitTest.promptToConfigure": false, + "python.unitTest.pyTestEnabled": false, + "python.unitTest.unittestEnabled": false, + "python.unitTest.nosetestsEnabled": false } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b6eaae4..5dafc0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,12 @@ FROM kennethreitz/pipenv +# Instlall kube-ctl. +RUN apt-get update && apt-get install -y apt-transport-https +RUN curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - +RUN touch /etc/apt/sources.list.d/kubernetes.list +RUN echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list +RUN apt-get update +RUN apt-get install -y kubectl + COPY . /app CMD python3 -m bruce_operator diff --git a/Pipfile b/Pipfile index 94e7953..b3e6348 100644 --- a/Pipfile +++ b/Pipfile @@ -10,8 +10,14 @@ pyyaml = "*" "jinja2" = "*" click = "*" logme = "*" +background = "*" [dev-packages] +"flake8" = "*" +black = "*" [requires] python_version = "3.7" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index 35b0334..686d8e2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "32bf55fc946ce7a517b1424e1b1f14579e1b0782d66e9c73279d869a649a25ba" + "sha256": "6ce8c782cb25701a9b5c8203f9deee1aeed26ca6cd732df4cda162dd986f149e" }, "pipfile-spec": 6, "requires": { @@ -31,6 +31,14 @@ ], "version": "==0.24.0" }, + "background": { + "hashes": [ + "sha256:8f64df9b1f1d4f91603f16d25f79691fa87eff62244d631b8b69cf121396b725", + "sha256:b2fb684c150aaf1c4716686e7bd0e81a5c13db1852e7af48c01b98a3486a84c6" + ], + "index": "pypi", + "version": "==0.1.1" + }, "bnmutils": { "hashes": [ "sha256:09d4fe9ec208b7f244a7d1954fc36209b84a1b7bbb87d85dc2cc69cb2fbccaa0", @@ -137,6 +145,13 @@ "index": "pypi", "version": "==0.1.1" }, + "futures": { + "hashes": [ + "sha256:51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd", + "sha256:c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f" + ], + "version": "==3.1.1" + }, "google-auth": { "hashes": [ "sha256:9ca363facbf2622d9ba828017536ccca2e0f58bd15e659b52f312172f8815530", @@ -239,20 +254,14 @@ }, "pyyaml": { "hashes": [ - "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", - "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", - "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", - "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", - "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", - "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", - "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", - "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", - "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", - "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", + "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", + "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", + "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", + "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" ], "index": "pypi", - "version": "==3.13" + "version": "==4.2b4" }, "requests": { "hashes": [ @@ -298,5 +307,72 @@ "version": "==0.53.0" } }, - "develop": {} + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "black": { + "hashes": [ + "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", + "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5" + ], + "index": "pypi", + "version": "==18.9b0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "index": "pypi", + "version": "==7.0" + }, + "flake8": { + "hashes": [ + "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", + "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37" + ], + "index": "pypi", + "version": "==3.5.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", + "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9" + ], + "version": "==2.3.1" + }, + "pyflakes": { + "hashes": [ + "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", + "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" + ], + "version": "==1.6.0" + }, + "toml": { + "hashes": [ + "sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42", + "sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957" + ], + "version": "==0.9.6" + } + } } diff --git a/bruce_operator/core.py b/bruce_operator/core.py index 0570c57..0184122 100644 --- a/bruce_operator/core.py +++ b/bruce_operator/core.py @@ -1,67 +1,102 @@ import time +import json +from functools import lru_cache +import logme import kubernetes +import background from kubernetes.client.configuration import Configuration from kubernetes.client.api_client import ApiClient -from .env import WATCH_NAMESPACE +from .env import WATCH_NAMESPACE, API_GROUP, API_VERSION +from .kubectl import kubectl + +# https://github.com/kubernetes-client/python/blob/master/examples/create_thirdparty_resource.md +@logme.log class Operator: def __init__(self, api_client=None): - self.watchers = [] - self.api_client = None - config = Configuration() - if api_client: - self.api_client = api_client - else: - if not config.api_client: - config.api_client = ApiClient() - self.api_client = config.api_client + # Load Kube configuration into module (ugh). + try: + kubernetes.config.load_kube_config() + except FileNotFoundError: + pass - def _build_url(self, api_version, kind, namespace=None): - resource = self.lookup_resource(api_version, kind) - if not resource: - print("ERR", api_version, kind) - exit + # Setup clients. + self.client = kubernetes.client.CoreV1Api() + self.custom_client = kubernetes.client.CustomObjectsApi(self.client.api_client) - api_prefix = "api" if api_version == "v1" else "apis" - if resource["namespaced"] and namespace: - if not namespace: - namespace = "default" - return ( - f"/{api_prefix}/{api_version}/namespaces/{namespace}/{resource['name']}" + # Ensure resource definitions. + self.ensure_resource_definitions() + # print(self.installed_buildpacks) + # exit() + + @property + def installed_buildpacks(self): + + group = API_GROUP # str | The custom resource's group name + version = API_VERSION # str | The custom resource's version + namespace = WATCH_NAMESPACE # str | The custom resource's namespace + plural = ( + "buildpacks" + ) # str | The custom resource's plural name. For TPRs this would be lowercase plural kind. + pretty = ( + "true" + ) # str | If 'true', then the output is pretty printed. (optional) + watch = ( + False + ) # bool | Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. (optional) + + try: + api_response = self.custom_client.list_namespaced_custom_object( + group, version, namespace, plural, pretty=pretty, watch=watch ) + return api_response["items"] + except kubernetes.client.rest.ApiException: + return None - return f"/{api_prefix}/{api_version}/{resource['name']}" + @property + def installed_apps(self): + group = "bruce.kennethreitz.org" # str | The custom resource's group name + version = "v1alpha1" # str | The custom resource's version + namespace = WATCH_NAMESPACE # str | The custom resource's namespace + plural = ( + "apps" + ) # str | The custom resource's plural name. For TPRs this would be lowercase plural kind. + pretty = ( + "true" + ) # str | If 'true', then the output is pretty printed. (optional) + watch = ( + False + ) # bool | Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. (optional) - def register_watcher(self, callback): - self.watchers.append(callback) + try: + api_response = self.custom_client.list_namespaced_custom_object( + group, version, namespace, plural, pretty=pretty, watch=watch + ) + return api_response["items"] + except kubernetes.client.rest.ApiException: + return None + + def ensure_resource_definitions(self): + # Create Buildpacks resource. + self.logger.info("Ensuring Buildpack resource definitions...") + kubectl("apply -f ./deploy/buildpack-resource-definition.yml") + + # Create Apps resource. + self.logger.info("Ensuring App resource definitions...") + kubectl("apply -f ./deploy/app-resource-definition.yml") def watch(self): - url = self._build_url(api_version="v1", kind="", namespace=WATCH_NAMESPACE) - - w = kubernetes.watch.Watch(return_type=object) - for event in w.stream( - self.generic.list_generic, resource_path=url, timeout_seconds=90000 - ): - for watcher in self.watchers: - watcher(event) - - -def app_watcher(event): - print(event) - - -def buildpack_watcher(event): - print(event) + self.logger.info("Pretending to watch...") + time.sleep(5) operator = Operator() -operator.register_watcher(app_watcher) -operator.register_watcher(buildpack_watcher) def watch(): - operator.watch() + while True: + operator.watch() diff --git a/bruce_operator/core.py.c46fb31d35cc0636f376c70f90af377f.py b/bruce_operator/core.py.c46fb31d35cc0636f376c70f90af377f.py new file mode 100644 index 0000000..0184122 --- /dev/null +++ b/bruce_operator/core.py.c46fb31d35cc0636f376c70f90af377f.py @@ -0,0 +1,102 @@ +import time +import json +from functools import lru_cache + +import logme +import kubernetes +import background +from kubernetes.client.configuration import Configuration +from kubernetes.client.api_client import ApiClient + +from .env import WATCH_NAMESPACE, API_GROUP, API_VERSION +from .kubectl import kubectl + +# https://github.com/kubernetes-client/python/blob/master/examples/create_thirdparty_resource.md + + +@logme.log +class Operator: + def __init__(self, api_client=None): + + # Load Kube configuration into module (ugh). + try: + kubernetes.config.load_kube_config() + except FileNotFoundError: + pass + + # Setup clients. + self.client = kubernetes.client.CoreV1Api() + self.custom_client = kubernetes.client.CustomObjectsApi(self.client.api_client) + + # Ensure resource definitions. + self.ensure_resource_definitions() + # print(self.installed_buildpacks) + # exit() + + @property + def installed_buildpacks(self): + + group = API_GROUP # str | The custom resource's group name + version = API_VERSION # str | The custom resource's version + namespace = WATCH_NAMESPACE # str | The custom resource's namespace + plural = ( + "buildpacks" + ) # str | The custom resource's plural name. For TPRs this would be lowercase plural kind. + pretty = ( + "true" + ) # str | If 'true', then the output is pretty printed. (optional) + watch = ( + False + ) # bool | Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. (optional) + + try: + api_response = self.custom_client.list_namespaced_custom_object( + group, version, namespace, plural, pretty=pretty, watch=watch + ) + return api_response["items"] + except kubernetes.client.rest.ApiException: + return None + + @property + def installed_apps(self): + group = "bruce.kennethreitz.org" # str | The custom resource's group name + version = "v1alpha1" # str | The custom resource's version + namespace = WATCH_NAMESPACE # str | The custom resource's namespace + plural = ( + "apps" + ) # str | The custom resource's plural name. For TPRs this would be lowercase plural kind. + pretty = ( + "true" + ) # str | If 'true', then the output is pretty printed. (optional) + watch = ( + False + ) # bool | Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. (optional) + + try: + api_response = self.custom_client.list_namespaced_custom_object( + group, version, namespace, plural, pretty=pretty, watch=watch + ) + return api_response["items"] + except kubernetes.client.rest.ApiException: + return None + + def ensure_resource_definitions(self): + # Create Buildpacks resource. + self.logger.info("Ensuring Buildpack resource definitions...") + kubectl("apply -f ./deploy/buildpack-resource-definition.yml") + + # Create Apps resource. + self.logger.info("Ensuring App resource definitions...") + kubectl("apply -f ./deploy/app-resource-definition.yml") + + def watch(self): + self.logger.info("Pretending to watch...") + time.sleep(5) + + +operator = Operator() + + +def watch(): + while True: + operator.watch() diff --git a/bruce_operator/env.py b/bruce_operator/env.py index 7a681e7..68a749d 100644 --- a/bruce_operator/env.py +++ b/bruce_operator/env.py @@ -1,3 +1,5 @@ import os -WATCH_NAMESPACE = os.environ.get("WATCH_NAMESPACE", "default") +WATCH_NAMESPACE = os.environ.get("WATCH_NAMESPACE", "bruce") +API_VERSION = "v1alpha1" +API_GROUP = "bruce.kennethreitz.org" diff --git a/bruce_operator/kubectl.py b/bruce_operator/kubectl.py new file mode 100644 index 0000000..e36c71f --- /dev/null +++ b/bruce_operator/kubectl.py @@ -0,0 +1,23 @@ +import json + + +import delegator + + +def kubectl(cmd, as_json=True, raise_on_error=True): + json_flag = "-o=json" if as_json else "" + cmd = f"kubectl {cmd} {json_flag}" + + c = delegator.run(cmd) + + if as_json: + try: + assert c.ok + return json.loads(c.out) + except AssertionError as e: + if not raise_on_error: + return c + else: + raise e + else: + return c diff --git a/logme.ini b/logme.ini new file mode 100644 index 0000000..f2de60d --- /dev/null +++ b/logme.ini @@ -0,0 +1,20 @@ +[colors] +CRITICAL = + color: PURPLE + style: BOLD +ERROR = RED +WARNING = YELLOW +INFO = None +DEBUG = GREEN + +[logme] +level = DEBUG +formatter = {asctime} - {name} - {levelname} - {message} +stream = + type: StreamHandler + active: True + level: DEBUG +null = + type: NullHandler + active: False + level: NOTSET