From 0dc6ee40a6c8309f4ae5e57351110279b8274030 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 14:19:21 -0600 Subject: [PATCH 01/30] add support for using ssl also changing the default port to 8443 to mimic other servers i have seen! Signed-off-by: vsoch --- CHANGELOG.md | 18 ++++++++++++++++++ Dockerfile | 2 +- README.md | 16 +++++++++++----- flux_metrics_api/server.py | 21 +++++++++++++++++---- 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4a0b3b9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# CHANGELOG + +This is a manually generated log to track changes to the repository for each release. +Each section should include general headers such as **Implemented enhancements** +and **Merged pull requests**. Critical items to know are: + + - renamed commands + - deprecated / removed commands + - changed defaults + - backward incompatible changes + - migration guidance + - changed behaviour + +The versions coincide with releases on pip. Only major versions will be released as tags on Github. + +## [0.0.x](https://github.com/converged-computing/flux-metrics-api/tree/main) (0.0.x) + - Support for certificates for uvicorn and change default port to 8443 (0.0.1) + - Skelton release (0.0.0) diff --git a/Dockerfile b/Dockerfile index 1da27f8..87a85ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM fluxrm/flux-sched:focal # docker build -t flux_metrics_api . -# docker run -it -p 8080:8080 flux_metrics_api +# docker run -it -p 8443:8443 flux_metrics_api LABEL maintainer="Vanessasaurus <@vsoch>" diff --git a/README.md b/README.md index 1a61243..480a9ba 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ You'll want to be running in a Flux instance, as we need to connect to the broke $ flux start --test-size=4 ``` -And then start the server. This will use a default port and host (0.0.0.0:8080) that you can customize +And then start the server. This will use a default port and host (0.0.0.0:8443) that you can customize if desired. ```bash @@ -56,6 +56,12 @@ $ flux-metrics-api start $ flux-metrics-api start --port 9000 --host 127.0.0.1 ``` +If you want ssl (port 443) you can provide the path to a certificate and keyfile: + +```bash +$ flux-metrics-api start --ssl-certfile /etc/certs/tls.crt --ssl-keyfile /etc/certs/tls.key +``` + See `--help` to see other options available. ### Endpoints @@ -67,7 +73,7 @@ See `--help` to see other options available. Here is an example to get the "node_up_count" metric: ```bash - curl -s http://localhost:8080/apis/custom.metrics.k8s.io/v1beta2/namespaces/flux-operator/metrics/node_up_count | jq + curl -s http://localhost:8443/apis/custom.metrics.k8s.io/v1beta2/namespaces/flux-operator/metrics/node_up_count | jq ``` ```console { @@ -101,15 +107,15 @@ be a demo. You can either build it yourself, or use our build. ```bash $ docker build -t flux_metrics_api . -$ docker run -it -p 8080:8080 flux_metrics_api +$ docker run -it -p 8443:8443 flux_metrics_api ``` or ```bash -$ docker run -it -p 8080:8080 ghcr.io/converged-computing/flux-metrics-api +$ docker run -it -p 8443:8443 ghcr.io/converged-computing/flux-metrics-api ``` -You can then open up the browser at [http://localhost:8080/metrics/](http://localhost:8080/metrics) to see +You can then open up the browser at [http://localhost:8443/metrics/](http://localhost:8443/metrics) to see the metrics! ## 😁️ Contributors 😁️ diff --git a/flux_metrics_api/server.py b/flux_metrics_api/server.py index b9708bf..7804225 100644 --- a/flux_metrics_api/server.py +++ b/flux_metrics_api/server.py @@ -65,8 +65,8 @@ def get_parser(): ) start.add_argument( "--port", - help="Port to run application", - default=8080, + help="Port to run application (defaults to 8443)", + default=8443, type=int, ) start.add_argument( @@ -78,7 +78,6 @@ def get_parser(): help="Custom API path (defaults to /apis/custom.metrics.k8s.io/v1beta2)", default=None, ) - start.add_argument( "--host", help="Host address to run application", @@ -90,6 +89,8 @@ def get_parser(): default=False, action="store_true", ) + start.add_argument("--ssl-keyfile", help="full path to ssl keyfile") + start.add_argument("--ssl-certfile", help="full path to ssl certfile") return parser @@ -97,8 +98,20 @@ def start(args): """ Start the server with uvicorn """ + # Validate certificates if provided + if args.ssl_keyfile and not args.ssl_certfile: + sys.exit("A --ssl-keyfile was provided without a --ssl-certfile.") + if args.ssl_certfile and not args.ssl_keyfile: + sys.exit("A --ssl-certfile was provided without a --ssl-keyfile.") + app = Starlette(debug=args.debug, routes=routes) - uvicorn.run(app, host=args.host, port=args.port) + uvicorn.run( + app, + host=args.host, + port=args.port, + ssl_keyfile=args.ssl_keyfile, + ssl_certfile=args.ssl_certfile, + ) def main(): From b623f1ddc7f9cc0a92e1f9b4d0caad05ec19bcc0 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 15:47:53 -0600 Subject: [PATCH 02/30] test different format for hpa Signed-off-by: vsoch --- flux_metrics_api/defaults.py | 13 +++++++++++-- flux_metrics_api/routes.py | 5 ++++- flux_metrics_api/server.py | 15 ++++++++++----- flux_metrics_api/types.py | 23 ++++++++++++++++------- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/flux_metrics_api/defaults.py b/flux_metrics_api/defaults.py index 5d539c0..c27bb5c 100644 --- a/flux_metrics_api/defaults.py +++ b/flux_metrics_api/defaults.py @@ -3,6 +3,15 @@ # # SPDX-License-Identifier: (MIT) -API_VERSION = "custom.metrics.k8s.io/v1beta2" +API_ENDPOINT = "custom.metrics.k8s.io/v1beta2" API_ROOT = "/apis/custom.metrics.k8s.io/v1beta2" -NAMESPACES = None +NAMESPACE = "flux-operator" +SERVICE_NAME = "custom-metrics-apiserver" + + +def API_VERSION(): + """ + Derive the api version from the endpoint + """ + global API_ENDPOINT + return API_ENDPOINT.rstrip("/").rsplit("/")[-1] diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 1e8b07c..4970286 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -63,7 +63,10 @@ def get_metric(request): # Get the value from Flux, assemble into listing value = metrics[metric_name]() metric_value = types.new_metric(metric, value=value) - listing = types.new_metric_list([metric_value]) + + # Give the endpoint for the service as metadata + metadata = {"selfLink": defaults.API_ROOT} + listing = types.new_metric_list([metric_value], metadata=metadata) return JSONResponse(listing) diff --git a/flux_metrics_api/server.py b/flux_metrics_api/server.py index 7804225..3588094 100644 --- a/flux_metrics_api/server.py +++ b/flux_metrics_api/server.py @@ -69,8 +69,9 @@ def get_parser(): default=8443, type=int, ) + start.add_argument("--namespace", help="Namespace the API is running in") start.add_argument( - "--namespace", help="Scope to running in these namespace(s)", action="append" + "--service-name", help="Service name the metrics service is running from" ) start.add_argument( "--api-path", @@ -144,14 +145,18 @@ def help(return_code=0): ) # Setup the registry - non verbose is default - print(f"API endpoint is at {defaults.API_ROOT}") if args.api_path is not None: - print(f"Setting API endpoint to {args.api_path}") defaults.API_ROOT = args.api_path + print(f"API endpoint is at {defaults.API_ROOT}") - # Limit to specific namespaces? + # Set namespace or service name to be different than defaults if args.namespace: - defaults.NAMESPACES = args.namespace + defaults.NAMESPACE = args.namespace + print(f"Running from namespace {defaults.NAMESPACE}") + + if args.service_name: + defaults.SERVICE_NAME = args.service_name + print(f"Service name {defaults.SERVICE_NAME}") # Does the user want a shell? if args.command == "start": diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index e2df024..4fcfd12 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -16,7 +16,7 @@ def new_identifier(name: str, selector: dict = None): return metric -def new_metric(metric, value, time="", windowSeconds=0, describedObject=None): +def new_metric(metric, value, time="", windowSeconds=0): """ Get the metric value for an object. @@ -25,21 +25,30 @@ def new_metric(metric, value, time="", windowSeconds=0, describedObject=None): which the metric was calculated (0 for instantaneous, which is what we are making). describedObject is the object the metric was collected from. """ + # Our custom metrics API always comes from a service + describedObject = { + "kind": "Service", + "namespace": defaults.NAMESPACE, + "name": defaults.SERVICE_NAME, + "apiVersion": defaults.API_VERSION(), + } return { - "metric": metric, + "metricName": metric, "value": value, - "time": time, - "windowSeconds": windowSeconds, + "timestamp": time, "describedObject": describedObject, } -def new_metric_list(metrics): +def new_metric_list(metrics, metadata=None): """ Put list of metrics into proper list format """ - return { + listing = { "items": metrics, - "apiVersion": defaults.API_VERSION, + "apiVersion": defaults.API_ENDPOINT, "kind": "MetricValueList", } + if metadata is not None: + listing["metadata"] = metadata + return metadata From 9dd51323c4ee7781f7bbdc901cd5043e8a0dde65 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 15:51:52 -0600 Subject: [PATCH 03/30] do not check namespace, already running in one Signed-off-by: vsoch --- flux_metrics_api/routes.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 4970286..6be70e0 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -41,18 +41,13 @@ def get_metric(request): """ metric_name = request.path_params["metric_name"] namespace = request.path_params.get("namespace") + print(f"Requested metric {metric_name} in namespace {namespace}") - if ( - namespace is not None - and defaults.NAMESPACES is not None - and namespace not in defaults.NAMESPACES - ): - return JSONResponse( - {"detail": "This namespace is not known to the server."}, status_code=404 - ) - + # TODO we don't do anything with namespace currently, we assume we won't + # be able to hit this if running in the wrong one # Unknown metric if metric_name not in metrics: + print(f"Unknown metric requested {metric_name}") return JSONResponse( {"detail": "This metric is not known to the server."}, status_code=404 ) From 4efe1f5e3c4f578a4b09601916edc28ace9e8139 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 15:52:41 -0600 Subject: [PATCH 04/30] do not check namespace, already running in one Signed-off-by: vsoch --- flux_metrics_api/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index 4fcfd12..547852c 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -51,4 +51,4 @@ def new_metric_list(metrics, metadata=None): } if metadata is not None: listing["metadata"] = metadata - return metadata + return listing From 9df654d17814bb47afd8193b3bfd15a1cbba0086 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 15:53:44 -0600 Subject: [PATCH 05/30] typo Signed-off-by: vsoch --- flux_metrics_api/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index 547852c..6c47053 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -33,7 +33,7 @@ def new_metric(metric, value, time="", windowSeconds=0): "apiVersion": defaults.API_VERSION(), } return { - "metricName": metric, + "metricName": metric['name'], "value": value, "timestamp": time, "describedObject": describedObject, From 91deccc6b4e74a351c8d46d1cc2ca8c29ae87f97 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 15:53:53 -0600 Subject: [PATCH 06/30] typo Signed-off-by: vsoch --- flux_metrics_api/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index 6c47053..d3d4fe0 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -33,7 +33,7 @@ def new_metric(metric, value, time="", windowSeconds=0): "apiVersion": defaults.API_VERSION(), } return { - "metricName": metric['name'], + "metricName": metric["name"], "value": value, "timestamp": time, "describedObject": describedObject, From 647e4c1917cf93947648f5e5fab53c8fe28a47c1 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:19:48 -0600 Subject: [PATCH 07/30] update format to match the current custom api server spec Signed-off-by: vsoch --- README.md | 5 +++++ flux_metrics_api/types.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 480a9ba..15a7a20 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,11 @@ or $ docker run -it -p 8443:8443 ghcr.io/converged-computing/flux-metrics-api ``` +### Development + +Note that this is implemented in Python, but (I found this after) we could [also use Go](https://github.com/kubernetes-sigs/custom-metrics-apiserver). +Specifically, I found this repository useful to see the [spec format](https://github.com/kubernetes-sigs/custom-metrics-apiserver/blob/master/pkg/generated/openapi/custommetrics/zz_generated.openapi.go). + You can then open up the browser at [http://localhost:8443/metrics/](http://localhost:8443/metrics) to see the metrics! diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index d3d4fe0..741e75c 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: (MIT) +from datetime import datetime + import flux_metrics_api.defaults as defaults @@ -11,12 +13,14 @@ def new_identifier(name: str, selector: dict = None): Get a new metric identifier. """ metric = {"name": name} + + # A selector would be a label on a metric (we don't have any currently) if selector is not None: metric["selector"] = selector return metric -def new_metric(metric, value, time="", windowSeconds=0): +def new_metric(metric, value, timestamp="", windowSeconds=0): """ Get the metric value for an object. @@ -25,6 +29,9 @@ def new_metric(metric, value, time="", windowSeconds=0): which the metric was calculated (0 for instantaneous, which is what we are making). describedObject is the object the metric was collected from. """ + # This probably needs work - I just fudged it for now + timestamp = timestamp or datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00") + # Our custom metrics API always comes from a service describedObject = { "kind": "Service", @@ -33,9 +40,10 @@ def new_metric(metric, value, time="", windowSeconds=0): "apiVersion": defaults.API_VERSION(), } return { - "metricName": metric["name"], + "metric": metric, "value": value, - "timestamp": time, + "timestamp": timestamp, + "windowSeconds": windowSeconds, "describedObject": describedObject, } From 7a90e5abe5bd44de3d3de84452a162e7b8135055 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:45:06 -0600 Subject: [PATCH 08/30] add resource list endpoint Signed-off-by: vsoch --- flux_metrics_api/routes.py | 5 +++-- flux_metrics_api/types.py | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 6be70e0..02186f2 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -25,11 +25,12 @@ class Root(HTTPEndpoint): """ Root of the API - This needs to return 200 for a health check + This needs to return 200 for a health check. I later discovered it also needs + to return the listing of available metrics! """ async def get(self, request): - return JSONResponse({}) + return JSONResponse(types.new_api_resource_list()) def get_metric(request): diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index 741e75c..4bed8ad 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -6,6 +6,31 @@ from datetime import datetime import flux_metrics_api.defaults as defaults +from flux_metrics_api.metrics import metrics + + +def new_resource_list(): + """ + The root of the server returns the api list with available metrics. + """ + listing = { + "kind": "APIResourceList", + "apiVersion": defaults.API_VERSION(), + "groupVersion": defaults.API_ENDPOINT, + "resources": [], + } + + for metric_name in metrics: + listing["resources"].append( + { + "name": f"service/{metric_name}", + "singularName": metric_name, + "namespaced": False, + "kind": "MetricValueList", + "verbs": ["get"], + } + ) + return listing def new_identifier(name: str, selector: dict = None): From 1c04a46058ea3424d3d05a97566f18cd4fef871f Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:45:47 -0600 Subject: [PATCH 09/30] typo Signed-off-by: vsoch --- flux_metrics_api/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 02186f2..0b7fcb7 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -30,7 +30,7 @@ class Root(HTTPEndpoint): """ async def get(self, request): - return JSONResponse(types.new_api_resource_list()) + return JSONResponse(types.new_resource_list()) def get_metric(request): From 022512390f453324796105ffcc77c4820e4a5660 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:51:08 -0600 Subject: [PATCH 10/30] test different schema package Signed-off-by: vsoch --- flux_metrics_api/routes.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 0b7fcb7..fd736c7 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -20,6 +20,19 @@ } ) +from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin +from starlette_apispec import APISpecSchemaGenerator + +schemas = APISpecSchemaGenerator( + APISpec( + title="Flux Metrics API", + version=version.__version__, + openapi_version="3.0.0", + info={"description": "Export Flux custom metrics."}, + plugins=[MarshmallowPlugin()], + ) +) class Root(HTTPEndpoint): """ @@ -82,9 +95,11 @@ def openapi_schema(request): """ Get the openapi spec from the endpoints - TODO: debug why paths empty + TODO: debug why paths empty. For now I'm adding them manually. """ - return JSONResponse(schemas.get_schema(routes=routes)) + return schemas.OpenAPIResponse(request=request) + +# return JSONResponse(schemas.get_schema(routes=routes)) # STOPPED HERE - make open api spec s we can see endpoints and query From 7ae93c51856c8d48a46784c81657357b7bf6dc33 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:52:04 -0600 Subject: [PATCH 11/30] test different schema package Signed-off-by: vsoch --- flux_metrics_api/routes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index fd736c7..1dd59e8 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -21,7 +21,6 @@ ) from apispec import APISpec -from apispec.ext.marshmallow import MarshmallowPlugin from starlette_apispec import APISpecSchemaGenerator schemas = APISpecSchemaGenerator( @@ -30,7 +29,6 @@ version=version.__version__, openapi_version="3.0.0", info={"description": "Export Flux custom metrics."}, - plugins=[MarshmallowPlugin()], ) ) From abbbd937695f009c722e224eeb2979934991e42d Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:53:53 -0600 Subject: [PATCH 12/30] test different schema package Signed-off-by: vsoch --- flux_metrics_api/routes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 1dd59e8..42359de 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -21,6 +21,7 @@ ) from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin from starlette_apispec import APISpecSchemaGenerator schemas = APISpecSchemaGenerator( @@ -29,9 +30,11 @@ version=version.__version__, openapi_version="3.0.0", info={"description": "Export Flux custom metrics."}, + plugins=[MarshmallowPlugin()], ) ) + class Root(HTTPEndpoint): """ Root of the API @@ -97,6 +100,7 @@ def openapi_schema(request): """ return schemas.OpenAPIResponse(request=request) + # return JSONResponse(schemas.get_schema(routes=routes)) From 8ecbe88f91dfc6863e7a0576be9fd96f63f61503 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:55:11 -0600 Subject: [PATCH 13/30] test different schema package Signed-off-by: vsoch --- flux_metrics_api/routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 42359de..9ff4b0a 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -98,6 +98,7 @@ def openapi_schema(request): TODO: debug why paths empty. For now I'm adding them manually. """ + print(schemas.get_schemas()) return schemas.OpenAPIResponse(request=request) From 373543a88d937eb7e277997aab8ffea94f7a33ab Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 16:59:19 -0600 Subject: [PATCH 14/30] test different schema package Signed-off-by: vsoch --- flux_metrics_api/routes.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 9ff4b0a..bb649ab 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -98,11 +98,9 @@ def openapi_schema(request): TODO: debug why paths empty. For now I'm adding them manually. """ - print(schemas.get_schemas()) - return schemas.OpenAPIResponse(request=request) - - -# return JSONResponse(schemas.get_schema(routes=routes)) + #print(schemas.get_schema()) + #return schemas.OpenAPIResponse(request=request) + return JSONResponse(schemas.get_schema(routes=routes)) # STOPPED HERE - make open api spec s we can see endpoints and query From cc46112203553457ddc12ab07433c34b5faa8a82 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 17:01:50 -0600 Subject: [PATCH 15/30] test without marshmallow Signed-off-by: vsoch --- flux_metrics_api/routes.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index bb649ab..87aaa78 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -3,34 +3,23 @@ # # SPDX-License-Identifier: (MIT) +from apispec import APISpec from starlette.endpoints import HTTPEndpoint from starlette.responses import JSONResponse from starlette.routing import Route -from starlette.schemas import SchemaGenerator +from starlette_apispec import APISpecSchemaGenerator import flux_metrics_api.defaults as defaults import flux_metrics_api.types as types import flux_metrics_api.version as version from flux_metrics_api.metrics import metrics -schemas = SchemaGenerator( - { - "openapi": "3.0.0", - "info": {"title": "Flux Metrics API", "version": version.__version__}, - } -) - -from apispec import APISpec -from apispec.ext.marshmallow import MarshmallowPlugin -from starlette_apispec import APISpecSchemaGenerator - schemas = APISpecSchemaGenerator( APISpec( title="Flux Metrics API", version=version.__version__, openapi_version="3.0.0", info={"description": "Export Flux custom metrics."}, - plugins=[MarshmallowPlugin()], ) ) @@ -95,11 +84,7 @@ async def get(self, request): def openapi_schema(request): """ Get the openapi spec from the endpoints - - TODO: debug why paths empty. For now I'm adding them manually. """ - #print(schemas.get_schema()) - #return schemas.OpenAPIResponse(request=request) return JSONResponse(schemas.get_schema(routes=routes)) From c2c89683e489d32b34b258ec9c6491f2afd97f3b Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 17:04:30 -0600 Subject: [PATCH 16/30] try setting namespaced to true Signed-off-by: vsoch --- flux_metrics_api/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index 4bed8ad..69acf69 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -25,7 +25,7 @@ def new_resource_list(): { "name": f"service/{metric_name}", "singularName": metric_name, - "namespaced": False, + "namespaced": True, "kind": "MetricValueList", "verbs": ["get"], } From 77ea6238b7ebd0819d79372dd8555199f9e4e6b7 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 17:09:24 -0600 Subject: [PATCH 17/30] test without marshmallow Signed-off-by: vsoch --- flux_metrics_api/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index 69acf69..c58f439 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -23,7 +23,7 @@ def new_resource_list(): for metric_name in metrics: listing["resources"].append( { - "name": f"service/{metric_name}", + "name": metric_name, "singularName": metric_name, "namespaced": True, "kind": "MetricValueList", From 80d9ab6b3448542bd0c2fa97ad530763bfb44d8a Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 17:22:25 -0600 Subject: [PATCH 18/30] add faux group list Signed-off-by: vsoch --- flux_metrics_api/routes.py | 12 +++++++++++- flux_metrics_api/types.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 87aaa78..3b698ba 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -81,6 +81,15 @@ async def get(self, request): return get_metric(request) +class APIGroupList(HTTPEndpoint): + """ + Service a faux resource list just for our custom metrics endpoint. + """ + + async def get(self, request): + return types.new_group_list() + + def openapi_schema(request): """ Get the openapi spec from the endpoints @@ -91,7 +100,8 @@ def openapi_schema(request): # STOPPED HERE - make open api spec s we can see endpoints and query routes = [ Route(defaults.API_ROOT, Root), - # Optional for openapi, we could add if needed + # This is a faux route so we can get the preferred resource version + Route(defaults.API_ROOT + "/apis", APIGroupList), Route(defaults.API_ROOT + "/namespaces/{namespace}/metrics/{metric_name}", Metric), Route(defaults.API_ROOT + "/{resource}/{name}/{metric_name}", Metric), Route( diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index c58f439..070e72f 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -73,6 +73,31 @@ def new_metric(metric, value, timestamp="", windowSeconds=0): } +def new_group_list(): + """ + Return a faux group list to get the version of the custom metrics API + """ + return { + "kind": "APIGroupList", + "apiVersion": "v1", + "groups": [ + { + "name": "custom.metrics.k8s.io", + "versions": [ + { + "groupVersion": defaults.API_ENDPOINT, + "version": defaults.API_VERSION(), + } + ], + "preferredVersion": { + "groupVersion": defaults.API_ENDPOINT, + "version": defaults.API_VERSION(), + }, + } + ], + } + + def new_metric_list(metrics, metadata=None): """ Put list of metrics into proper list format From 85304657cef76baa927209009c269f513311c3f1 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 17:23:56 -0600 Subject: [PATCH 19/30] add faux group list Signed-off-by: vsoch --- flux_metrics_api/routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 3b698ba..3d2f496 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -101,12 +101,13 @@ def openapi_schema(request): routes = [ Route(defaults.API_ROOT, Root), # This is a faux route so we can get the preferred resource version - Route(defaults.API_ROOT + "/apis", APIGroupList), + Route("/apis", APIGroupList), Route(defaults.API_ROOT + "/namespaces/{namespace}/metrics/{metric_name}", Metric), Route(defaults.API_ROOT + "/{resource}/{name}/{metric_name}", Metric), Route( defaults.API_ROOT + "/namespaces/{namespace}/{resource}/{name}/{metric_name}", Metric, ), + Route("/openapi/v2", openapi_schema, include_in_schema=False), Route(f"{defaults.API_ROOT}/openapi/v2", openapi_schema, include_in_schema=False), ] From d72b225fa8a4705c5a3b40ef2605b66369301489 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 17:25:46 -0600 Subject: [PATCH 20/30] ok that broke the entire cluster lol Signed-off-by: vsoch --- flux_metrics_api/routes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 3d2f496..8ea274d 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -101,13 +101,13 @@ def openapi_schema(request): routes = [ Route(defaults.API_ROOT, Root), # This is a faux route so we can get the preferred resource version - Route("/apis", APIGroupList), + # Route("/apis", APIGroupList), Route(defaults.API_ROOT + "/namespaces/{namespace}/metrics/{metric_name}", Metric), Route(defaults.API_ROOT + "/{resource}/{name}/{metric_name}", Metric), Route( defaults.API_ROOT + "/namespaces/{namespace}/{resource}/{name}/{metric_name}", Metric, ), - Route("/openapi/v2", openapi_schema, include_in_schema=False), + # Route("/openapi/v2", openapi_schema, include_in_schema=False), Route(f"{defaults.API_ROOT}/openapi/v2", openapi_schema, include_in_schema=False), ] From 80800f949859dc56647e4ee7ab121be5b5487e2d Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 18:18:47 -0600 Subject: [PATCH 21/30] enable api endpoint again (danger!) Signed-off-by: vsoch --- flux_metrics_api/routes.py | 2 +- flux_metrics_api/types.py | 186 ++++++++++++++++++++++++++++++++++++ flux_metrics_api/version.py | 1 + 3 files changed, 188 insertions(+), 1 deletion(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 8ea274d..f7dc857 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -101,7 +101,7 @@ def openapi_schema(request): routes = [ Route(defaults.API_ROOT, Root), # This is a faux route so we can get the preferred resource version - # Route("/apis", APIGroupList), + Route("/apis", APIGroupList), Route(defaults.API_ROOT + "/namespaces/{namespace}/metrics/{metric_name}", Metric), Route(defaults.API_ROOT + "/{resource}/{name}/{metric_name}", Metric), Route( diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index 070e72f..d42f49e 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -77,6 +77,192 @@ def new_group_list(): """ Return a faux group list to get the version of the custom metrics API """ + return { + "kind": "APIGroupList", + "apiVersion": "v1", + "groups": [ + { + "name": "apiregistration.k8s.io", + "versions": [ + {"groupVersion": "apiregistration.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "apiregistration.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "apps", + "versions": [{"groupVersion": "apps/v1", "version": "v1"}], + "preferredVersion": {"groupVersion": "apps/v1", "version": "v1"}, + }, + { + "name": "events.k8s.io", + "versions": [{"groupVersion": "events.k8s.io/v1", "version": "v1"}], + "preferredVersion": { + "groupVersion": "events.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "authentication.k8s.io", + "versions": [ + {"groupVersion": "authentication.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "authentication.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "authorization.k8s.io", + "versions": [ + {"groupVersion": "authorization.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "authorization.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "autoscaling", + "versions": [ + {"groupVersion": "autoscaling/v2", "version": "v2"}, + {"groupVersion": "autoscaling/v1", "version": "v1"}, + ], + "preferredVersion": {"groupVersion": "autoscaling/v2", "version": "v2"}, + }, + { + "name": "batch", + "versions": [{"groupVersion": "batch/v1", "version": "v1"}], + "preferredVersion": {"groupVersion": "batch/v1", "version": "v1"}, + }, + { + "name": "certificates.k8s.io", + "versions": [ + {"groupVersion": "certificates.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "certificates.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "networking.k8s.io", + "versions": [{"groupVersion": "networking.k8s.io/v1", "version": "v1"}], + "preferredVersion": { + "groupVersion": "networking.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "policy", + "versions": [{"groupVersion": "policy/v1", "version": "v1"}], + "preferredVersion": {"groupVersion": "policy/v1", "version": "v1"}, + }, + { + "name": "rbac.authorization.k8s.io", + "versions": [ + {"groupVersion": "rbac.authorization.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "rbac.authorization.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "storage.k8s.io", + "versions": [{"groupVersion": "storage.k8s.io/v1", "version": "v1"}], + "preferredVersion": { + "groupVersion": "storage.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "admissionregistration.k8s.io", + "versions": [ + {"groupVersion": "admissionregistration.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "admissionregistration.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "apiextensions.k8s.io", + "versions": [ + {"groupVersion": "apiextensions.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "apiextensions.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "scheduling.k8s.io", + "versions": [{"groupVersion": "scheduling.k8s.io/v1", "version": "v1"}], + "preferredVersion": { + "groupVersion": "scheduling.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "coordination.k8s.io", + "versions": [ + {"groupVersion": "coordination.k8s.io/v1", "version": "v1"} + ], + "preferredVersion": { + "groupVersion": "coordination.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "node.k8s.io", + "versions": [{"groupVersion": "node.k8s.io/v1", "version": "v1"}], + "preferredVersion": {"groupVersion": "node.k8s.io/v1", "version": "v1"}, + }, + { + "name": "discovery.k8s.io", + "versions": [{"groupVersion": "discovery.k8s.io/v1", "version": "v1"}], + "preferredVersion": { + "groupVersion": "discovery.k8s.io/v1", + "version": "v1", + }, + }, + { + "name": "flowcontrol.apiserver.k8s.io", + "versions": [ + { + "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta3", + "version": "v1beta3", + }, + { + "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta2", + "version": "v1beta2", + }, + ], + "preferredVersion": { + "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta3", + "version": "v1beta3", + }, + }, + { + "name": "flux-framework.org", + "versions": [ + { + "groupVersion": "flux-framework.org/v1alpha1", + "version": "v1alpha1", + } + ], + "preferredVersion": { + "groupVersion": "flux-framework.org/v1alpha1", + "version": "v1alpha1", + }, + }, + ], + } + + # return { "kind": "APIGroupList", "apiVersion": "v1", diff --git a/flux_metrics_api/version.py b/flux_metrics_api/version.py index e2c9043..c0a73f0 100644 --- a/flux_metrics_api/version.py +++ b/flux_metrics_api/version.py @@ -18,6 +18,7 @@ INSTALL_REQUIRES = ( ("uvicorn", {"min_version": None}), ("starlette", {"min_version": None}), + ("starlette-apispec", {"min_version": None}), ) TESTS_REQUIRES = (("pytest", {"min_version": "4.6.2"}),) From 321339605442aca3ed88e60ae600eaea747f7b1f Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 18:22:46 -0600 Subject: [PATCH 22/30] typo Signed-off-by: vsoch --- flux_metrics_api/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index f7dc857..8301095 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -87,7 +87,7 @@ class APIGroupList(HTTPEndpoint): """ async def get(self, request): - return types.new_group_list() + return JSONResponse(types.new_group_list()) def openapi_schema(request): From 38724faaf08005fcff58cd08862e9d4af750b955 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 20:52:56 -0600 Subject: [PATCH 23/30] forward api response Signed-off-by: vsoch --- flux_metrics_api/routes.py | 8 +- flux_metrics_api/types.py | 219 ++----------------------------------- 2 files changed, 15 insertions(+), 212 deletions(-) diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 8301095..4d29aea 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -87,7 +87,13 @@ class APIGroupList(HTTPEndpoint): """ async def get(self, request): - return JSONResponse(types.new_group_list()) + listing = types.new_group_list() + if not listing: + return JSONResponse( + {"detail": "The metric server is not running in a Kubernetes pod."}, + status_code=404, + ) + return JSONResponse(listing) def openapi_schema(request): diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index d42f49e..fe64373 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -5,10 +5,18 @@ from datetime import datetime +import flux_metrics_api.apis as apis import flux_metrics_api.defaults as defaults from flux_metrics_api.metrics import metrics +def new_group_list(): + """ + Return a faux group list to get the version of the custom metrics API + """ + return apis.get_api_group_list() + + def new_resource_list(): """ The root of the server returns the api list with available metrics. @@ -73,217 +81,6 @@ def new_metric(metric, value, timestamp="", windowSeconds=0): } -def new_group_list(): - """ - Return a faux group list to get the version of the custom metrics API - """ - return { - "kind": "APIGroupList", - "apiVersion": "v1", - "groups": [ - { - "name": "apiregistration.k8s.io", - "versions": [ - {"groupVersion": "apiregistration.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "apiregistration.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "apps", - "versions": [{"groupVersion": "apps/v1", "version": "v1"}], - "preferredVersion": {"groupVersion": "apps/v1", "version": "v1"}, - }, - { - "name": "events.k8s.io", - "versions": [{"groupVersion": "events.k8s.io/v1", "version": "v1"}], - "preferredVersion": { - "groupVersion": "events.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "authentication.k8s.io", - "versions": [ - {"groupVersion": "authentication.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "authentication.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "authorization.k8s.io", - "versions": [ - {"groupVersion": "authorization.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "authorization.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "autoscaling", - "versions": [ - {"groupVersion": "autoscaling/v2", "version": "v2"}, - {"groupVersion": "autoscaling/v1", "version": "v1"}, - ], - "preferredVersion": {"groupVersion": "autoscaling/v2", "version": "v2"}, - }, - { - "name": "batch", - "versions": [{"groupVersion": "batch/v1", "version": "v1"}], - "preferredVersion": {"groupVersion": "batch/v1", "version": "v1"}, - }, - { - "name": "certificates.k8s.io", - "versions": [ - {"groupVersion": "certificates.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "certificates.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "networking.k8s.io", - "versions": [{"groupVersion": "networking.k8s.io/v1", "version": "v1"}], - "preferredVersion": { - "groupVersion": "networking.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "policy", - "versions": [{"groupVersion": "policy/v1", "version": "v1"}], - "preferredVersion": {"groupVersion": "policy/v1", "version": "v1"}, - }, - { - "name": "rbac.authorization.k8s.io", - "versions": [ - {"groupVersion": "rbac.authorization.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "rbac.authorization.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "storage.k8s.io", - "versions": [{"groupVersion": "storage.k8s.io/v1", "version": "v1"}], - "preferredVersion": { - "groupVersion": "storage.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "admissionregistration.k8s.io", - "versions": [ - {"groupVersion": "admissionregistration.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "admissionregistration.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "apiextensions.k8s.io", - "versions": [ - {"groupVersion": "apiextensions.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "apiextensions.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "scheduling.k8s.io", - "versions": [{"groupVersion": "scheduling.k8s.io/v1", "version": "v1"}], - "preferredVersion": { - "groupVersion": "scheduling.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "coordination.k8s.io", - "versions": [ - {"groupVersion": "coordination.k8s.io/v1", "version": "v1"} - ], - "preferredVersion": { - "groupVersion": "coordination.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "node.k8s.io", - "versions": [{"groupVersion": "node.k8s.io/v1", "version": "v1"}], - "preferredVersion": {"groupVersion": "node.k8s.io/v1", "version": "v1"}, - }, - { - "name": "discovery.k8s.io", - "versions": [{"groupVersion": "discovery.k8s.io/v1", "version": "v1"}], - "preferredVersion": { - "groupVersion": "discovery.k8s.io/v1", - "version": "v1", - }, - }, - { - "name": "flowcontrol.apiserver.k8s.io", - "versions": [ - { - "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta3", - "version": "v1beta3", - }, - { - "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta2", - "version": "v1beta2", - }, - ], - "preferredVersion": { - "groupVersion": "flowcontrol.apiserver.k8s.io/v1beta3", - "version": "v1beta3", - }, - }, - { - "name": "flux-framework.org", - "versions": [ - { - "groupVersion": "flux-framework.org/v1alpha1", - "version": "v1alpha1", - } - ], - "preferredVersion": { - "groupVersion": "flux-framework.org/v1alpha1", - "version": "v1alpha1", - }, - }, - ], - } - - # - return { - "kind": "APIGroupList", - "apiVersion": "v1", - "groups": [ - { - "name": "custom.metrics.k8s.io", - "versions": [ - { - "groupVersion": defaults.API_ENDPOINT, - "version": defaults.API_VERSION(), - } - ], - "preferredVersion": { - "groupVersion": defaults.API_ENDPOINT, - "version": defaults.API_VERSION(), - }, - } - ], - } - - def new_metric_list(metrics, metadata=None): """ Put list of metrics into proper list format From b62894b34b8e777a0c508a2318f1ed6696f0b7c2 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 20:54:23 -0600 Subject: [PATCH 24/30] forward api response Signed-off-by: vsoch --- flux_metrics_api/apis.py | 53 +++++++++++++++++++++++++++++++++++++++ flux_metrics_api/utils.py | 13 ++++++++++ 2 files changed, 66 insertions(+) create mode 100644 flux_metrics_api/apis.py create mode 100644 flux_metrics_api/utils.py diff --git a/flux_metrics_api/apis.py b/flux_metrics_api/apis.py new file mode 100644 index 0000000..e4654ff --- /dev/null +++ b/flux_metrics_api/apis.py @@ -0,0 +1,53 @@ +# Copyright 2023 Lawrence Livermore National Security, LLC and other +# HPCIC DevTools Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (MIT) + +import json +import os +import subprocess + +import flux_metrics_api.utils as utils + + +def get_api_group_list(): + """ + Get the API group list, assuming we are inside a pod. + """ + return get_kubernetes_endpoint("apis") + + +def get_kubernetes_endpoint(endpoint): + """ + Get an endpoint from the cluster. + """ + # Point to the internal API server hostname + api_server = "https://kubernetes.default.svc" + + # Path to ServiceAccount directory + sa_account_dir = "/var/run/secrets/kubernetes.io/serviceaccount" + namespace_file = os.path.join(sa_account_dir, "namespace") + cert_file = os.path.join(sa_account_dir, "ca.crt") + token_file = os.path.join(sa_account_dir, "token") + + # Cut out early if we aren't running in the pod + if not all( + map(os.path.exists, [sa_account_dir, namespace_file, token_file, cert_file]) + ): + return {} + + # Get the token to do the request + token = utils.read_file(token_file) + + # Using subprocess to not add extra dependency - yes requires curl + # res = requests.get(f"{api_server}/apis", headers=headers, verify=cert_file) + # Kids don't do this at home + output = subprocess.check_output( + f'curl --cacert {cert_file} --header "Authorization: Bearer {token}" -X GET {api_server}/{endpoint}', + shell=True, + ) + try: + output = json.loadss(output) + except Exception: + return {} + return output diff --git a/flux_metrics_api/utils.py b/flux_metrics_api/utils.py new file mode 100644 index 0000000..199596e --- /dev/null +++ b/flux_metrics_api/utils.py @@ -0,0 +1,13 @@ +# Copyright 2023 Lawrence Livermore National Security, LLC and other +# HPCIC DevTools Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (MIT) + + +def read_file(path): + """ + Read content from a file + """ + with open(path, "r") as fd: + content = fd.read() + return content From 1297c837cfba9362223f0c21b9ed253ce0300cf1 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 20:59:11 -0600 Subject: [PATCH 25/30] try silencing headers Signed-off-by: vsoch --- flux_metrics_api/apis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flux_metrics_api/apis.py b/flux_metrics_api/apis.py index e4654ff..6daec7f 100644 --- a/flux_metrics_api/apis.py +++ b/flux_metrics_api/apis.py @@ -43,7 +43,7 @@ def get_kubernetes_endpoint(endpoint): # res = requests.get(f"{api_server}/apis", headers=headers, verify=cert_file) # Kids don't do this at home output = subprocess.check_output( - f'curl --cacert {cert_file} --header "Authorization: Bearer {token}" -X GET {api_server}/{endpoint}', + f'curl -s --cacert {cert_file} --header "Authorization: Bearer {token}" -X GET {api_server}/{endpoint}', shell=True, ) try: From 41fbeacf867f86cdefb3714b09f708d993eaf5b8 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 21:06:20 -0600 Subject: [PATCH 26/30] add endpoints for openapi Signed-off-by: vsoch --- flux_metrics_api/apis.py | 9 ++++++++- flux_metrics_api/routes.py | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/flux_metrics_api/apis.py b/flux_metrics_api/apis.py index 6daec7f..5c5b3fd 100644 --- a/flux_metrics_api/apis.py +++ b/flux_metrics_api/apis.py @@ -17,6 +17,13 @@ def get_api_group_list(): return get_kubernetes_endpoint("apis") +def get_cluster_schema(version="v2"): + """ + Get the API group list, assuming we are inside a pod. + """ + return get_kubernetes_endpoint(f"openapi/{version}") + + def get_kubernetes_endpoint(endpoint): """ Get an endpoint from the cluster. @@ -43,7 +50,7 @@ def get_kubernetes_endpoint(endpoint): # res = requests.get(f"{api_server}/apis", headers=headers, verify=cert_file) # Kids don't do this at home output = subprocess.check_output( - f'curl -s --cacert {cert_file} --header "Authorization: Bearer {token}" -X GET {api_server}/{endpoint}', + f'curl --cacert {cert_file} --header "Authorization: Bearer {token}" -X GET {api_server}/{endpoint}', shell=True, ) try: diff --git a/flux_metrics_api/routes.py b/flux_metrics_api/routes.py index 4d29aea..aa24c66 100644 --- a/flux_metrics_api/routes.py +++ b/flux_metrics_api/routes.py @@ -23,6 +23,11 @@ ) ) +not_found_response = JSONResponse( + {"detail": "The metric server is not running in a Kubernetes pod."}, + status_code=404, +) + class Root(HTTPEndpoint): """ @@ -89,13 +94,23 @@ class APIGroupList(HTTPEndpoint): async def get(self, request): listing = types.new_group_list() if not listing: - return JSONResponse( - {"detail": "The metric server is not running in a Kubernetes pod."}, - status_code=404, - ) + return not_found_response return JSONResponse(listing) +class OpenAPI(HTTPEndpoint): + """ + Forward the cluster openapi endpoint + """ + + async def get(self, request): + version = request.path_params["version"] + openapi = types.get_cluster_schema(version) + if not openapi: + return not_found_response + return JSONResponse(openapi) + + def openapi_schema(request): """ Get the openapi spec from the endpoints @@ -108,6 +123,7 @@ def openapi_schema(request): Route(defaults.API_ROOT, Root), # This is a faux route so we can get the preferred resource version Route("/apis", APIGroupList), + Route("/openapi/{version}", OpenAPI), Route(defaults.API_ROOT + "/namespaces/{namespace}/metrics/{metric_name}", Metric), Route(defaults.API_ROOT + "/{resource}/{name}/{metric_name}", Metric), Route( From f03b5f5e5d47ffea09c7c1aad461db6d2a144147 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 21:09:15 -0600 Subject: [PATCH 27/30] add endpoints for openapi Signed-off-by: vsoch --- flux_metrics_api/apis.py | 14 -------------- flux_metrics_api/types.py | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/flux_metrics_api/apis.py b/flux_metrics_api/apis.py index 5c5b3fd..03785d5 100644 --- a/flux_metrics_api/apis.py +++ b/flux_metrics_api/apis.py @@ -10,20 +10,6 @@ import flux_metrics_api.utils as utils -def get_api_group_list(): - """ - Get the API group list, assuming we are inside a pod. - """ - return get_kubernetes_endpoint("apis") - - -def get_cluster_schema(version="v2"): - """ - Get the API group list, assuming we are inside a pod. - """ - return get_kubernetes_endpoint(f"openapi/{version}") - - def get_kubernetes_endpoint(endpoint): """ Get an endpoint from the cluster. diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index fe64373..f6de053 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -14,7 +14,21 @@ def new_group_list(): """ Return a faux group list to get the version of the custom metrics API """ - return apis.get_api_group_list() + return apis.get_kubernetes_endpoint("apis") + + +def get_api_group_list(): + """ + Get the API group list, assuming we are inside a pod. + """ + return apis.get_kubernetes_endpoint("apis") + + +def get_cluster_schema(version="v2"): + """ + Get the API group list, assuming we are inside a pod. + """ + return apis.get_kubernetes_endpoint(f"openapi/{version}") def new_resource_list(): From 0eda4a245860b458e256f3eecb106d051b4be1f0 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 21:10:09 -0600 Subject: [PATCH 28/30] add endpoints for openapi Signed-off-by: vsoch --- flux_metrics_api/types.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/flux_metrics_api/types.py b/flux_metrics_api/types.py index f6de053..c8f8393 100644 --- a/flux_metrics_api/types.py +++ b/flux_metrics_api/types.py @@ -17,13 +17,6 @@ def new_group_list(): return apis.get_kubernetes_endpoint("apis") -def get_api_group_list(): - """ - Get the API group list, assuming we are inside a pod. - """ - return apis.get_kubernetes_endpoint("apis") - - def get_cluster_schema(version="v2"): """ Get the API group list, assuming we are inside a pod. From 4854d86d6250d6a92883b280b4b0daac2a7ac103 Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 21:46:59 -0600 Subject: [PATCH 29/30] add endpoints for openapi Signed-off-by: vsoch --- flux_metrics_api/apis.py | 9 ++++++++- flux_metrics_api/defaults.py | 1 + flux_metrics_api/server.py | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/flux_metrics_api/apis.py b/flux_metrics_api/apis.py index 03785d5..7c5fb78 100644 --- a/flux_metrics_api/apis.py +++ b/flux_metrics_api/apis.py @@ -7,13 +7,20 @@ import os import subprocess +import flux_metrics_api.defaults as defaults import flux_metrics_api.utils as utils +# Global cache of responses +cache = {} + def get_kubernetes_endpoint(endpoint): """ Get an endpoint from the cluster. """ + if defaults.USE_CACHE and endpoint in cache: + return cache[endpoint] + # Point to the internal API server hostname api_server = "https://kubernetes.default.svc" @@ -40,7 +47,7 @@ def get_kubernetes_endpoint(endpoint): shell=True, ) try: - output = json.loadss(output) + output = json.loads(output) except Exception: return {} return output diff --git a/flux_metrics_api/defaults.py b/flux_metrics_api/defaults.py index c27bb5c..ad374d8 100644 --- a/flux_metrics_api/defaults.py +++ b/flux_metrics_api/defaults.py @@ -7,6 +7,7 @@ API_ROOT = "/apis/custom.metrics.k8s.io/v1beta2" NAMESPACE = "flux-operator" SERVICE_NAME = "custom-metrics-apiserver" +USE_CACHE = True def API_VERSION(): diff --git a/flux_metrics_api/server.py b/flux_metrics_api/server.py index 3588094..02a8581 100644 --- a/flux_metrics_api/server.py +++ b/flux_metrics_api/server.py @@ -90,6 +90,12 @@ def get_parser(): default=False, action="store_true", ) + start.add_argument( + "--no-cache", + help="Do not cache Kubernetes API responses.", + default=False, + action="store_true", + ) start.add_argument("--ssl-keyfile", help="full path to ssl keyfile") start.add_argument("--ssl-certfile", help="full path to ssl certfile") return parser @@ -149,6 +155,10 @@ def help(return_code=0): defaults.API_ROOT = args.api_path print(f"API endpoint is at {defaults.API_ROOT}") + # Do not cache responses + if args.no_cache is True: + defaults.USE_CACHE = False + # Set namespace or service name to be different than defaults if args.namespace: defaults.NAMESPACE = args.namespace From 1e713c5b15ce3b4b8a76fe86c5d92d6a61c95b5b Mon Sep 17 00:00:00 2001 From: vsoch Date: Tue, 30 May 2023 21:47:46 -0600 Subject: [PATCH 30/30] add endpoints for openapi Signed-off-by: vsoch --- flux_metrics_api/apis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flux_metrics_api/apis.py b/flux_metrics_api/apis.py index 7c5fb78..39c0dd6 100644 --- a/flux_metrics_api/apis.py +++ b/flux_metrics_api/apis.py @@ -48,6 +48,7 @@ def get_kubernetes_endpoint(endpoint): ) try: output = json.loads(output) + cache[endpoint] = output except Exception: return {} return output