diff --git a/cmd/experimental/kueue-viz/.gitignore b/cmd/experimental/kueue-viz/.gitignore new file mode 100644 index 0000000000..eaeec31871 --- /dev/null +++ b/cmd/experimental/kueue-viz/.gitignore @@ -0,0 +1,2 @@ +kubeconfig + diff --git a/cmd/experimental/kueue-viz/CONTRIBUTING.md b/cmd/experimental/kueue-viz/CONTRIBUTING.md new file mode 100644 index 0000000000..ac5624453e --- /dev/null +++ b/cmd/experimental/kueue-viz/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contribution guide + + + +## Backend +See [backend contribution guide](backend/CONTRIBUTING.md) + +## Frontend +See [frontend contribution guide](frontend/CONTRIBUTING.md) + + + diff --git a/cmd/experimental/kueue-viz/LICENSE b/cmd/experimental/kueue-viz/LICENSE new file mode 100644 index 0000000000..f37fd9764f --- /dev/null +++ b/cmd/experimental/kueue-viz/LICENSE @@ -0,0 +1,191 @@ + + 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 + + Copyright 2024, The Kubernetes Authors. + + 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/cmd/experimental/kueue-viz/README.md b/cmd/experimental/kueue-viz/README.md new file mode 100644 index 0000000000..6df879063c --- /dev/null +++ b/cmd/experimental/kueue-viz/README.md @@ -0,0 +1,91 @@ +# Build and Run locally + +## Prerequisites +You need a kubernetes cluster running kueue. +If you don't have a running cluster, you can create one using kind and install kueue using helm. + +``` +kind create cluster +kind get kubeconfig > kubeconfig +export KUBECONFIG=$PWD/kubeconfig +helm install kueue oci://us-central1-docker.pkg.dev/k8s-staging-images/charts/kueue \ + --version="v0.9.1" --create-namespace --namespace=kueue-system +``` + +## Build +Clone the kueue repository and build the projects: + +``` +git clone https://github.com/kubernetes-sigs/kueue +cd kueue/cmd/experimental/kueue-viz +KUEUE_VIZ_HOME=$PWD +kubectl create ns kueue-viz +cd backend && make && cd .. +cd frontend && make && cd .. +``` + +## Authorize +Create a cluster role that just has read only access on +`kueue` objects and pods, nodes and events. + +Create the cluster role: + +``` +kubectl create clusterrole kueue-backend-read-access --verb=get,list,watch \ + --resource=workloads,clusterqueues,localqueues,resourceflavors,pods,workloadpriorityclass,events,nodes +``` + +and bind it to the service account `default` in the `kueue-viz` namespace: + +``` +kubectl create -f - << EOF +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kueue-backend-read-access-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kueue-backend-read-access +subjects: +- kind: ServiceAccount + name: default + namespace: kueue-viz +EOF +``` +## Run + +`backend` uses `CompilerDaemon` to automatically rebuild go code. +You may need to install `CompilerDaemon` with the following command: + +``` +go get github.com/githubnemo/CompileDaemon +``` + +Then, in a first terminal run: + +``` +cd backend && make debug +``` + +And, in another terminal run: + +``` +cd frontend && make debug +``` + +## Test +Create test data using the resources in `examples/` directory. + +``` +kubectl create -f examples/ +``` +And check that you have some data on the dashboard. + +## Improve +See [contribution guide](CONTRIBUTING.md) + + + + + diff --git a/cmd/experimental/kueue-viz/backend/.gitignore b/cmd/experimental/kueue-viz/backend/.gitignore new file mode 100644 index 0000000000..2d67e10233 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/.gitignore @@ -0,0 +1,3 @@ +bin/ +kubeconfig + diff --git a/cmd/experimental/kueue-viz/backend/CONTRIBUTING.md b/cmd/experimental/kueue-viz/backend/CONTRIBUTING.md new file mode 100644 index 0000000000..bd83414682 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contribution guide + +## Local development + +Run the backend locally using in debug mode. Automatic code reload is enabled. + +``` +make debug +``` + +## Local run + +Run the backend locally using release mode. Logging is only set for fatal and error messages. + +``` +make run +``` + +## Local container run + +``` +podman build . -t backend +podman run -v $HOME/.kube:/nonexistent/.kube/ -p 8080:8080 backend +``` + diff --git a/cmd/experimental/kueue-viz/backend/Dockerfile b/cmd/experimental/kueue-viz/backend/Dockerfile new file mode 100644 index 0000000000..c0e41fb9d4 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/Dockerfile @@ -0,0 +1,31 @@ +# Build stage +ARG BUILDER_IMAGE=golang:1.23 +ARG BASE_IMAGE=gcr.io/distroless/static:nonroot +FROM --platform=${BUILDPLATFORM} ${BUILDER_IMAGE} AS builder + +# Copy Go modules manifests and download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the application source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 go build -o /kueue-viz + +# Runtime stage +FROM --platform=${BUILDPLATFORM} ${BASE_IMAGE} +USER 65532:65532 + +# Copy the built application from the builder stage +COPY --from=builder /kueue-viz / + +# Set environment variables +ENV PORT=8080 +# Expose the application port +EXPOSE 8080 + +# Command to run the application +ENTRYPOINT ["/kueue-viz"] + + diff --git a/cmd/experimental/kueue-viz/backend/Makefile b/cmd/experimental/kueue-viz/backend/Makefile new file mode 100644 index 0000000000..dda11f8001 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/Makefile @@ -0,0 +1,49 @@ +# Makefile for kueue-viz-go + +APP_NAME := kueue-viz +MODULE_NAME := kueue-viz + +.PHONY: all build run test tidy clean help + +# Default target +all: build + +## Build the Go application +build: + @echo "Building the application..." + go build -o bin/$(APP_NAME) + +## Run the application +run: build + @echo "Running the application..." + GIN_MODE=release ./bin/$(APP_NAME) + +## Run debug +debug: build + @echo "Running in debug mode with hot code replacement enabled..." + CompileDaemon --build="make" --command=./bin/$(APP_NAME) + +## Run tests +test: + @echo "Running tests..." + go test ./... -v + +## Tidy up dependencies +tidy: + @echo "Tidying up module dependencies..." + go mod tidy + +## Clean up build artifacts +clean: + @echo "Cleaning up..." + rm -f $(APP_NAME) + +## Display help +help: + @echo "Usage: make [target]" + @echo + @echo "Targets:" + @awk '/^[a-zA-Z\-]+:/ {print $$1}' $(MAKEFILE_LIST) | sed 's/:$$//' | xargs -n1 -I{} echo " {}" + + + diff --git a/cmd/experimental/kueue-viz/backend/README.md b/cmd/experimental/kueue-viz/backend/README.md new file mode 100644 index 0000000000..3b3219ae35 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/README.md @@ -0,0 +1,54 @@ +# Kueue WebSocket Application + +## Description +This Go application provides WebSocket endpoints for interacting with Kueue resources in a Kubernetes cluster. It uses the Gin framework for HTTP and WebSocket handling and the Kubernetes Go client for API interactions. + +## Features +- Fetch and broadcast `localqueues` over WebSocket. + +## Prerequisites +- A Kubernetes cluster +- Go 1.19+ +- `kubectl` configured to access the cluster + +## Installation + +1. Clone this repository. +2. Ensure Go is installed on your machine. + +## Build + +Run the following command to build the application: +```bash +CGO_ENABLED=0 go build -o kueue_ws_app +``` + +## Run + +Run the application: +```bash +./kueue_ws_app +``` + +The application starts on port `8080`. + +## Endpoints + +### WebSocket +## WebSocket Endpoints + +| Endpoint | Description | +|---------------------------------------------|--------------------------------------| +| `/ws/local-queues` | Streams updates for local queues | +| `/ws/cluster-queues` | Streams updates for cluster queues | +| `/ws/workloads` | Streams updates for workloads | +| `/ws/resource-flavors` | Streams updates for resource flavors | +| `/ws/resource-flavor/{flavor_name}` | Streams updates for a specific flavor| +| `/ws/local-queue/{namespace}/{queue_name}` | Streams updates for a specific queue| +| `/ws/cohorts` | Streams updates for cohorts | +| `/ws/cohort/{cohort_name}` | Streams updates for a specific cohort| +| `/ws/workload/{namespace}/{workload_name}` | Streams updates for a specific workload| +| `/ws/workload/{namespace}/{workload_name}/events` | Streams events for a specific workload| + + + diff --git a/cmd/experimental/kueue-viz/backend/go.mod b/cmd/experimental/kueue-viz/backend/go.mod new file mode 100644 index 0000000000..95b60446fc --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/go.mod @@ -0,0 +1,71 @@ +module kueue-viz + +go 1.23.0 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/gorilla/websocket v1.5.3 + k8s.io/api v0.31.3 + k8s.io/apimachinery v0.31.3 + k8s.io/client-go v0.31.3 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/gomega v1.33.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/cmd/experimental/kueue-viz/backend/go.sum b/cmd/experimental/kueue-viz/backend/go.sum new file mode 100644 index 0000000000..6cb47639d4 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/go.sum @@ -0,0 +1,207 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= +k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= +k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= +k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= +k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/cmd/experimental/kueue-viz/backend/handlers/cluster_queues.go b/cmd/experimental/kueue-viz/backend/handlers/cluster_queues.go new file mode 100644 index 0000000000..9da9b42f74 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/cluster_queues.go @@ -0,0 +1,166 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "context" + "fmt" + + "github.com/gin-gonic/gin" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" +) + +// ClusterQueuesWebSocketHandler streams all cluster queues +func ClusterQueuesWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return GenericWebSocketHandler(func() (interface{}, error) { + return fetchClusterQueues(dynamicClient) + }) +} + +// ClusterQueueDetailsWebSocketHandler streams details for a specific cluster queue +func ClusterQueueDetailsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + clusterQueueName := c.Param("cluster_queue_name") + GenericWebSocketHandler(func() (interface{}, error) { + return fetchClusterQueueDetails(dynamicClient, clusterQueueName) + })(c) + } +} + +// Fetch all cluster queues +func fetchClusterQueues(dynamicClient dynamic.Interface) ([]map[string]interface{}, error) { + // Fetch the list of ClusterQueue objects + clusterQueues, err := dynamicClient.Resource(ClusterQueuesGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching cluster queues: %v", err) + } + + // Process the ClusterQueue objects + var result []map[string]interface{} + for _, item := range clusterQueues.Items { + // Extract relevant fields + name := item.GetName() + spec, specExists := item.Object["spec"].(map[string]interface{}) + status, statusExists := item.Object["status"].(map[string]interface{}) + + var cohort string + var resourceGroups []interface{} + if specExists { + cohort, _ = spec["cohort"].(string) + resourceGroups, _ = spec["resourceGroups"].([]interface{}) + } + + var admittedWorkloads, pendingWorkloads, reservingWorkloads int64 + if statusExists { + admittedWorkloads, _ = status["admittedWorkloads"].(int64) + pendingWorkloads, _ = status["pendingWorkloads"].(int64) + reservingWorkloads, _ = status["reservingWorkloads"].(int64) + } + + // Extract flavors from resourceGroups + var flavors []string + for _, rg := range resourceGroups { + rgMap, ok := rg.(map[string]interface{}) + if !ok { + continue + } + flavorsList, _ := rgMap["flavors"].([]interface{}) + for _, flavor := range flavorsList { + flavorMap, ok := flavor.(map[string]interface{}) + if !ok { + continue + } + if flavorName, ok := flavorMap["name"].(string); ok { + flavors = append(flavors, flavorName) + } + } + } + + // Add the cluster queue to the result list + result = append(result, map[string]interface{}{ + "name": name, + "cohort": cohort, + "resourceGroups": resourceGroups, + "admittedWorkloads": admittedWorkloads, + "pendingWorkloads": pendingWorkloads, + "reservingWorkloads": reservingWorkloads, + "flavors": flavors, + }) + } + + return result, nil +} + +// Fetch details for a specific cluster queue +func fetchClusterQueueDetails(dynamicClient dynamic.Interface, clusterQueueName string) (map[string]interface{}, error) { + + // Fetch the specific ClusterQueue + clusterQueue, err := dynamicClient.Resource(ClusterQueuesGVR()).Get(context.TODO(), clusterQueueName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching cluster queue %s: %v", clusterQueueName, err) + } + + // Retrieve all LocalQueues + localQueues, err := dynamicClient.Resource(LocalQueuesGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching local queues: %v", err) + } + + // Filter LocalQueues based on the ClusterQueue name + var queuesUsingClusterQueue []map[string]interface{} + for _, item := range localQueues.Items { + spec, specExists := item.Object["spec"].(map[string]interface{}) + if !specExists { + continue + } + clusterQueueRef, _ := spec["clusterQueue"].(string) + if clusterQueueRef != clusterQueueName { + continue + } + + // Extract relevant fields + namespace := item.GetNamespace() + name := item.GetName() + status, statusExists := item.Object["status"].(map[string]interface{}) + + var reservation, usage interface{} + if statusExists { + reservation = status["flavorsReservation"] + usage = status["flavorUsage"] + } + + queuesUsingClusterQueue = append(queuesUsingClusterQueue, map[string]interface{}{ + "namespace": namespace, + "name": name, + "reservation": reservation, + "usage": usage, + }) + } + + // Attach the queues information to the ClusterQueue details + clusterQueueDetails := clusterQueue.Object + clusterQueueDetails["queues"] = queuesUsingClusterQueue + + return clusterQueueDetails, nil +} + +func fetchClusterQueuesList(dynamicClient dynamic.Interface) (*unstructured.UnstructuredList, error) { + clusterQueues, err := dynamicClient.Resource(ClusterQueuesGVR()).List(context.TODO(), metav1.ListOptions{}) + return clusterQueues, err +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/cohorts.go b/cmd/experimental/kueue-viz/backend/handlers/cohorts.go new file mode 100644 index 0000000000..8847bd1a9f --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/cohorts.go @@ -0,0 +1,128 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "k8s.io/client-go/dynamic" +) + +// CohortsWebSocketHandler streams all cohorts +func CohortsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return GenericWebSocketHandler(func() (interface{}, error) { + return fetchCohorts(dynamicClient) + }) +} + +// CohortDetailsWebSocketHandler streams details for a specific cohort +func CohortDetailsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + cohortName := c.Param("cohort_name") + + GenericWebSocketHandler(func() (interface{}, error) { + return fetchCohortDetails(dynamicClient, cohortName) + })(c) + } +} + +// Fetch all cohorts +func fetchCohorts(dynamicClient dynamic.Interface) (interface{}, error) { + clusterQueues, err := fetchClusterQueuesList(dynamicClient) + + if err != nil { + return nil, fmt.Errorf("error fetching cohorts: %v", err) + } + cohorts := make(map[string]map[string]interface{}) + + // Iterate through cluster queue items + for _, item := range clusterQueues.Items { + // Extract spec and metadata + spec, specExists := item.Object["spec"].(map[string]interface{}) + metadata, metadataExists := item.Object["metadata"].(map[string]interface{}) + if !specExists || !metadataExists { + continue + } + + // Get cohort name from the spec + cohortName, cohortExists := spec["cohort"].(string) + if !cohortExists || cohortName == "" { + continue + } + + // Get cluster queue name + queueName, queueNameExists := metadata["name"].(string) + if !queueNameExists { + continue + } + + // Initialize the cohort in the map if it doesn't exist + if _, exists := cohorts[cohortName]; !exists { + cohorts[cohortName] = map[string]interface{}{ + "name": cohortName, + "clusterQueues": []map[string]interface{}{}, + } + } + + // Add the current cluster queue to the cohort + clusterQueuesList := cohorts[cohortName]["clusterQueues"].([]map[string]interface{}) + clusterQueuesList = append(clusterQueuesList, map[string]interface{}{ + "name": queueName, + }) + cohorts[cohortName]["clusterQueues"] = clusterQueuesList + } + + // Convert the cohorts map to a list + var result []map[string]interface{} + for _, cohort := range cohorts { + result = append(result, cohort) + } + + return result, nil +} + +// Fetch details for a specific cohort +func fetchCohortDetails(dynamicClient dynamic.Interface, cohortName string) (map[string]interface{}, error) { + // Retrieve all cluster queues + clusterQueues, err := fetchClusterQueuesList(dynamicClient) + if err != nil { + return nil, fmt.Errorf("error fetching cohort details: %v", err) + } + + // Prepare the result + cohortDetails := make(map[string]interface{}) + cohortDetails["cohort"] = cohortName + cohortDetails["clusterQueues"] = []map[string]interface{}{} + + // Iterate through the cluster queues and filter by cohort name + for _, item := range clusterQueues.Items { + queue := item.Object + if queueSpec, ok := queue["spec"].(map[string]interface{}); ok { + if queueSpec["cohort"] == cohortName { + queueDetails := map[string]interface{}{ + "name": item.GetName(), + "spec": queueSpec, + "status": queue["status"], + } + cohortDetails["clusterQueues"] = append(cohortDetails["clusterQueues"].([]map[string]interface{}), queueDetails) + } + } + } + + return cohortDetails, nil +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/gvr.go b/cmd/experimental/kueue-viz/backend/handlers/gvr.go new file mode 100644 index 0000000000..fa310111a8 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/gvr.go @@ -0,0 +1,100 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ClusterQueuesGVR defines the GroupVersionResource for ClusterQueues +func ClusterQueuesGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: "kueue.x-k8s.io", + Version: "v1beta1", + Resource: "clusterqueues", + } +} + +// WorkloadsGVR defines the GroupVersionResource for Workloads +func WorkloadsGVR() schema.GroupVersionResource { + workloadsGVR := schema.GroupVersionResource{ + Group: "kueue.x-k8s.io", + Version: "v1beta1", + Resource: "workloads", + } + return workloadsGVR +} + +// LocalQueuesGVR defines the GroupVersionResource for LocalQueues +func LocalQueuesGVR() schema.GroupVersionResource { + localQueuesGVR := schema.GroupVersionResource{ + Group: "kueue.x-k8s.io", + Version: "v1beta1", + Resource: "localqueues", + } + return localQueuesGVR +} + +// CohortsGVR defines the GroupVersionResource for Cohorts +func CohortsGVR() schema.GroupVersionResource { + cohortsGVR := schema.GroupVersionResource{ + Group: "kueue.x-k8s.io", + Version: "v1beta1", + Resource: "cohorts", + } + return cohortsGVR +} + +// ResourceFlavorsGVR defines the GroupVersionResource for ResourceFlavors +func ResourceFlavorsGVR() schema.GroupVersionResource { + resourceFlavorsGVR := schema.GroupVersionResource{ + Group: "kueue.x-k8s.io", + Version: "v1beta1", + Resource: "resourceflavors", + } + return resourceFlavorsGVR +} + +// NodesGVR defines the GroupVersionResource for Nodes +func NodesGVR() schema.GroupVersionResource { + nodeGVR := schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "nodes", + } + return nodeGVR +} + +// EventsGVR defines the GroupVersionResource for Events +func EventsGVR() schema.GroupVersionResource { + eventsGVR := schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "events", + } + return eventsGVR +} + +// PodsGVR defines the GroupVersionResource Pods +func PodsGVR() schema.GroupVersionResource { + podsGVR := schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "pods", + } + return podsGVR +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/handlers.go b/cmd/experimental/kueue-viz/backend/handlers/handlers.go new file mode 100644 index 0000000000..496546c3c5 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/handlers.go @@ -0,0 +1,50 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "github.com/gin-gonic/gin" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +func InitializeWebSocketRoutes(router *gin.Engine, dynamicClient dynamic.Interface, k8sClient *kubernetes.Clientset) { + + // Workloads + router.GET("/ws/workloads", WorkloadsWebSocketHandler(dynamicClient)) + router.GET("/ws/workloads/dashboard", WorkloadsDashboardWebSocketHandler(dynamicClient)) + + router.GET("/ws/workload/:namespace/:workload_name", WorkloadDetailsWebSocketHandler(dynamicClient)) + router.GET("/ws/workload/:namespace/:workload_name/events", WorkloadEventsWebSocketHandler(dynamicClient)) + + // Local Queues + router.GET("/ws/local-queues", LocalQueuesWebSocketHandler(dynamicClient)) + router.GET("/ws/local-queue/:namespace/:queue_name", LocalQueueDetailsWebSocketHandler(dynamicClient)) + router.GET("/ws/local-queue/:namespace/:queue_name/workloads", LocalQueueWorkloadsWebSocketHandler(dynamicClient)) + + // Cluster Queues + router.GET("/ws/cluster-queues", ClusterQueuesWebSocketHandler(dynamicClient)) + router.GET("/ws/cluster-queue/:cluster_queue_name", ClusterQueueDetailsWebSocketHandler(dynamicClient)) // New route + + // Cohorts + router.GET("/ws/cohorts", CohortsWebSocketHandler(dynamicClient)) + router.GET("/ws/cohort/:cohort_name", CohortDetailsWebSocketHandler(dynamicClient)) + + // Resource Flavors + router.GET("/ws/resource-flavors", ResourceFlavorsWebSocketHandler(dynamicClient)) + router.GET("/ws/resource-flavor/:flavor_name", ResourceFlavorDetailsWebSocketHandler(dynamicClient)) +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/local_queue_workloads.go b/cmd/experimental/kueue-viz/backend/handlers/local_queue_workloads.go new file mode 100644 index 0000000000..a24cf4c454 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/local_queue_workloads.go @@ -0,0 +1,51 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "context" + "fmt" + + "github.com/gin-gonic/gin" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +func LocalQueueWorkloadsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + namespace := c.Param("namespace") + queueName := c.Param("queue_name") + GenericWebSocketHandler(func() (interface{}, error) { + return fetchLocalQueueWorkloads(dynamicClient, namespace, queueName) + })(c) + } +} + +func fetchLocalQueueWorkloads(dynamicClient dynamic.Interface, namespace, queueName string) (interface{}, error) { + result, err := dynamicClient.Resource(WorkloadsGVR()).Namespace(namespace).List(context.TODO(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("spec.queueName=%s", queueName), + }) + if err != nil { + return nil, fmt.Errorf("error fetching workloads for local queue %s: %v", queueName, err) + } + + var workloads []map[string]interface{} + for _, item := range result.Items { + workloads = append(workloads, item.Object) + } + return workloads, nil +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/local_queues.go b/cmd/experimental/kueue-viz/backend/handlers/local_queues.go new file mode 100644 index 0000000000..468b36d7cc --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/local_queues.go @@ -0,0 +1,76 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "context" + "fmt" + + "github.com/gin-gonic/gin" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +// LocalQueuesWebSocketHandler streams all local queues +func LocalQueuesWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return GenericWebSocketHandler(func() (interface{}, error) { + return fetchLocalQueues(dynamicClient) + }) +} + +// LocalQueueDetailsWebSocketHandler streams details for a specific local queue +func LocalQueueDetailsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + namespace := c.Param("namespace") + queueName := c.Param("queue_name") + GenericWebSocketHandler(func() (interface{}, error) { + return fetchLocalQueueDetails(dynamicClient, namespace, queueName) + })(c) + } +} + +// Fetch all local queues +func fetchLocalQueues(dynamicClient dynamic.Interface) (interface{}, error) { + result, err := dynamicClient.Resource(LocalQueuesGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching local queues: %v", err) + } + + var queues []map[string]interface{} + for _, item := range result.Items { + queue := item.Object + queue["namespace"] = item.GetNamespace() + queue["name"] = item.GetName() + status, statusExists := item.Object["status"].(map[string]interface{}) + // Include the status if it exists + if statusExists { + queue["status"] = status + } + queues = append(queues, queue) + } + return queues, nil +} + +// Fetch details for a specific local queue +func fetchLocalQueueDetails(dynamicClient dynamic.Interface, namespace, queueName string) (interface{}, error) { + + result, err := dynamicClient.Resource(LocalQueuesGVR()).Namespace(namespace).Get(context.TODO(), queueName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching details for local queue %s: %v", queueName, err) + } + return result.Object, nil +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/resource_flavors.go b/cmd/experimental/kueue-viz/backend/handlers/resource_flavors.go new file mode 100644 index 0000000000..506a72a55f --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/resource_flavors.go @@ -0,0 +1,221 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "context" + "fmt" + "log" + + "github.com/gin-gonic/gin" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" +) + +// ResourceFlavorsWebSocketHandler streams all resource flavors +func ResourceFlavorsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + + return GenericWebSocketHandler(func() (interface{}, error) { + return fetchResourceFlavors(dynamicClient) + }) +} + +// ResourceFlavorDetailsWebSocketHandler streams details for a specific resource flavor +func ResourceFlavorDetailsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + flavorName := c.Param("flavor_name") + GenericWebSocketHandler(func() (interface{}, error) { + return fetchResourceFlavorDetails(dynamicClient, flavorName) + })(c) + } +} + +// Fetch all resource flavors +func fetchResourceFlavors(dynamicClient dynamic.Interface) (interface{}, error) { + result, err := dynamicClient.Resource(ResourceFlavorsGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching resource flavors: %v", err) + } + + var flavors []map[string]interface{} + for _, item := range result.Items { + object := item.Object + object["name"] = item.GetName() + spec, _ := item.Object["spec"].(map[string]interface{}) + object["details"] = spec + + flavors = append(flavors, object) + } + return flavors, nil +} + +// Fetch details for a specific Resource Flavor +func fetchResourceFlavorDetails(dynamicClient dynamic.Interface, flavorName string) (map[string]interface{}, error) { + // Fetch the specified resource flavor details + flavor, err := dynamicClient.Resource(ResourceFlavorsGVR()).Get(context.TODO(), flavorName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching resource flavor %s: %v", flavorName, err) + } + + // List all cluster queues + clusterQueues, err := dynamicClient.Resource(ClusterQueuesGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("error listing cluster queues: %v", err) + } + + queuesUsingFlavor := []map[string]interface{}{} + + // Iterate through each cluster queue to find queues using the specified flavor + for _, item := range clusterQueues.Items { + queueName, _, _ := unstructured.NestedString(item.Object, "metadata", "name") + resourceGroups, _, _ := unstructured.NestedSlice(item.Object, "spec", "resourceGroups") + + for _, group := range resourceGroups { + groupMap, ok := group.(map[string]interface{}) + if !ok { + continue + } + flavors, _, _ := unstructured.NestedSlice(groupMap, "flavors") + + for _, fl := range flavors { + flavorMap, ok := fl.(map[string]interface{}) + if !ok { + continue + } + name, _, _ := unstructured.NestedString(flavorMap, "name") + if name == flavorName { + // Collect resource and quota information + quotaInfo := []map[string]interface{}{} + resources, _, _ := unstructured.NestedSlice(flavorMap, "resources") + + for _, res := range resources { + resMap, ok := res.(map[string]interface{}) + if !ok { + continue + } + resourceName, _, _ := unstructured.NestedString(resMap, "name") + nominalQuota, _, _ := unstructured.NestedString(resMap, "nominalQuota") + + quotaInfo = append(quotaInfo, map[string]interface{}{ + "resource": resourceName, + "nominalQuota": nominalQuota, + }) + } + + queuesUsingFlavor = append(queuesUsingFlavor, map[string]interface{}{ + "queueName": queueName, + "quota": quotaInfo, + }) + //log.Println(queuesUsingFlavor) + break // Stop searching this queue once the flavor is found + } + } + } + } + + // Retrieve matching nodes for the flavor (assumes getNodesForFlavor is implemented) + matchingNodes, _ := getNodesForFlavor(dynamicClient, flavorName) + log.Println(matchingNodes) + + details := map[string]interface{}{ + "tolerations": []map[string]interface{}{}, + "taints": []map[string]interface{}{}, + } + if spec, exists := flavor.Object["spec"]; exists { + details = spec.(map[string]interface{}) + } + tolerations, found, err := unstructured.NestedSlice(flavor.Object, "spec", "tolerations") + if err == nil && found { + details["tolerations"] = tolerations + } + details["taints"] = queuesUsingFlavor + + // Construct the result + result := map[string]interface{}{ + "name": flavorName, + "details": details, + "queues": queuesUsingFlavor, + "nodes": matchingNodes, + } + + return result, nil +} + +// getNodesForFlavor retrieves a list of nodes that match a specific resource flavor. +func getNodesForFlavor(dynamicClient dynamic.Interface, flavorName string) ([]map[string]interface{}, error) { + + flavor, err := dynamicClient.Resource(ResourceFlavorsGVR()).Get(context.TODO(), flavorName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching resource flavor %s: %v", flavorName, err) + } + + // List all nodes + nodeList, err := dynamicClient.Resource(NodesGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching nodes: %v", err) + } + + var matchingNodes []map[string]interface{} + nodeLabels, _, err := unstructured.NestedMap(flavor.Object, "spec", "nodeLabels") + if err != nil { + return nil, fmt.Errorf("error reading nodeLabels for flavor %s: %v", flavorName, err) + } + + // Iterate through each node to find matches for the flavor's nodeLabels + for _, node := range nodeList.Items { + nodeName := node.GetName() + // Convert the unstructured node object to the corev1.Node type + nodeObj := &v1.Node{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(node.Object, nodeObj) + if err != nil { + log.Printf("Error converting node %s to corev1.Node: %v", nodeName, err) + continue + } + // Check if the node has all the labels specified in the flavor + if hasMatchingLabels(nodeObj.Labels, nodeLabels) { + taints := []map[string]interface{}{} + for _, taint := range nodeObj.Spec.Taints { + taints = append(taints, map[string]interface{}{ + "key": taint.Key, + "value": taint.Value, + "effect": string(taint.Effect), + }) + } + matchingNodes = append(matchingNodes, map[string]interface{}{ + "name": nodeName, + "labels": nodeObj.Labels, + "taints": taints, + "tolerations": []v1.Taint{}, + }) + } + } + return matchingNodes, nil +} + +// hasMatchingLabels checks if a node's labels contain all the labels specified in nodeLabels. +func hasMatchingLabels(nodeLabels map[string]string, flavorLabels map[string]interface{}) bool { + for key, value := range flavorLabels { + strValue, ok := value.(string) + if !ok || nodeLabels[key] != strValue { + return false + } + } + return true +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/websocket_handler.go b/cmd/experimental/kueue-viz/backend/handlers/websocket_handler.go new file mode 100644 index 0000000000..5a3f1ea394 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/websocket_handler.go @@ -0,0 +1,124 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "encoding/json" + log "log/slog" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +// WebSocket upgrader +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// GenericWebSocketHandler creates a WebSocket endpoint with periodic data updates +func GenericWebSocketHandler(dataFetcher func() (interface{}, error)) gin.HandlerFunc { + return func(c *gin.Context) { + startTime := time.Now() + log.Debug("WebSocket handler started") + + // Upgrade the HTTP connection to a WebSocket connection + connStart := time.Now() + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Debug("Failed to upgrade to WebSocket: %v", "error", err) + return + } + defer conn.Close() + log.Debug("WebSocket connection established took %v", "duration", time.Since(connStart)) + + // Fetch the initial data to send it immediately + fetchStart := time.Now() + data, err := dataFetcher() + if err != nil { + log.Error("Error fetching data %v", "error", err) + return + } + log.Debug("Data fetched took %v", "duration", time.Since(fetchStart)) + + // Marshal the fetched data into JSON + marshalStart := time.Now() + jsonData, err := json.Marshal(data) + if err != nil { + log.Error("Error marshaling data : %v", "error", err) + return + } + log.Debug("Data marshaled into JSON at took %v", "duration", time.Since(marshalStart)) + + // Set write deadline to avoid blocking indefinitely + conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) + + // Send the initial data to the WebSocket client immediately + writeStart := time.Now() + err = conn.WriteMessage(websocket.TextMessage, jsonData) + if err != nil { + log.Error("Error writing message : %v", "error", err) + // If writing fails, break the loop and close the connection + return + } + log.Debug("Initial message sent to client took %v", "duration", time.Since(writeStart)) + + // Start a ticker for periodic updates (every 5 seconds) + // TODO use SharedInformers and TTL to only send updates if they happen + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + // Continue sending updates every 5 seconds + for range ticker.C { + // Fetch the latest data + fetchStart := time.Now() + data, err := dataFetcher() + if err != nil { + log.Error("Error fetching data %v", "error", err) + continue + } + log.Debug("Data fetched at took %v", "duration", time.Since(fetchStart)) + + // Marshal the fetched data into JSON + marshalStart := time.Now() + jsonData, err := json.Marshal(data) + if err != nil { + log.Error("Error marshaling data at %v", "error", err) + continue + } + log.Debug("Data marshaled into JSON took %v", "duration", time.Since(marshalStart)) + + // Set write deadline to avoid blocking indefinitely + conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) + + // Send the JSON data to the WebSocket client + writeStart := time.Now() + err = conn.WriteMessage(websocket.TextMessage, jsonData) + if err != nil { + log.Error("Error writing message: ", "error", err) + // If writing fails, break the loop and close the connection + break + } + log.Debug("Message sent to client took %v", "duration", time.Since(writeStart)) + } + + log.Debug("WebSocket handler completed total time: %v", "duration", time.Since(startTime)) + } +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/workloads.go b/cmd/experimental/kueue-viz/backend/handlers/workloads.go new file mode 100644 index 0000000000..014415cd6d --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/workloads.go @@ -0,0 +1,127 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "context" + "fmt" + + "github.com/gin-gonic/gin" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +func WorkloadsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return GenericWebSocketHandler(func() (interface{}, error) { + workloads, err := fetchWorkloads(dynamicClient) + result := map[string]interface{}{ + "workloads": workloads, + } + return result, err + }) +} + +func WorkloadDetailsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + namespace := c.Param("namespace") + workloadName := c.Param("workload_name") + GenericWebSocketHandler(func() (interface{}, error) { + return fetchWorkloadDetails(dynamicClient, namespace, workloadName) + })(c) + } +} + +func fetchWorkloads(dynamicClient dynamic.Interface) (interface{}, error) { + result, err := dynamicClient.Resource(WorkloadsGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching resource flavors: %v", err) + } + return result, nil +} + +func fetchWorkloadDetails(dynamicClient dynamic.Interface, namespace, workloadName string) (interface{}, error) { + // Fetch the workload details + workload, err := dynamicClient.Resource(WorkloadsGVR()).Namespace(namespace).Get(context.TODO(), workloadName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching workload %s: %v", workloadName, err) + } + + // Add preemption details if available + preempted := false + if status, ok := workload.Object["status"].(map[string]interface{}); ok { + preempted, _ = status["preempted"].(bool) + } + preemptionReason := "None" + if reason, ok := workload.Object["status"].(map[string]interface{})["preemptionReason"].(string); ok { + preemptionReason = reason + } + workload.Object["preemption"] = map[string]interface{}{ + "preempted": preempted, + "reason": preemptionReason, + } + + // Get the local queue name from workload's spec + localQueueName := "" + if spec, ok := workload.Object["spec"].(map[string]interface{}); ok { + localQueueName, _ = spec["queueName"].(string) + } + + // If local queue name is found, fetch the local queue and its cluster queue + if localQueueName != "" { + localQueue, err := dynamicClient.Resource(LocalQueuesGVR()).Namespace(namespace).Get(context.TODO(), localQueueName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("error fetching local queue %s: %v", localQueueName, err) + } + // Retrieve the targeted cluster queue name from the local queue's spec + if spec, ok := localQueue.Object["spec"].(map[string]interface{}); ok { + clusterQueueName, _ := spec["clusterQueue"].(string) + workload.Object["clusterQueueName"] = clusterQueueName + } else { + workload.Object["clusterQueueName"] = "Unknown" + } + } else { + workload.Object["clusterQueueName"] = "Unknown" + } + + return workload, nil +} + +func WorkloadEventsWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + namespace := c.Param("namespace") + workloadName := c.Param("workload_name") + + GenericWebSocketHandler(func() (interface{}, error) { + return fetchWorkloadEvents(dynamicClient, namespace, workloadName) + })(c) + } +} + +func fetchWorkloadEvents(dynamicClient dynamic.Interface, namespace, workloadName string) (interface{}, error) { + result, err := dynamicClient.Resource(EventsGVR()).Namespace(namespace).List(context.TODO(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("involvedObject.name=%s", workloadName), + }) + if err != nil { + return nil, fmt.Errorf("error fetching events for workload %s: %v", workloadName, err) + } + + var events []map[string]interface{} + for _, item := range result.Items { + events = append(events, item.Object) + } + return events, nil +} diff --git a/cmd/experimental/kueue-viz/backend/handlers/workloads_dashboard.go b/cmd/experimental/kueue-viz/backend/handlers/workloads_dashboard.go new file mode 100644 index 0000000000..650a1cde38 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/handlers/workloads_dashboard.go @@ -0,0 +1,173 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package handlers + +import ( + "context" + "fmt" + "log" + + "github.com/gin-gonic/gin" // Import v1 for Pod and PodStatus + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" +) + +// WorkloadsDashboardWebSocketHandler streams workloads along with attached pod details +func WorkloadsDashboardWebSocketHandler(dynamicClient dynamic.Interface) gin.HandlerFunc { + return func(c *gin.Context) { + GenericWebSocketHandler(func() (interface{}, error) { + return fetchDashboardData(dynamicClient) + })(c) + } +} + +func fetchDashboardData(dynamicClient dynamic.Interface) (map[string]interface{}, error) { + resourceFlavors, _ := fetchResourceFlavors(dynamicClient) + clusterQueues, _ := fetchClusterQueues(dynamicClient) + localQueues, _ := fetchLocalQueues(dynamicClient) + workloads := fetchWorkloadsDashboardData(dynamicClient) + result := map[string]interface{}{ + "flavors": removeManagedFields(resourceFlavors), + "clusterQueues": removeManagedFields(clusterQueues), + "queues": removeManagedFields(localQueues), + "workloads": removeManagedFields(workloads), + } + return result, nil + +} + +func fetchWorkloadsDashboardData(dynamicClient dynamic.Interface) interface{} { + workloadList, err := dynamicClient.Resource(WorkloadsGVR()).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + fmt.Printf("error fetching workloads: %v", err) + } + + workloadList = removeManagedFieldsFromUnstructuredList(workloadList) + workloadsByUID := make(map[string]string) + var processedWorkloads []unstructured.Unstructured + + for _, workload := range workloadList.Items { + metadata, _, _ := unstructured.NestedMap(workload.Object, "metadata") + status, _, _ := unstructured.NestedMap(workload.Object, "status") + labels, _, _ := unstructured.NestedStringMap(metadata, "labels") + namespace := metadata["namespace"].(string) + workloadName := metadata["name"].(string) + workloadUID := metadata["uid"].(string) + jobUID := labels["kueue.x-k8s.io/job-uid"] + + podList, err := dynamicClient.Resource(PodsGVR()).Namespace(namespace).List(context.TODO(), metav1.ListOptions{}) + podList = removeManagedFieldsFromUnstructuredList(podList) + if err != nil { + fmt.Printf("error fetching pods in namespace %s: %v", namespace, err) + return nil + } + + var workloadPods []map[string]interface{} + for _, pod := range podList.Items { + podLabels, _, _ := unstructured.NestedStringMap(pod.Object, "metadata", "labels") + controllerUID := podLabels["controller-uid"] + if controllerUID == jobUID { + podDetails := map[string]interface{}{ + "name": pod.Object["metadata"].(map[string]interface{})["name"], + "status": pod.Object["status"], + } + workloadPods = append(workloadPods, podDetails) + } + } + + preempted := false + if preemptedVal, ok := status["preempted"].(bool); ok { + preempted = preemptedVal + } else { + preempted = false // Default to false if not found or not a bool + } + + preemptionReason := "None" + if reason, ok := status["preemptionReason"].(string); ok { + preemptionReason = reason + } + + preemption := map[string]interface{}{"preempted": preempted, "reason": preemptionReason} + unstructured.SetNestedField(workload.Object, preemption, "preemption") + addPodsToWorkload(&workload, workloadPods) + workloadsByUID[workloadUID] = workloadName + processedWorkloads = append(processedWorkloads, workload) + } + workloads := map[string]interface{}{ + "items": processedWorkloads, + "workloads_by_uid": workloadsByUID, + } + + return workloads +} + +func addPodsToWorkload(workload *unstructured.Unstructured, pods []map[string]interface{}) error { + // Convert []map[string]interface{} to []interface{} + var podsInterface []interface{} + for _, pod := range pods { + podsInterface = append(podsInterface, pod) + } + + // Add pods as a field named "pods" in workload.Object + err := unstructured.SetNestedField(workload.Object, podsInterface, "pods") + if err != nil { + log.Printf("Error setting pods in workload: %v", err) + return err + } + return nil +} + +// removeManagedFieldsFromUnstructuredList removes "managedFields" recursively from *unstructured.UnstructuredList +func removeManagedFieldsFromUnstructuredList(list *unstructured.UnstructuredList) *unstructured.UnstructuredList { + for i, item := range list.Items { + list.Items[i] = *removeManagedFieldsFromUnstructured(&item) + } + return list +} + +// removeManagedFieldsFromUnstructured removes "managedFields" recursively from *unstructured.Unstructured +func removeManagedFieldsFromUnstructured(obj *unstructured.Unstructured) *unstructured.Unstructured { + obj.Object = removeManagedFields(obj.Object).(map[string]interface{}) + return obj +} + +// removeManagedFields recursively removes "managedFields" from maps and slices. +func removeManagedFields(obj interface{}) interface{} { + switch val := obj.(type) { + case map[string]interface{}: + // Remove "managedFields" if present + delete(val, "managedFields") + + // Recursively apply to nested items + for key, value := range val { + val[key] = removeManagedFields(value) + } + return val + + case []interface{}: + // Recursively apply to each item in the list + for i, item := range val { + val[i] = removeManagedFields(item) + } + return val + + default: + // Return the object as is if it's neither a map nor a slice + return obj + } +} diff --git a/cmd/experimental/kueue-viz/backend/kubernetes_client.go b/cmd/experimental/kueue-viz/backend/kubernetes_client.go new file mode 100644 index 0000000000..c6d829e936 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/kubernetes_client.go @@ -0,0 +1,69 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package main + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// createK8sClient initializes Kubernetes clients, checking for in-cluster or local kubeconfig +func createK8sClient() (*kubernetes.Clientset, dynamic.Interface, error) { + var config *rest.Config + var err error + + // Check for in-cluster configuration + if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token"); err == nil { + config, err = rest.InClusterConfig() + if err != nil { + return nil, nil, fmt.Errorf("failed to load in-cluster config: %v", err) + } + fmt.Println("Using in-cluster configuration") + } else { + // Fall back to using KUBECONFIG or default kubeconfig path + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") + } + + config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, nil, fmt.Errorf("failed to load kubeconfig from %s: %v", kubeconfig, err) + } + fmt.Printf("Using kubeconfig: %s\n", kubeconfig) + } + + // Create the Kubernetes clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, nil, fmt.Errorf("failed to create Kubernetes clientset: %v", err) + } + + // Create the dynamic client + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, nil, fmt.Errorf("failed to create dynamic client: %v", err) + } + + return clientset, dynamicClient, nil +} diff --git a/cmd/experimental/kueue-viz/backend/main.go b/cmd/experimental/kueue-viz/backend/main.go new file mode 100644 index 0000000000..ccac243b53 --- /dev/null +++ b/cmd/experimental/kueue-viz/backend/main.go @@ -0,0 +1,53 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +package main + +import ( + "log" + + "kueue-viz/handlers" + + "github.com/gin-gonic/gin" + + "net/http" + _ "net/http/pprof" +) + +func main() { + // Start pprof server for profiling + go func() { + log.Println("Starting pprof server on :6060") + if err := http.ListenAndServe("localhost:6060", nil); err != nil { + log.Fatalf("Error starting pprof server: %v", err) + } + }() + + k8sClient, dynamicClient, err := createK8sClient() + if err != nil { + log.Fatalf("Error creating Kubernetes client: %v", err) + } + r := gin.New() + r.Use(gin.Logger()) + r.SetTrustedProxies(nil) + + handlers.InitializeWebSocketRoutes(r, dynamicClient, k8sClient) + + if err := r.Run(":8080"); err != nil { + log.Fatalf("Failed to start server: %v", err) + } + +} diff --git a/cmd/experimental/kueue-viz/examples/00-resource-flavor.yaml b/cmd/experimental/kueue-viz/examples/00-resource-flavor.yaml new file mode 100644 index 0000000000..5a439aa8f2 --- /dev/null +++ b/cmd/experimental/kueue-viz/examples/00-resource-flavor.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ResourceFlavor +metadata: + name: "default-flavor" + +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ResourceFlavor +metadata: + name: "gpu" +spec: + nodeLabels: + instance-type: gpu + +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ResourceFlavor +metadata: + name: "spot" +spec: + nodeLabels: + instance-type: spot + nodeTaints: + - effect: NoSchedule + key: spot + value: "true" + tolerations: + - key: "spot-taint" + operator: "Exists" + effect: "NoSchedule" + diff --git a/cmd/experimental/kueue-viz/examples/01-cluster-queues.yaml b/cmd/experimental/kueue-viz/examples/01-cluster-queues.yaml new file mode 100644 index 0000000000..57d97d2d07 --- /dev/null +++ b/cmd/experimental/kueue-viz/examples/01-cluster-queues.yaml @@ -0,0 +1,78 @@ +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ClusterQueue +metadata: + name: "emergency-cluster-queue" +spec: + cohort: ai-for-humanity-foundation + namespaceSelector: {} # match all. + preemption: + borrowWithinCohort: + policy: LowerPriority + withinClusterQueue: LowerPriority + reclaimWithinCohort: Any # if the pending Workload fits within the nominal quota of its ClusterQueue, preempt any Workload in the cohort, irrespective of priority. + flavorFungibility: + whenCanBorrow: Borrow # this is the default but I'm making it explicit here + whenCanPreempt: Preempt # ensures that accelerators aren't hit with compute workloads + resourceGroups: + - coveredResources: ["cpu", "memory"] + flavors: + - name: "default-flavor" + resources: + - name: "cpu" + nominalQuota: 1 + - name: "memory" + nominalQuota: 1Gi + - coveredResources: [ "gpu"] + flavors: + - name: "gpu" + resources: + - name: "gpu" + nominalQuota: 1Gi +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ClusterQueue +metadata: + name: agi-cluster-queue +spec: + cohort: ai-for-humanity-foundation + namespaceSelector: {} + preemption: + reclaimWithinCohort: Never # do not preempt Workloads in the cohort. + flavorFungibility: + whenCanBorrow: Borrow # this is the default but I'm making it explicit here + whenCanPreempt: Preempt # ensures that accelerators aren't hit with compute workloads + resourceGroups: + - coveredResources: ["cpu", "memory"] + flavors: + - name: "default-flavor" + resources: + - name: "cpu" + nominalQuota: 300m + - name: "memory" + nominalQuota: 300Mi + +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ClusterQueue +metadata: + name: llm-cluster-queue +spec: + cohort: ai-for-humanity-foundation + namespaceSelector: {} + preemption: + reclaimWithinCohort: LowerPriority # only preempt Workloads in the cohort that have lower priority than the pending Workload. + flavorFungibility: + whenCanBorrow: Borrow # this is the default but I'm making it explicit here + whenCanPreempt: Preempt # ensures that accelerators aren't hit with compute workloads + resourceGroups: + - coveredResources: ["cpu", "memory"] + flavors: + - name: "default-flavor" + resources: + - name: "cpu" + nominalQuota: 300m + - name: "memory" + nominalQuota: 300Mi + + diff --git a/cmd/experimental/kueue-viz/examples/02-local-queues.yaml b/cmd/experimental/kueue-viz/examples/02-local-queues.yaml new file mode 100644 index 0000000000..8ed7c05fe0 --- /dev/null +++ b/cmd/experimental/kueue-viz/examples/02-local-queues.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: LocalQueue +metadata: + name: emergency-queue +spec: + clusterQueue: emergency-cluster-queue + +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: LocalQueue +metadata: + name: llm-model-queue +spec: + clusterQueue: llm-cluster-queue + + +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: LocalQueue +metadata: + name: agi-model-queue +spec: + clusterQueue: agi-cluster-queue + + diff --git a/cmd/experimental/kueue-viz/examples/03-agi-job.yaml b/cmd/experimental/kueue-viz/examples/03-agi-job.yaml new file mode 100644 index 0000000000..77827625ce --- /dev/null +++ b/cmd/experimental/kueue-viz/examples/03-agi-job.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + generateName: agi- + labels: + kueue.x-k8s.io/queue-name: agi-model-queue + kueue.x-k8s.io/priority-class: long-term-research +spec: + parallelism: 3 + completions: 3 + suspend: true + template: + spec: + containers: + - name: agi-training-brain + image: gcr.io/google-containers/busybox:latest + command: ['sh', '-c', 'echo "Training AGI ... this may take a while ..." && sleep 600'] + resources: + requests: + cpu: 50m + memory: "100Mi" + restartPolicy: Never + + + diff --git a/cmd/experimental/kueue-viz/examples/04-llm-job.yaml b/cmd/experimental/kueue-viz/examples/04-llm-job.yaml new file mode 100644 index 0000000000..57fe8d268f --- /dev/null +++ b/cmd/experimental/kueue-viz/examples/04-llm-job.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + generateName: llm- + labels: + kueue.x-k8s.io/queue-name: llm-model-queue + kueue.x-k8s.io/priority-class: business-impacting +spec: + parallelism: 3 + completions: 3 + suspend: true + template: + spec: + containers: + - name: llm-training + image: gcr.io/google-containers/busybox:latest + command: ['sh', '-c', 'echo "Training llm..." && sleep 180'] + resources: + requests: + cpu: 10m + memory: "100Mi" + restartPolicy: Never + + + diff --git a/cmd/experimental/kueue-viz/examples/05-cancer-cure-research.yaml b/cmd/experimental/kueue-viz/examples/05-cancer-cure-research.yaml new file mode 100644 index 0000000000..84af16ed00 --- /dev/null +++ b/cmd/experimental/kueue-viz/examples/05-cancer-cure-research.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + generateName: cancer-research- + labels: + kueue.x-k8s.io/queue-name: emergency-queue + kueue.x-k8s.io/priority-class: human-critical +spec: + parallelism: 3 + completions: 3 + suspend: true + template: + spec: + containers: + - name: cancer-cure-research-model-update + image: gcr.io/google-containers/busybox:latest + command: ['sh', '-c', 'echo "Searching cure against cancer..." && sleep 120'] + resources: + requests: + cpu: 10m + memory: "100Mi" + restartPolicy: Never + + + diff --git a/cmd/experimental/kueue-viz/examples/07-workload-priority-classes.yaml b/cmd/experimental/kueue-viz/examples/07-workload-priority-classes.yaml new file mode 100644 index 0000000000..a6ad5497e7 --- /dev/null +++ b/cmd/experimental/kueue-viz/examples/07-workload-priority-classes.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: WorkloadPriorityClass +metadata: + name: human-critical +description: "Use for critical human critical workloads like research on disease or natural disaster avoidance" +#preemptionPolicy: Never # set to prevent pods of this priorityClass from being preempted to make space for other pods +value: 1000000 # 1M out of 1B, higher is better + +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: WorkloadPriorityClass +metadata: + name: business-impacting +description: "Use for business critical impacting workloads" +#preemptionPolicy: Never # set to prevent pods of this priorityClass from being preempted to make space for other pods +value: 1000 # 1M out of 1B, higher is better + + +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: WorkloadPriorityClass +metadata: + name: long-term-research +description: "Use for long term research processes like extraterrestiral research" +#preemptionPolicy: Never # set to prevent pods of this priorityClass from being preempted to make space for other pods +value: 1 # 1M out of 1B, higher is better + + + diff --git a/cmd/experimental/kueue-viz/frontend/.env b/cmd/experimental/kueue-viz/frontend/.env new file mode 100644 index 0000000000..c2f275d406 --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/.env @@ -0,0 +1 @@ +REACT_APP_WEBSOCKET_URL=ws://localhost:8080 diff --git a/cmd/experimental/kueue-viz/frontend/.gitignore b/cmd/experimental/kueue-viz/frontend/.gitignore new file mode 100644 index 0000000000..a8f60abbb1 --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +package-lock.json +build/ +yarn.lock + diff --git a/cmd/experimental/kueue-viz/frontend/CONTRIBUTING.md b/cmd/experimental/kueue-viz/frontend/CONTRIBUTING.md new file mode 100644 index 0000000000..64ec8c244d --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contribution guide + + +## Local development + +Edit `.env` file to match the address of the backend. + +``` +REACT_APP_WEBSOCKET_URL=wss://localhost:8080 +``` + +Download dependencies and build the frontend: +``` +make build +``` + +Start npm in dev mode: +``` +make debug +``` + +This will open your browser and enables hot code replacement on the client side. + diff --git a/cmd/experimental/kueue-viz/frontend/Dockerfile b/cmd/experimental/kueue-viz/frontend/Dockerfile new file mode 100644 index 0000000000..6cd86e583a --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/Dockerfile @@ -0,0 +1,19 @@ +#Build the React app +FROM node:16 AS build + +ENV NPM_CONFIG_CACHE=/tmp/.npm-cache +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . + +# Build the production-ready static files +RUN npm run build +RUN mkdir -p build && touch build/env.js && chmod ugo+rw -R build/ $NPM_CONFIG_CACHE +EXPOSE 8080 + +# Use react-inject-env to be able to inject env vars at runtime +ENTRYPOINT npx react-inject-env set && npx http-server build + + + diff --git a/cmd/experimental/kueue-viz/frontend/Makefile b/cmd/experimental/kueue-viz/frontend/Makefile new file mode 100644 index 0000000000..6ee72cc029 --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/Makefile @@ -0,0 +1,32 @@ +# Makefile for Frontend App + +# Default target: build the app +.PHONY: all +all: install build + +# Build target: runs the production build +.PHONY: build +build: + npm run build + +# Install target: retrieves npm dependencies +.PHONY: install +install: + npm install + +# Run target: serves the built app +.PHONY: run +run: + npx react-inject-env set && npx http-server build + +# Debug target: runs the app in development mode +.PHONY: debug +debug: + npm start + +# Clean target: cleans the build directory +.PHONY: clean +clean: + rm -rf build + + diff --git a/cmd/experimental/kueue-viz/frontend/package.json b/cmd/experimental/kueue-viz/frontend/package.json new file mode 100644 index 0000000000..e58c2c8f20 --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "kueue-viz-frontend", + "version": "1.0.0", + "private": true, + "description": "Frontend dashboard for visualizing Kueue status", + "main": "src/index.js", + "scripts": { + "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", + "build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" + }, + "dependencies": { + "@emotion/react": "^11.0.0", + "@emotion/styled": "^11.0.0", + "@mui/icons-material": "^5.0.0", + "@mui/material": "^5.0.0", + "axios": "^0.27.2", + "chart.js": "^3.5.0", + "react": "^18.0.0", + "react-chartjs-2": "^4.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^6.0.0", + "react-scripts": "^5.0.0", + "react-toastify": "^9.1.1" + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "7.18.6", + "react-inject-env": "^2.1.0" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "author": "Akram Ben Aissi", + "license": "Apache 2.0" +} diff --git a/cmd/experimental/kueue-viz/frontend/public/env.js b/cmd/experimental/kueue-viz/frontend/public/env.js new file mode 100644 index 0000000000..63649a035c --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/public/env.js @@ -0,0 +1,19 @@ +/* +Copyright 2024 The Kubernetes Authors. + +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. +*/ + +export const env = { ...process.env, ...window['env'] } + + diff --git a/cmd/experimental/kueue-viz/frontend/public/favicon.ico b/cmd/experimental/kueue-viz/frontend/public/favicon.ico new file mode 100644 index 0000000000..8791e5b2b2 Binary files /dev/null and b/cmd/experimental/kueue-viz/frontend/public/favicon.ico differ diff --git a/cmd/experimental/kueue-viz/frontend/public/index.html b/cmd/experimental/kueue-viz/frontend/public/index.html new file mode 100644 index 0000000000..54b15a46ed --- /dev/null +++ b/cmd/experimental/kueue-viz/frontend/public/index.html @@ -0,0 +1,20 @@ + + +
+ + + + + + +