From 61fac3aaa7679a3991dd6e6f6deb35737174af1f Mon Sep 17 00:00:00 2001 From: Mark Winter Date: Fri, 29 Oct 2021 19:11:10 +0900 Subject: [PATCH 1/2] Show logs for all containers Signed-off-by: Mark Winter --- .gitignore | 21 ++ LICENSE | 201 ++++++++++++++++++ OWNERS | 1 + backend/.flake8 | 4 + backend/apps/common/__init__.py | 2 + backend/apps/common/routes/__init__.py | 1 + backend/apps/common/routes/delete.py | 2 + backend/apps/common/routes/get.py | 90 +++++--- backend/apps/common/utils.py | 86 ++++---- backend/apps/common/versions.py | 6 + backend/apps/v1beta1/__init__.py | 3 + backend/apps/v1beta1/routes/__init__.py | 1 + backend/apps/v1beta1/routes/post.py | 2 + backend/entrypoint.py | 1 + config/overlays/kubeflow/kustomization.yaml | 2 +- .../src/app/pages/index/index.component.ts | 4 +- .../predictor/predictor.component.html | 13 +- .../transformer/transformer.component.html | 3 + .../logs-viewer/logs-viewer.component.html | 3 - .../logs/logs-viewer/logs-viewer.component.ts | 7 - .../logs/logs-viewer/logs-viewer.module.ts | 8 +- .../server-info/logs/logs.component.html | 71 +++---- .../pages/server-info/logs/logs.component.ts | 93 ++++++-- .../app/pages/server-info/logs/logs.module.ts | 2 + .../metrics/metrics.component.html | 2 +- .../overview/overview.component.html | 10 +- frontend/src/app/services/backend.service.ts | 33 ++- frontend/src/app/shared/utils.ts | 84 +++----- frontend/src/app/types/backend.ts | 9 +- frontend/src/app/types/kfserving/v1beta1.ts | 12 ++ hack/install-isvcs.sh | 16 ++ hack/setup-dev-cluster.sh | 61 ++++++ hack/variables.sh | 5 + 33 files changed, 626 insertions(+), 233 deletions(-) create mode 100644 LICENSE create mode 100644 backend/.flake8 create mode 100755 hack/install-isvcs.sh create mode 100755 hack/setup-dev-cluster.sh create mode 100644 hack/variables.sh diff --git a/.gitignore b/.gitignore index 8a5ea62..3c3623e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,24 @@ **/__pycache__/ **/.vscode/ **/static/* +**/fonts/ + +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/OWNERS b/OWNERS index 938580d..ba3b3e2 100644 --- a/OWNERS +++ b/OWNERS @@ -1,5 +1,6 @@ approvers: - kimwnasptd + - yuzisun reviewers: - elikatsis - StefanoFioravanzo diff --git a/backend/.flake8 b/backend/.flake8 new file mode 100644 index 0000000..e397387 --- /dev/null +++ b/backend/.flake8 @@ -0,0 +1,4 @@ +[flake8] +docstring_convention = google +exclude = assets,__init__.py,__pycache__ +ignore = D100,D104,D107,W503 diff --git a/backend/apps/common/__init__.py b/backend/apps/common/__init__.py index 8b5894a..44c70b8 100644 --- a/backend/apps/common/__init__.py +++ b/backend/apps/common/__init__.py @@ -1,3 +1,4 @@ +"""Package with the base code between backend versions.""" import kubeflow.kubeflow.crud_backend as base from kubeflow.kubeflow.crud_backend import config, logging @@ -8,6 +9,7 @@ def create_app(name=__name__, static_folder="static", cfg: config.Config = None): + """Create the WSGI app.""" cfg = config.Config() if cfg is None else cfg app = base.create_app(name, static_folder, cfg) diff --git a/backend/apps/common/routes/__init__.py b/backend/apps/common/routes/__init__.py index f7247e8..aa793e0 100644 --- a/backend/apps/common/routes/__init__.py +++ b/backend/apps/common/routes/__init__.py @@ -1,3 +1,4 @@ +"""Common routes across backends.""" from flask import Blueprint bp = Blueprint("base_routes", __name__) diff --git a/backend/apps/common/routes/delete.py b/backend/apps/common/routes/delete.py index ae8b8fa..bfa006f 100644 --- a/backend/apps/common/routes/delete.py +++ b/backend/apps/common/routes/delete.py @@ -1,3 +1,4 @@ +"""Route handlers for DELETE requests.""" from kubeflow.kubeflow.crud_backend import api, logging from .. import versions @@ -11,6 +12,7 @@ methods=["DELETE"], ) def delete_inference_service(inference_service, namespace): + """Handle DELETE requests and delete the provided InferenceService.""" log.info("Deleting InferenceService %s/%s'", namespace, inference_service) gvk = versions.inference_service_gvk() api.delete_custom_rsrc(**gvk, diff --git a/backend/apps/common/routes/get.py b/backend/apps/common/routes/get.py index f45701d..b33072f 100644 --- a/backend/apps/common/routes/get.py +++ b/backend/apps/common/routes/get.py @@ -1,5 +1,3 @@ -from flask import request - from kubeflow.kubeflow.crud_backend import api, logging from .. import utils, versions @@ -7,9 +5,12 @@ log = logging.getLogger(__name__) +KSERVE_CONTAINER = "kserve-container" + @bp.route("/api/namespaces//inferenceservices") def get_inference_services(namespace): + """Return a list of InferenceService CRs as json objects.""" gvk = versions.inference_service_gvk() inference_services = api.list_custom_rsrc(**gvk, namespace=namespace) @@ -19,45 +20,79 @@ def get_inference_services(namespace): @bp.route("/api/namespaces//inferenceservices/") def get_inference_service(namespace, name): + """Return an InferenceService CR as a json object.""" inference_service = api.get_custom_rsrc(**versions.inference_service_gvk(), namespace=namespace, name=name) - if request.args.get("logs", "false") == "true": - # find the logs - return api.success_response( - "serviceLogs", get_inference_service_logs(inference_service) - ) return api.success_response("inferenceService", inference_service) -def get_inference_service_logs(svc): - namespace = svc["metadata"]["namespace"] - components = request.args.getlist("component") +@bp.route("/api/namespaces//inferenceservices//components//pods/containers") +def get_inference_service_containers(namespace: str, name: str, component: str): + """Get all containers and init-containers for the latest pod of the given component. + + The kserve-container will always be the first in the list if it exists + + Return: + { + "containers": ["kserve-container", "container2", ...] + } + """ + inference_service = api.get_custom_rsrc(**versions.inference_service_gvk(), + namespace=namespace, name=name) + + latest_pod = utils.get_component_latest_pod(inference_service, component) + + if latest_pod is None: + return api.failed_response(f"couldn't find latest pod for component: {component}", 404) - log.info(components) + containers = [] + for container in latest_pod.spec.init_containers: + containers.append(container.name) - # dictionary{component: [pod-names]} - component_pods_dict = utils.get_inference_service_pods(svc, components) + for container in latest_pod.spec.containers: + containers.append(container.name) + + # Make kserve-container always the first container in the list if it exists + try: + idx = containers.index(KSERVE_CONTAINER) + containers.insert(0, containers.pop(idx)) + except ValueError: + # kserve-container not found in list + pass + + return api.success_response("containers", containers) + + +@bp.route("/api/namespaces//inferenceservices//components//pods/containers" + "//logs") +def get_container_logs(namespace: str, name: str, component: str, container: str): + """Get logs for a particular container inside the latest pod of the given component + + Logs are split on newline and returned as an array of lines + + Return: + { + "logs": ["log\n", "text\n", ...] + } + """ + inference_service = api.get_custom_rsrc(**versions.inference_service_gvk(), + namespace=namespace, name=name) + namespace = inference_service["metadata"]["namespace"] - if len(component_pods_dict.keys()) == 0: - return {} + latest_pod = utils.get_component_latest_pod(inference_service, component) + if latest_pod is None: + return api.failed_response(f"couldn't find latest pod for component: {component}", 404) - resp = {} - logging.info("Component pods: %s", component_pods_dict) - for component, pods in component_pods_dict.items(): - if component not in resp: - resp[component] = [] + logs = api.get_pod_logs(namespace, latest_pod.metadata.name, container, auth=False) + logs = logs.split("\n") - for pod in pods: - logs = api.get_pod_logs(namespace, pod, "kserve-container", - auth=False) - resp[component].append({"podName": pod, - "logs": logs.split("\n")}) - return resp + return api.success_response("logs", logs) @bp.route("/api/namespaces//knativeServices/") def get_knative_service(namespace, name): + """Return a Knative Services object as json.""" svc = api.get_custom_rsrc(**versions.KNATIVE_SERVICE, namespace=namespace, name=name) @@ -66,6 +101,7 @@ def get_knative_service(namespace, name): @bp.route("/api/namespaces//configurations/") def get_knative_configuration(namespace, name): + """Return a Knative Configurations object as json.""" svc = api.get_custom_rsrc(**versions.KNATIVE_CONF, namespace=namespace, name=name) @@ -74,6 +110,7 @@ def get_knative_configuration(namespace, name): @bp.route("/api/namespaces//revisions/") def get_knative_revision(namespace, name): + """Return a Knative Revision object as json.""" svc = api.get_custom_rsrc(**versions.KNATIVE_REVISION, namespace=namespace, name=name) @@ -82,6 +119,7 @@ def get_knative_revision(namespace, name): @bp.route("/api/namespaces//routes/") def get_knative_route(namespace, name): + """Return a Knative Route object as json.""" svc = api.get_custom_rsrc(**versions.KNATIVE_ROUTE, namespace=namespace, name=name) diff --git a/backend/apps/common/utils.py b/backend/apps/common/utils.py index 2503c00..369d10e 100644 --- a/backend/apps/common/utils.py +++ b/backend/apps/common/utils.py @@ -1,10 +1,13 @@ +"""Common utils for parsing and handling InferenceServices.""" import os +from typing import Dict, Union from kubeflow.kubeflow.crud_backend import api, helpers, logging log = logging.getLogger(__name__) KNATIVE_REVISION_LABEL = "serving.knative.dev/revision" +LATEST_CREATED_REVISION = "latestCreatedRevision" FILE_ABS_PATH = os.path.abspath(os.path.dirname(__file__)) INFERENCESERVICE_TEMPLATE_YAML = os.path.join( @@ -13,79 +16,64 @@ def load_inference_service_template(**kwargs): """ - kwargs: the parameters to be replaced in the yaml + Return an InferenceService dict, with defaults from the local yaml. Reads the yaml for the web app's custom resource, replaces the variables and returns it as a python dict. + + kwargs: the parameters to be replaced in the yaml """ return helpers.load_param_yaml(INFERENCESERVICE_TEMPLATE_YAML, **kwargs) -# helper functions for accessing the logs of an InferenceService -def get_inference_service_pods(svc, components=[]): - """ - Return a dictionary with (endpoint, component) keys, - i.e. ("default", "predictor") and a list of pod names as values +def get_component_latest_pod(svc: Dict, component: str) -> Union[api.client.V1Pod, None]: + """Get pod of the latest Knative revision for the given component. + + Return: + Latest pod: k8s V1Pod """ namespace = svc["metadata"]["namespace"] - # dictionary{revisionName: (endpoint, component)} - revisions_dict = get_components_revisions_dict(components, svc) + latest_revision = get_component_latest_revision(svc, component) - if len(revisions_dict.keys()) == 0: - return {} + if latest_revision is None: + return None pods = api.list_pods(namespace, auth=False).items - component_pods_dict = {} + for pod in pods: - for revision in revisions_dict: - if KNATIVE_REVISION_LABEL not in pod.metadata.labels: - continue + if KNATIVE_REVISION_LABEL not in pod.metadata.labels: + continue - if pod.metadata.labels[KNATIVE_REVISION_LABEL] != revision: - continue + if pod.metadata.labels[KNATIVE_REVISION_LABEL] != latest_revision: + continue - component = revisions_dict[revision] - curr_pod_names = component_pods_dict.get(component, []) - curr_pod_names.append(pod.metadata.name) - component_pods_dict[component] = curr_pod_names + return pod - if len(component_pods_dict.keys()) == 0: - log.info("No pods are found for inference service: %s", - svc["metadata"]["name"]) + log.info(f"No pods are found for inference service: {svc['metadata']['name']}") - return component_pods_dict + return None -# FIXME(elikatsis,kimwnasptd): Change the logic of this function according to -# https://github.com/arrikto/dev/issues/867 -def get_components_revisions_dict(components, svc): - """ - Return a dictionary{revisionId: component} +def get_component_latest_revision(svc: Dict, component: str) -> Union[str, None]: + """Get the name of the latest created knative revision for the given component. + + Return: + Latest Created Knative Revision: str """ status = svc["status"] - revisions_dict = {} - for component in components: - if "components" not in status: - log.info("Component '%s' not in inference service '%s'", - component, svc["metadata"]["name"]) - continue - - if component not in status["components"]: - log.info("Component '%s' not in inference service '%s'", - component, svc["metadata"]["name"]) - continue + if "components" not in status: + log.info(f"Components field not found in status object of {svc['metadata']['name']}") + return None - if "latestReadyRevision" in status["components"][component]: - revision = status["components"][component]["latestReadyRevision"] + if component not in status["components"]: + log.info(f"Component {component} not found in inference service {svc['metadata']['name']}") + return None - revisions_dict[revision] = component + if LATEST_CREATED_REVISION in status["components"][component]: + return status["components"][component][LATEST_CREATED_REVISION] - if len(revisions_dict.keys()) == 0: - log.info( - "No revisions found for the inference service's components: %s", - svc["metadata"]["name"], - ) + log.info(f"No {LATEST_CREATED_REVISION} found for the {component} in {svc['metadata']['name']}") - return revisions_dict + return None diff --git a/backend/apps/common/versions.py b/backend/apps/common/versions.py index e90c106..ceba2a2 100644 --- a/backend/apps/common/versions.py +++ b/backend/apps/common/versions.py @@ -1,3 +1,4 @@ +"""Helpers with GVK needed, stored in one place.""" from flask import current_app KNATIVE_ROUTE = {"group": "serving.knative.dev", @@ -15,6 +16,11 @@ def inference_service_gvk(): + """ + Return the GVK needed for an InferenceService. + + This also checks the APP_VERSION env var to detect the version. + """ try: version = current_app.config["APP_VERSION"] if version not in ['v1alpha2', 'v1beta1']: diff --git a/backend/apps/v1beta1/__init__.py b/backend/apps/v1beta1/__init__.py index bdc2d7d..c76fd66 100644 --- a/backend/apps/v1beta1/__init__.py +++ b/backend/apps/v1beta1/__init__.py @@ -1,3 +1,5 @@ +"""Package for the v1beta1 backend of the web app.""" + import os from kubeflow.kubeflow.crud_backend import config, logging @@ -9,6 +11,7 @@ def create_app(name=__name__, cfg: config.Config = None): + """Create a WSGI app.""" cfg = config.Config() if cfg is None else cfg # Properly set the static serving directory diff --git a/backend/apps/v1beta1/routes/__init__.py b/backend/apps/v1beta1/routes/__init__.py index 0ad2090..a7bdedd 100644 --- a/backend/apps/v1beta1/routes/__init__.py +++ b/backend/apps/v1beta1/routes/__init__.py @@ -1,3 +1,4 @@ +"""Include routes of the app.""" from flask import Blueprint bp = Blueprint("default_routes", __name__) diff --git a/backend/apps/v1beta1/routes/post.py b/backend/apps/v1beta1/routes/post.py index 91b7c49..bce9dfb 100644 --- a/backend/apps/v1beta1/routes/post.py +++ b/backend/apps/v1beta1/routes/post.py @@ -1,3 +1,4 @@ +"""POST routes of the backend.""" from flask import request from kubeflow.kubeflow.crud_backend import api, decorators, logging @@ -12,6 +13,7 @@ @decorators.request_is_json_type @decorators.required_body_params("apiVersion", "kind", "metadata", "spec") def post_inference_service(namespace): + """Handle creation of an InferenceService.""" cr = request.get_json() gvk = versions.inference_service_gvk() diff --git a/backend/entrypoint.py b/backend/entrypoint.py index c6b8e38..70904b3 100755 --- a/backend/entrypoint.py +++ b/backend/entrypoint.py @@ -1,3 +1,4 @@ +"""The entrypoint of the backend.""" import os import sys diff --git a/config/overlays/kubeflow/kustomization.yaml b/config/overlays/kubeflow/kustomization.yaml index bce13ca..56fef28 100644 --- a/config/overlays/kubeflow/kustomization.yaml +++ b/config/overlays/kubeflow/kustomization.yaml @@ -10,7 +10,7 @@ commonLabels: app.kubernetes.io/name: kserve bases: -- ../../default +- ../../web-app - web-app-authorization-policy.yaml patchesStrategicMerge: diff --git a/frontend/src/app/pages/index/index.component.ts b/frontend/src/app/pages/index/index.component.ts index fbf4c8c..5db7070 100644 --- a/frontend/src/app/pages/index/index.component.ts +++ b/frontend/src/app/pages/index/index.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { MWABackendService } from 'src/app/services/backend.service'; import { Clipboard } from '@angular/cdk-experimental/clipboard'; import { - PredictorSpec, InferenceServiceK8s, InferenceServiceIR, } from 'src/app/types/kfserving/v1beta1'; @@ -186,9 +185,10 @@ export class IndexComponent implements OnInit, OnDestroy { svc.ui.actions.copy = this.getCopyActionStatus(svc); svc.ui.actions.delete = this.getDeletionActionStatus(svc); + const predictorType = getPredictorType(svc.spec.predictor); const predictor = getPredictorExtensionSpec(svc.spec.predictor); - svc.ui.predictorType = getPredictorType(svc.spec.predictor); + svc.ui.predictorType = predictorType; svc.ui.runtimeVersion = predictor.runtimeVersion; svc.ui.storageUri = predictor.storageUri; svc.ui.protocolVersion = predictor.protocolVersion; diff --git a/frontend/src/app/pages/server-info/details/predictor/predictor.component.html b/frontend/src/app/pages/server-info/details/predictor/predictor.component.html index 010af6c..922d5e4 100644 --- a/frontend/src/app/pages/server-info/details/predictor/predictor.component.html +++ b/frontend/src/app/pages/server-info/details/predictor/predictor.component.html @@ -2,14 +2,15 @@ {{ basePredictor?.storageUri }} - - {{ predictorType }} {{ basePredictor.runtimeVersion }} + + {{ predictorType }} - + + {{ basePredictor.runtimeVersion }} + + + {{ basePredictor.protocolVersion }} diff --git a/frontend/src/app/pages/server-info/details/transformer/transformer.component.html b/frontend/src/app/pages/server-info/details/transformer/transformer.component.html index baa3c30..3b51c77 100644 --- a/frontend/src/app/pages/server-info/details/transformer/transformer.component.html +++ b/frontend/src/app/pages/server-info/details/transformer/transformer.component.html @@ -3,3 +3,6 @@ + + + diff --git a/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.html b/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.html index 746c261..e22f395 100644 --- a/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.html +++ b/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.html @@ -1,6 +1,3 @@ - - -
{{ index }} diff --git a/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.ts b/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.ts index c5d6d1f..27ca8dd 100644 --- a/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.ts +++ b/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.component.ts @@ -2,15 +2,10 @@ import { Component, Input, ViewChild, - NgZone, - SimpleChanges, - OnChanges, HostBinding, - ElementRef, AfterViewInit, } from '@angular/core'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; -import { take } from 'rxjs/operators'; @Component({ selector: 'app-logs-viewer', @@ -22,8 +17,6 @@ export class LogsViewerComponent implements AfterViewInit { @ViewChild(CdkVirtualScrollViewport, { static: true }) viewPort: CdkVirtualScrollViewport; - @Input() heading = 'Logs'; - @Input() subHeading = 'tit'; @Input() height = '400px'; @Input() set logs(newLogs: string[]) { diff --git a/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.module.ts b/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.module.ts index e4f2d13..7f2e9e3 100644 --- a/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.module.ts +++ b/frontend/src/app/pages/server-info/logs/logs-viewer/logs-viewer.module.ts @@ -1,12 +1,18 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ScrollingModule } from '@angular/cdk/scrolling'; +import { MatTabsModule } from '@angular/material/tabs'; import { LogsViewerComponent } from './logs-viewer.component'; import { HeadingSubheadingRowModule } from 'kubeflow'; @NgModule({ declarations: [LogsViewerComponent], - imports: [CommonModule, ScrollingModule, HeadingSubheadingRowModule], + imports: [ + CommonModule, + MatTabsModule, + ScrollingModule, + HeadingSubheadingRowModule, + ], exports: [LogsViewerComponent], }) export class LogsViewerModule {} diff --git a/frontend/src/app/pages/server-info/logs/logs.component.html b/frontend/src/app/pages/server-info/logs/logs.component.html index 3d2ed74..fe70f5a 100644 --- a/frontend/src/app/pages/server-info/logs/logs.component.html +++ b/frontend/src/app/pages/server-info/logs/logs.component.html @@ -1,50 +1,41 @@ - - - - + - No logs were found for this InferenceService. + Logs are shown for the latest created revision - - - + + + + + + + + + + + + + + {{ loadErrorMsg }} -
- -
- -
- -
- -
- -
+
diff --git a/frontend/src/app/pages/server-info/logs/logs.component.ts b/frontend/src/app/pages/server-info/logs/logs.component.ts index 2fe73c0..dc35529 100644 --- a/frontend/src/app/pages/server-info/logs/logs.component.ts +++ b/frontend/src/app/pages/server-info/logs/logs.component.ts @@ -1,10 +1,18 @@ -import { Component, Input, OnDestroy } from '@angular/core'; +import { Component, Input, OnDestroy, ViewChild } from '@angular/core'; import { MWABackendService } from 'src/app/services/backend.service'; import { ExponentialBackoff } from 'kubeflow'; import { Subscription } from 'rxjs'; -import { InferenceServiceLogs } from 'src/app/types/backend'; import { InferenceServiceK8s } from 'src/app/types/kfserving/v1beta1'; -import { dictIsEmpty } from 'src/app/shared/utils'; + +enum IsvcComponent { + predictor = 'predictor', + transformer = 'transformer', + explainer = 'explainer', +} + +interface IsvcComponents { + IsvcComponent?: { containers: string[] }; +} @Component({ selector: 'app-logs', @@ -12,11 +20,22 @@ import { dictIsEmpty } from 'src/app/shared/utils'; styleUrls: ['./logs.component.scss'], }) export class LogsComponent implements OnDestroy { - public goToBottom = true; - public currLogs: InferenceServiceLogs = {}; + public currLogs: string[] = []; public logsRequestCompleted = false; public loadErrorMsg = ''; + public isvcComponents: IsvcComponents = {}; + private currentComponent: string; + private currentContainer: string; + private hasLoadedContainers = false; + + @ViewChild('componentTabGroup', {static: false}) componentTabGroup; + @ViewChild('containerTabGroup', {static: false}) containerTabGroup; + + get components() { + return Object.keys(this.isvcComponents); + } + @Input() set svc(s: InferenceServiceK8s) { this.svcPrv = s; @@ -29,14 +48,41 @@ export class LogsComponent implements OnDestroy { this.pollingSub.unsubscribe(); } + for (const component of Object.keys(IsvcComponent)) { + if (!(component in this.svcPrv.spec)) { + continue; + } + + this.isvcComponents[component] = {containers: []}; + + this.backend.getInferenceServiceContainers(this.svcPrv, component).subscribe( + containers => { + if (!this.hasLoadedContainers) { + this.currentComponent = component; + this.currentContainer = containers[0]; + this.hasLoadedContainers = true; + } + this.isvcComponents[component].containers = containers; + }, + error => { + console.log(`error getting ${component} containers'`, error); + }, + ); + } + this.pollingSub = this.poller.start().subscribe(() => { - this.backend.getInferenceServiceLogs(s).subscribe( + if (!this.currentComponent || !this.currentContainer) { + return; + } + + this.backend.getInferenceServiceLogs(this.svcPrv, this.currentComponent, this.currentContainer).subscribe( logs => { this.currLogs = logs; this.logsRequestCompleted = true; this.loadErrorMsg = ''; }, error => { + this.currLogs = []; this.logsRequestCompleted = true; this.loadErrorMsg = error; }, @@ -44,26 +90,39 @@ export class LogsComponent implements OnDestroy { }); } - get logsNotEmpty(): boolean { - return !dictIsEmpty(this.currLogs); - } - private svcPrv: InferenceServiceK8s; - private components: [string, string][] = []; private pollingSub: Subscription; private poller = new ExponentialBackoff({ - interval: 3000, + interval: 5000, retries: 1, - maxInterval: 3001, + maxInterval: 5001, }); constructor(public backend: MWABackendService) {} - ngOnDestroy() { - this.pollingSub.unsubscribe(); + resetLogDisplay() { + this.logsRequestCompleted = false; + this.currLogs = []; + this.loadErrorMsg = ''; + this.poller.reset(); } - logsTrackFn(i: number, podLogs: any) { - return podLogs.podName; + componentTabChange(index: number) { + this.currentComponent = Object.keys(this.isvcComponents)[index]; + + if (!(this.currentContainer in this.isvcComponents[this.currentComponent].containers)) { + this.currentContainer = this.isvcComponents[this.currentComponent].containers[0]; + } + + this.resetLogDisplay(); + } + + containerTabChange(index: number) { + this.currentContainer = this.isvcComponents[this.currentComponent].containers[index]; + this.resetLogDisplay(); + } + + ngOnDestroy() { + this.pollingSub.unsubscribe(); } } diff --git a/frontend/src/app/pages/server-info/logs/logs.module.ts b/frontend/src/app/pages/server-info/logs/logs.module.ts index 0f35911..53ea266 100644 --- a/frontend/src/app/pages/server-info/logs/logs.module.ts +++ b/frontend/src/app/pages/server-info/logs/logs.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { MatTabsModule } from '@angular/material/tabs'; import { LogsComponent } from './logs.component'; import { KubeflowModule, PanelModule, LoadingSpinnerModule } from 'kubeflow'; import { LogsViewerModule } from './logs-viewer/logs-viewer.module'; @@ -10,6 +11,7 @@ import { LogsViewerModule } from './logs-viewer/logs-viewer.module'; CommonModule, KubeflowModule, LogsViewerModule, + MatTabsModule, LoadingSpinnerModule, PanelModule, ], diff --git a/frontend/src/app/pages/server-info/metrics/metrics.component.html b/frontend/src/app/pages/server-info/metrics/metrics.component.html index e7a26e3..cacccec 100644 --- a/frontend/src/app/pages/server-info/metrics/metrics.component.html +++ b/frontend/src/app/pages/server-info/metrics/metrics.component.html @@ -25,7 +25,7 @@
diff --git a/frontend/src/app/pages/server-info/overview/overview.component.html b/frontend/src/app/pages/server-info/overview/overview.component.html index c21088e..a93e3c0 100644 --- a/frontend/src/app/pages/server-info/overview/overview.component.html +++ b/frontend/src/app/pages/server-info/overview/overview.component.html @@ -21,11 +21,15 @@ {{ basePredictor?.storageUri }} - - {{ predictorType }} {{ basePredictor.runtimeVersion }} + + {{ predictorType }} - + + {{ basePredictor.runtimeVersion }} + + + {{ basePredictor.protocolVersion }} diff --git a/frontend/src/app/services/backend.service.ts b/frontend/src/app/services/backend.service.ts index b6ff7f9..96ed552 100644 --- a/frontend/src/app/services/backend.service.ts +++ b/frontend/src/app/services/backend.service.ts @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { svcHasComponent, getSvcComponents } from '../shared/utils'; import { InferenceServiceK8s } from '../types/kfserving/v1beta1'; -import { MWABackendResponse, InferenceServiceLogs } from '../types/backend'; +import { MWABackendResponse } from '../types/backend'; @Injectable({ providedIn: 'root', @@ -101,24 +101,37 @@ export class MWABackendService extends BackendService { ); } + public getInferenceServiceContainers( + svc: InferenceServiceK8s, + component: string, + ): Observable { + const name = svc.metadata.name; + const namespace = svc.metadata.namespace; + + const url = `api/namespaces/${namespace}/inferenceservices/${name}/components/${component}/pods/containers`; + + return this.http.get(url).pipe( + catchError(error => this.handleError(error, false)), + map((resp: MWABackendResponse) => { + return resp.containers; + }), + ); + } + public getInferenceServiceLogs( svc: InferenceServiceK8s, - components: string[] = [], - ): Observable { + component: string, + container: string, + ): Observable { const name = svc.metadata.name; const namespace = svc.metadata.namespace; - let url = `api/namespaces/${namespace}/inferenceservices/${name}?logs=true`; - ['predictor', 'explainer', 'transformer'].forEach(component => { - if (component in svc.spec) { - url += `&component=${component}`; - } - }); + const url = `api/namespaces/${namespace}/inferenceservices/${name}/components/${component}/pods/containers/${container}/logs`; return this.http.get(url).pipe( catchError(error => this.handleError(error, false)), map((resp: MWABackendResponse) => { - return resp.serviceLogs; + return resp.logs; }), ); } diff --git a/frontend/src/app/shared/utils.ts b/frontend/src/app/shared/utils.ts index 2eac828..17a7781 100644 --- a/frontend/src/app/shared/utils.ts +++ b/frontend/src/app/shared/utils.ts @@ -3,6 +3,7 @@ import { V1Container } from '@kubernetes/client-node'; import { InferenceServiceK8s, PredictorSpec, + PredictorType, PredictorExtensionSpec, ExplainerSpec, } from '../types/kfserving/v1beta1'; @@ -107,78 +108,41 @@ export function getK8sObjectStatus(obj: K8sObject): [string, string] { } // functions for processing the InferenceService spec -export function getPredictorType(predictor: PredictorSpec): string { - if ('tensorflow' in predictor) { - return 'Tensorflow'; - } - - if ('triton' in predictor) { - return 'Triton'; - } - - if ('sklearn' in predictor) { - return 'SKLearn'; - } - - if ('onnx' in predictor) { - return 'Onnx'; - } - - if ('pytorch' in predictor) { - return 'PyTorch'; - } - - if ('xgboost' in predictor) { - return 'XGBoost'; - } - - if ('pmml' in predictor) { - return 'PMML'; - } - - if ('lightgbm' in predictor) { - return 'LightGBM'; +export function getPredictorType(predictor: PredictorSpec): PredictorType { + for (const predictorType of Object.values(PredictorType)) { + if (predictorType in predictor) { + return predictorType; + } } - return 'Custom'; + return PredictorType.Custom; } export function getPredictorExtensionSpec( predictor: PredictorSpec, ): PredictorExtensionSpec { - if ('tensorflow' in predictor) { - return predictor.tensorflow; - } - - if ('triton' in predictor) { - return predictor.triton; - } - - if ('sklearn' in predictor) { - return predictor.sklearn; - } - - if ('onnx' in predictor) { - return predictor.onnx; - } - - if ('pytorch' in predictor) { - return predictor.pytorch; + for (const predictorType of Object.values(PredictorType)) { + if (predictorType in predictor) { + return predictor[predictorType]; + } } - if ('xgboost' in predictor) { - return predictor.xgboost; - } + // In the case of Custom predictors, set the additional PredictorExtensionSpec fields + // manually here + const spec = predictor.containers[0] as PredictorExtensionSpec; + spec.runtimeVersion = ''; + spec.protocolVersion = ''; - if ('pmml' in predictor) { - return predictor.pmml; - } - - if ('lightgbm' in predictor) { - return predictor.lightgbm; + if (predictor.containers[0].env) { + const storageUri = predictor.containers[0].env.find( + envVar => envVar.name.toLowerCase() === 'storage_uri' + ); + if (storageUri) { + spec.storageUri = storageUri.value; + } } - return null; + return spec; } export function getExplainerContainer(explainer: ExplainerSpec): V1Container { diff --git a/frontend/src/app/types/backend.ts b/frontend/src/app/types/backend.ts index dc24748..817d2bd 100644 --- a/frontend/src/app/types/backend.ts +++ b/frontend/src/app/types/backend.ts @@ -8,13 +8,8 @@ export interface MWABackendResponse extends BackendResponse { knativeConfiguration?: K8sObject; knativeRevision?: K8sObject; knativeRoute?: K8sObject; - serviceLogs?: InferenceServiceLogs; -} - -export interface InferenceServiceLogs { - predictor?: { podName: string; logs: string[] }[]; - transformer?: { podName: string; logs: string[] }[]; - explainer?: { podName: string; logs: string[] }[]; + logs?: string[]; + containers?: string[]; } // types presenting the InferenceService dependent k8s objects diff --git a/frontend/src/app/types/kfserving/v1beta1.ts b/frontend/src/app/types/kfserving/v1beta1.ts index 938c365..692314e 100644 --- a/frontend/src/app/types/kfserving/v1beta1.ts +++ b/frontend/src/app/types/kfserving/v1beta1.ts @@ -32,6 +32,18 @@ export interface InferenceServiceSpec { transformer: TransformerSpec; } +export enum PredictorType { + Tensorflow = 'tensorflow', + Triton = 'triton', + Sklean = 'sklearn', + Onnx = 'onnx', + Pytorch = 'pytorch', + Xgboost = 'xgboost', + Pmml = 'pmml', + Lightgbm = 'lightgbm', + Custom = 'custom', +} + export interface PredictorSpec extends V1PodSpec, ComponentExtensionSpec { sklearn?: PredictorExtensionSpec; xgboost?: PredictorExtensionSpec; diff --git a/hack/install-isvcs.sh b/hack/install-isvcs.sh new file mode 100755 index 0000000..dd9eefb --- /dev/null +++ b/hack/install-isvcs.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ +set -euo pipefail +IFS=$'\n\t' + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +source ${SCRIPT_DIR}/variables.sh + +# Apply a wide set of InferenceServices to help test the app +cd ${KSERVE_DIR} +kubectl apply -f docs/samples/v1beta1/sklearn/v1/sklearn.yaml +kubectl apply -f docs/samples/v1beta1/tensorflow/tensorflow.yaml +kubectl apply -f docs/samples/v1beta1/transformer/torchserve_image_transformer/transformer.yaml +kubectl apply -f docs/samples/v1beta1/custom/paddleserving/paddleserving-custom.yaml +kubectl apply -f docs/samples/v1beta1/xgboost/xgboost.yaml diff --git a/hack/setup-dev-cluster.sh b/hack/setup-dev-cluster.sh new file mode 100755 index 0000000..93fb3e8 --- /dev/null +++ b/hack/setup-dev-cluster.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# Dependencies: +# kind==0.11.1 +# kubectl [compatible with K8s 1.19] +# + +# strict mode http://redsymbol.net/articles/unofficial-bash-strict-mode/ +set -euo pipefail +IFS=$'\n\t' + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +source ${SCRIPT_DIR}/variables.sh + +############################################################ +# Help # +############################################################ +Help() +{ + # Display Help + echo "Setup the dev environment for using the web app." + echo + echo "Dependencies" + echo "kind 0.11.1" + echo "kubectl [compatible with K8s 1.19]" +} + +DeleteCluster() +{ + kind delete cluster --name ${KIND_CLUSTER} +} + +while getopts ":hd" option; do + case $option in + h) # display Help + Help + exit;; + d) # delete cluster + DeleteCluster + exit;; + \?) # Invalid option + echo "Error: Invalid option" + exit;; + esac +done + + +kind create cluster --name kserve --image ${KIND_NODE} + +# Install KServe +if [[ ! -d ${KSERVE_DIR} ]]; then + cd /tmp + git clone ${KSERVE_REPO} +fi + +cd ${KSERVE_DIR} +git checkout ${KSERVE_COMMIT} +./hack/quick_install.sh + +# use the KinD cluster +kubectl config set-context kind-kserve diff --git a/hack/variables.sh b/hack/variables.sh new file mode 100644 index 0000000..5c8955d --- /dev/null +++ b/hack/variables.sh @@ -0,0 +1,5 @@ +export KSERVE_REPO=https://github.com/kserve/kserve.git +export KSERVE_COMMIT=v0.7.0 +export KSERVE_DIR=${KSERVE_DIR:=/tmp/kserve} +export KIND_CLUSTER=kserve +export KIND_NODE=kindest/node:1.19.0@sha256:3b0289b2d1bab2cb9108645a006939d2f447a10ad2bb21919c332d06b548bbc6 From cdecff3572a7383f28ced560cd5b5c7b647aba01 Mon Sep 17 00:00:00 2001 From: Mark Winter Date: Tue, 21 Jun 2022 13:27:08 +0900 Subject: [PATCH 2/2] fix lint errors Signed-off-by: Mark Winter --- backend/apps/common/routes/get.py | 26 ++++++++++++++++---------- backend/apps/common/utils.py | 15 +++++++++------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/backend/apps/common/routes/get.py b/backend/apps/common/routes/get.py index b33072f..155c1c8 100644 --- a/backend/apps/common/routes/get.py +++ b/backend/apps/common/routes/get.py @@ -27,9 +27,11 @@ def get_inference_service(namespace, name): return api.success_response("inferenceService", inference_service) -@bp.route("/api/namespaces//inferenceservices//components//pods/containers") -def get_inference_service_containers(namespace: str, name: str, component: str): - """Get all containers and init-containers for the latest pod of the given component. +@bp.route("/api/namespaces//inferenceservices//components//pods/containers") # noqa: E501 +def get_inference_service_containers( + namespace: str, name: str, component: str): + """Get all containers and init-containers for the latest pod of + the given component. The kserve-container will always be the first in the list if it exists @@ -44,7 +46,8 @@ def get_inference_service_containers(namespace: str, name: str, component: str): latest_pod = utils.get_component_latest_pod(inference_service, component) if latest_pod is None: - return api.failed_response(f"couldn't find latest pod for component: {component}", 404) + return api.failed_response( + f"couldn't find latest pod for component: {component}", 404) containers = [] for container in latest_pod.spec.init_containers: @@ -64,10 +67,11 @@ def get_inference_service_containers(namespace: str, name: str, component: str): return api.success_response("containers", containers) -@bp.route("/api/namespaces//inferenceservices//components//pods/containers" - "//logs") -def get_container_logs(namespace: str, name: str, component: str, container: str): - """Get logs for a particular container inside the latest pod of the given component +@bp.route("/api/namespaces//inferenceservices//components//pods/containers//logs") # noqa: E501 +def get_container_logs(namespace: str, name: str, + component: str, container: str): + """Get logs for a particular container inside the latest pod of + the given component Logs are split on newline and returned as an array of lines @@ -82,9 +86,11 @@ def get_container_logs(namespace: str, name: str, component: str, container: str latest_pod = utils.get_component_latest_pod(inference_service, component) if latest_pod is None: - return api.failed_response(f"couldn't find latest pod for component: {component}", 404) + return api.failed_response( + f"couldn't find latest pod for component: {component}", 404) - logs = api.get_pod_logs(namespace, latest_pod.metadata.name, container, auth=False) + logs = api.get_pod_logs( + namespace, latest_pod.metadata.name, container, auth=False) logs = logs.split("\n") return api.success_response("logs", logs) diff --git a/backend/apps/common/utils.py b/backend/apps/common/utils.py index 369d10e..60898a7 100644 --- a/backend/apps/common/utils.py +++ b/backend/apps/common/utils.py @@ -26,7 +26,8 @@ def load_inference_service_template(**kwargs): return helpers.load_param_yaml(INFERENCESERVICE_TEMPLATE_YAML, **kwargs) -def get_component_latest_pod(svc: Dict, component: str) -> Union[api.client.V1Pod, None]: +def get_component_latest_pod(svc: Dict, + component: str) -> Union[api.client.V1Pod, None]: """Get pod of the latest Knative revision for the given component. Return: @@ -50,12 +51,14 @@ def get_component_latest_pod(svc: Dict, component: str) -> Union[api.client.V1Po return pod - log.info(f"No pods are found for inference service: {svc['metadata']['name']}") + log.info( + f"No pods are found for inference service: {svc['metadata']['name']}") return None -def get_component_latest_revision(svc: Dict, component: str) -> Union[str, None]: +def get_component_latest_revision(svc: Dict, + component: str) -> Union[str, None]: """Get the name of the latest created knative revision for the given component. Return: @@ -64,16 +67,16 @@ def get_component_latest_revision(svc: Dict, component: str) -> Union[str, None] status = svc["status"] if "components" not in status: - log.info(f"Components field not found in status object of {svc['metadata']['name']}") + log.info(f"Components field not found in status object of {svc['metadata']['name']}") # noqa: E501 return None if component not in status["components"]: - log.info(f"Component {component} not found in inference service {svc['metadata']['name']}") + log.info(f"Component {component} not found in inference service {svc['metadata']['name']}") # noqa: E501 return None if LATEST_CREATED_REVISION in status["components"][component]: return status["components"][component][LATEST_CREATED_REVISION] - log.info(f"No {LATEST_CREATED_REVISION} found for the {component} in {svc['metadata']['name']}") + log.info(f"No {LATEST_CREATED_REVISION} found for the {component} in {svc['metadata']['name']}") # noqa: E501 return None