Repository: tensorchord/openmodelz Branch: main Commit: b574e2bd838c Files: 413 Total size: 1.1 MB Directory structure: gitextract_h_b9vahq/ ├── .all-contributorsrc ├── .github/ │ └── workflows/ │ ├── CI.yaml │ └── publish.yaml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── agent/ │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── api/ │ │ └── types/ │ │ ├── build.go │ │ ├── error.go │ │ ├── event.go │ │ ├── inference_deployment.go │ │ ├── inference_deployment_instance.go │ │ ├── inference_status.go │ │ ├── info.go │ │ ├── log.go │ │ ├── modelz_cloud.go │ │ ├── namespace.go │ │ ├── queue.go │ │ ├── requests.go │ │ ├── secret.go │ │ └── server.go │ ├── client/ │ │ ├── build.go │ │ ├── client.go │ │ ├── const.go │ │ ├── errors.go │ │ ├── hijack.go │ │ ├── image_cache_create.go │ │ ├── inference_create.go │ │ ├── inference_get.go │ │ ├── inference_list.go │ │ ├── inference_remove.go │ │ ├── inference_scale.go │ │ ├── inference_update.go │ │ ├── info.go │ │ ├── instance_exec.go │ │ ├── instance_list.go │ │ ├── log.go │ │ ├── modelz_cloud.go │ │ ├── namespace_create.go │ │ ├── namespace_delete.go │ │ ├── options.go │ │ ├── request.go │ │ ├── server_label_create.go │ │ ├── server_list.go │ │ ├── server_node_delete.go │ │ ├── transport.go │ │ └── utils.go │ ├── cmd/ │ │ └── agent/ │ │ └── main.go │ ├── errdefs/ │ │ ├── defs.go │ │ ├── doc.go │ │ ├── helpers.go │ │ ├── http_helpers.go │ │ └── is.go │ ├── pkg/ │ │ ├── app/ │ │ │ ├── config.go │ │ │ └── root.go │ │ ├── config/ │ │ │ └── config.go │ │ ├── consts/ │ │ │ └── consts.go │ │ ├── docs/ │ │ │ └── docs.go │ │ ├── event/ │ │ │ ├── event.go │ │ │ ├── fake.go │ │ │ ├── suite_test.go │ │ │ ├── username.go │ │ │ └── util.go │ │ ├── k8s/ │ │ │ ├── convert_inference.go │ │ │ ├── convert_inference_test.go │ │ │ ├── convert_job.go │ │ │ ├── convert_pod.go │ │ │ ├── convert_pod_test.go │ │ │ ├── generate_image_cache.go │ │ │ ├── generate_job.go │ │ │ ├── managed_cluster.go │ │ │ ├── resolver.go │ │ │ └── suite_test.go │ │ ├── log/ │ │ │ ├── factory.go │ │ │ ├── k8s.go │ │ │ └── loki.go │ │ ├── metrics/ │ │ │ ├── exporter.go │ │ │ └── metrics.go │ │ ├── prom/ │ │ │ └── prometheus_query.go │ │ ├── runtime/ │ │ │ ├── build.go │ │ │ ├── cluster_info_get.go │ │ │ ├── image_cache.go │ │ │ ├── inference_create.go │ │ │ ├── inference_delete.go │ │ │ ├── inference_exec.go │ │ │ ├── inference_get.go │ │ │ ├── inference_instance.go │ │ │ ├── inference_list.go │ │ │ ├── inference_replicas.go │ │ │ ├── inference_update.go │ │ │ ├── mock/ │ │ │ │ └── mock.go │ │ │ ├── namespace.go │ │ │ ├── node.go │ │ │ ├── runtime.go │ │ │ ├── server_delete.go │ │ │ ├── server_label_create.go │ │ │ ├── server_list.go │ │ │ ├── util_domain.go │ │ │ └── util_resource.go │ │ ├── scaling/ │ │ │ ├── function_scaler.go │ │ │ ├── ranges.go │ │ │ ├── retry.go │ │ │ ├── service_query.go │ │ │ └── util.go │ │ ├── server/ │ │ │ ├── error.go │ │ │ ├── handler_build_create.go │ │ │ ├── handler_build_get.go │ │ │ ├── handler_build_list.go │ │ │ ├── handler_build_logs.go │ │ │ ├── handler_gradio_proxy.go │ │ │ ├── handler_healthz.go │ │ │ ├── handler_healthz_test.go │ │ │ ├── handler_image_cache.go │ │ │ ├── handler_inference_create.go │ │ │ ├── handler_inference_create_test.go │ │ │ ├── handler_inference_delete.go │ │ │ ├── handler_inference_delete_test.go │ │ │ ├── handler_inference_get.go │ │ │ ├── handler_inference_get_test.go │ │ │ ├── handler_inference_instance.go │ │ │ ├── handler_inference_instance_exec.go │ │ │ ├── handler_inference_list.go │ │ │ ├── handler_inference_logs.go │ │ │ ├── handler_inference_proxy.go │ │ │ ├── handler_inference_scale.go │ │ │ ├── handler_inference_update.go │ │ │ ├── handler_info.go │ │ │ ├── handler_mosec_proxy.go │ │ │ ├── handler_namespace_create.go │ │ │ ├── handler_namespace_delete.go │ │ │ ├── handler_namespace_delete_test.go │ │ │ ├── handler_namespace_list.go │ │ │ ├── handler_other_proxy.go │ │ │ ├── handler_root.go │ │ │ ├── handler_server_delete.go │ │ │ ├── handler_server_label_create.go │ │ │ ├── handler_server_list.go │ │ │ ├── handler_streamlit_proxy.go │ │ │ ├── middleware_callid.go │ │ │ ├── proxy_auth.go │ │ │ ├── server_factory.go │ │ │ ├── server_handlerfunc.go │ │ │ ├── server_init_kubernetes.go │ │ │ ├── server_init_logs.go │ │ │ ├── server_init_metrics.go │ │ │ ├── server_init_modelz_cloud.go │ │ │ ├── server_init_route.go │ │ │ ├── server_run.go │ │ │ ├── server_websocket.go │ │ │ ├── static/ │ │ │ │ ├── index.html │ │ │ │ ├── landing.go │ │ │ │ └── page_loading.go │ │ │ ├── suite_test.go │ │ │ ├── user.go │ │ │ └── validator/ │ │ │ └── validator.go │ │ └── version/ │ │ └── version.go │ └── sqlc.yaml ├── autoscaler/ │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ ├── cmd/ │ │ └── autoscaler/ │ │ └── main.go │ └── pkg/ │ ├── autoscaler/ │ │ ├── factory.go │ │ ├── inferencecache.go │ │ ├── loadcache.go │ │ └── scaler.go │ ├── autoscalerapp/ │ │ └── root.go │ ├── prom/ │ │ ├── prom.go │ │ └── types.go │ ├── server/ │ │ └── status.go │ └── version/ │ └── version.go ├── go.mod ├── go.sum ├── ingress-operator/ │ ├── .DEREK.yml │ ├── .dockerignore │ ├── .gitignore │ ├── .tools/ │ │ ├── README.md │ │ ├── code-generator.mod │ │ └── code-generator.sum │ ├── .vscode/ │ │ └── settings.json │ ├── Dockerfile │ ├── LICENSE │ ├── Makefile │ ├── artifacts/ │ │ ├── .gitignore │ │ ├── crds/ │ │ │ └── tensorchord.ai_inferenceingresses.yaml │ │ ├── operator-amd64.yaml │ │ └── operator-rbac.yaml │ ├── cmd/ │ │ └── ingress-operator/ │ │ └── main.go │ ├── hack/ │ │ ├── boilerplate.go.txt │ │ ├── custom-boilerplate.go.txt │ │ ├── print-codegen-version.sh │ │ ├── update-codegen.sh │ │ ├── update-crds.sh │ │ └── verify-codegen.sh │ ├── pkg/ │ │ ├── apis/ │ │ │ └── modelzetes/ │ │ │ ├── register.go │ │ │ └── v1/ │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ ├── types.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── app/ │ │ │ ├── config.go │ │ │ └── root.go │ │ ├── client/ │ │ │ ├── clientset/ │ │ │ │ └── versioned/ │ │ │ │ ├── clientset.go │ │ │ │ ├── doc.go │ │ │ │ ├── fake/ │ │ │ │ │ ├── clientset_generated.go │ │ │ │ │ ├── doc.go │ │ │ │ │ └── register.go │ │ │ │ ├── scheme/ │ │ │ │ │ ├── doc.go │ │ │ │ │ └── register.go │ │ │ │ └── typed/ │ │ │ │ └── modelzetes/ │ │ │ │ └── v1/ │ │ │ │ ├── doc.go │ │ │ │ ├── fake/ │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── fake_inferenceingress.go │ │ │ │ │ └── fake_modelzetes_client.go │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── inferenceingress.go │ │ │ │ └── modelzetes_client.go │ │ │ ├── informers/ │ │ │ │ └── externalversions/ │ │ │ │ ├── factory.go │ │ │ │ ├── generic.go │ │ │ │ ├── internalinterfaces/ │ │ │ │ │ └── factory_interfaces.go │ │ │ │ └── modelzetes/ │ │ │ │ ├── interface.go │ │ │ │ └── v1/ │ │ │ │ ├── inferenceingress.go │ │ │ │ └── interface.go │ │ │ └── listers/ │ │ │ └── modelzetes/ │ │ │ └── v1/ │ │ │ ├── expansion_generated.go │ │ │ └── inferenceingress.go │ │ ├── config/ │ │ │ └── config.go │ │ ├── consts/ │ │ │ └── consts.go │ │ ├── controller/ │ │ │ ├── core.go │ │ │ ├── core_test.go │ │ │ └── v1/ │ │ │ ├── controller.go │ │ │ ├── controller_factory.go │ │ │ ├── controller_test.go │ │ │ └── docs.go │ │ ├── signals/ │ │ │ ├── signal.go │ │ │ ├── signal_posix.go │ │ │ └── signal_windows.go │ │ └── version/ │ │ └── version.go │ └── vendor.go ├── mdz/ │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── cmd/ │ │ └── mdz/ │ │ └── main.go │ ├── docs/ │ │ ├── cli/ │ │ │ ├── mdz.md │ │ │ ├── mdz_delete.md │ │ │ ├── mdz_deploy.md │ │ │ ├── mdz_exec.md │ │ │ ├── mdz_list.md │ │ │ ├── mdz_list_instance.md │ │ │ ├── mdz_logs.md │ │ │ ├── mdz_port-forward.md │ │ │ ├── mdz_scale.md │ │ │ ├── mdz_server.md │ │ │ ├── mdz_server_delete.md │ │ │ ├── mdz_server_destroy.md │ │ │ ├── mdz_server_join.md │ │ │ ├── mdz_server_label.md │ │ │ ├── mdz_server_list.md │ │ │ ├── mdz_server_start.md │ │ │ ├── mdz_server_stop.md │ │ │ └── mdz_version.md │ │ └── macOS-quickstart.md │ ├── examples/ │ │ └── bloomz-560m-openai/ │ │ └── README.md │ ├── hack/ │ │ └── cli-doc-gen/ │ │ └── main.go │ └── pkg/ │ ├── agentd/ │ │ ├── runtime/ │ │ │ ├── create.go │ │ │ ├── delete.go │ │ │ ├── label.go │ │ │ ├── list.go │ │ │ ├── proxy.go │ │ │ └── runtime.go │ │ └── server/ │ │ ├── error.go │ │ ├── handler_healthz.go │ │ ├── handler_inference_create.go │ │ ├── handler_inference_delete.go │ │ ├── handler_inference_get.go │ │ ├── handler_inference_list.go │ │ ├── handler_inference_logs.go │ │ ├── handler_inference_proxy.go │ │ ├── handler_info.go │ │ ├── middleware_callid.go │ │ ├── server_factory.go │ │ ├── server_handlerfunc.go │ │ ├── server_init_route.go │ │ └── server_run.go │ ├── cmd/ │ │ ├── delete.go │ │ ├── deploy.go │ │ ├── exec.go │ │ ├── exec_stream.go │ │ ├── ioutils/ │ │ │ └── reader.go │ │ ├── list.go │ │ ├── list_instance.go │ │ ├── localagent.go │ │ ├── logs.go │ │ ├── portforward.go │ │ ├── root.go │ │ ├── scale.go │ │ ├── server.go │ │ ├── server_delete.go │ │ ├── server_destroy.go │ │ ├── server_join.go │ │ ├── server_label.go │ │ ├── server_list.go │ │ ├── server_start.go │ │ ├── server_stop.go │ │ ├── streams/ │ │ │ ├── in.go │ │ │ ├── out.go │ │ │ └── stream.go │ │ └── version.go │ ├── server/ │ │ ├── agentd_run.go │ │ ├── engine.go │ │ ├── gpu-resource.yaml │ │ ├── gpu_install.go │ │ ├── k3s-install.sh │ │ ├── k3s_destroy.go │ │ ├── k3s_install.go │ │ ├── k3s_join.go │ │ ├── k3s_killall.go │ │ ├── k3s_prepare.go │ │ ├── nginx-dep.yaml │ │ ├── nginx_install.go │ │ ├── openmodelz.yaml │ │ ├── openmodelz_install.go │ │ └── registries.yaml │ ├── telemetry/ │ │ └── telemetry.go │ ├── term/ │ │ ├── interrupt.go │ │ └── term.go │ └── version/ │ └── version.go ├── modelzetes/ │ ├── .dockerignore │ ├── .gitattributes │ ├── .gitignore │ ├── Dockerfile │ ├── LICENSE │ ├── Makefile │ ├── artifacts/ │ │ ├── crds/ │ │ │ └── tensorchord.ai_inferences.yaml │ │ └── samples/ │ │ └── v2alpha1.yaml │ ├── buf.yaml │ ├── cmd/ │ │ └── modelzetes/ │ │ └── main.go │ ├── hack/ │ │ ├── boilerplate.go.txt │ │ ├── print-codegen-version.sh │ │ ├── update-codegen.sh │ │ ├── update-crds.sh │ │ └── verify-codegen.sh │ ├── pkg/ │ │ ├── apis/ │ │ │ └── modelzetes/ │ │ │ ├── register.go │ │ │ └── v2alpha1/ │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ ├── types.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── app/ │ │ │ ├── config.go │ │ │ └── root.go │ │ ├── client/ │ │ │ ├── clientset/ │ │ │ │ └── versioned/ │ │ │ │ ├── clientset.go │ │ │ │ ├── doc.go │ │ │ │ ├── fake/ │ │ │ │ │ ├── clientset_generated.go │ │ │ │ │ ├── doc.go │ │ │ │ │ └── register.go │ │ │ │ ├── scheme/ │ │ │ │ │ ├── doc.go │ │ │ │ │ └── register.go │ │ │ │ └── typed/ │ │ │ │ └── modelzetes/ │ │ │ │ └── v2alpha1/ │ │ │ │ ├── doc.go │ │ │ │ ├── fake/ │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── fake_inference.go │ │ │ │ │ └── fake_modelzetes_client.go │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── inference.go │ │ │ │ └── modelzetes_client.go │ │ │ ├── informers/ │ │ │ │ └── externalversions/ │ │ │ │ ├── factory.go │ │ │ │ ├── generic.go │ │ │ │ ├── internalinterfaces/ │ │ │ │ │ └── factory_interfaces.go │ │ │ │ └── modelzetes/ │ │ │ │ ├── interface.go │ │ │ │ └── v2alpha1/ │ │ │ │ ├── inference.go │ │ │ │ └── interface.go │ │ │ └── listers/ │ │ │ └── modelzetes/ │ │ │ └── v2alpha1/ │ │ │ ├── expansion_generated.go │ │ │ └── inference.go │ │ ├── config/ │ │ │ └── config.go │ │ ├── consts/ │ │ │ └── consts.go │ │ ├── controller/ │ │ │ ├── annotations_test.go │ │ │ ├── controller.go │ │ │ ├── deployment.go │ │ │ ├── deployment_test.go │ │ │ ├── deployment_update_test.go │ │ │ ├── factory.go │ │ │ ├── framework_test.go │ │ │ ├── fromconfig.go │ │ │ ├── replicas_test.go │ │ │ ├── secrets.go │ │ │ ├── secrets_test.go │ │ │ ├── service.go │ │ │ └── service_test.go │ │ ├── k8s/ │ │ │ ├── config.go │ │ │ ├── errors.go │ │ │ ├── factory.go │ │ │ ├── factory_test.go │ │ │ ├── instance.go │ │ │ ├── instance_test.go │ │ │ ├── log.go │ │ │ ├── logs.go │ │ │ ├── probes.go │ │ │ ├── probes_test.go │ │ │ ├── proxy.go │ │ │ ├── proxy_test.go │ │ │ ├── secrets.go │ │ │ ├── secrets_factory_test.go │ │ │ ├── securityContext.go │ │ │ ├── securityContext_test.go │ │ │ └── utils.go │ │ ├── pointer/ │ │ │ └── ptr.go │ │ ├── signals/ │ │ │ ├── signal.go │ │ │ ├── signal_posix.go │ │ │ └── signal_windows.go │ │ └── version/ │ │ └── version.go │ └── vendor.go ├── pyproject.toml ├── setup.py └── typos.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "projectName": "openmodelz", "projectOwner": "tensorchord", "repoType": "github", "repoHost": "https://github.com", "files": [ "README.md" ], "imageSize": 70, "commit": true, "commitConvention": "angular", "contributorsSortAlphabetically": true, "contributors": [ { "login": "gaocegege", "name": "Ce Gao", "avatar_url": "https://avatars.githubusercontent.com/u/5100735?v=4", "profile": "https://github.com/gaocegege", "contributions": [ "code", "review", "tutorial" ] }, { "login": "tddschn", "name": "Teddy Xinyuan Chen", "avatar_url": "https://avatars.githubusercontent.com/u/45612704?v=4", "profile": "https://github.com/tddschn", "contributions": [ "doc" ] }, { "login": "VoVAllen", "name": "Jinjing Zhou", "avatar_url": "https://avatars.githubusercontent.com/u/8686776?v=4", "profile": "https://github.com/VoVAllen", "contributions": [ "question", "bug", "ideas" ] }, { "login": "kemingy", "name": "Keming", "avatar_url": "https://avatars.githubusercontent.com/u/12974685?v=4", "profile": "https://blog.mapotofu.org/", "contributions": [ "code", "design", "infra" ] }, { "login": "cutecutecat", "name": "cutecutecat", "avatar_url": "https://avatars.githubusercontent.com/u/19801166?v=4", "profile": "https://github.com/cutecutecat", "contributions": [ "ideas" ] }, { "login": "xieydd", "name": "xieydd", "avatar_url": "https://avatars.githubusercontent.com/u/20329697?v=4", "profile": "https://xieydd.github.io/", "contributions": [ "ideas" ] }, { "login": "Xuanwo", "name": "Xuanwo", "avatar_url": "https://avatars.githubusercontent.com/u/5351546?v=4", "profile": "https://xuanwo.io/", "contributions": [ "content", "design", "ideas" ] }, { "login": "Zheaoli", "name": "Nadeshiko Manju", "avatar_url": "https://avatars.githubusercontent.com/u/7054676?v=4", "profile": "http://manjusaka.itscoder.com/", "contributions": [ "bug", "design", "ideas" ] }, { "login": "zwpaper", "name": "Wei Zhang", "avatar_url": "https://avatars.githubusercontent.com/u/3764335?v=4", "profile": "https://page.codespaper.com", "contributions": [ "code" ] } ], "contributorsPerLine": 7, "commitType": "docs" } ================================================ FILE: .github/workflows/CI.yaml ================================================ name: CI on: push: branches: - main paths: - '.github/workflows/**' - '**.go' - '**/Makefile' - 'go.**' pull_request: paths: - '.github/workflows/**' - '**.go' - '**/Makefile' - 'go.**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: typos-check: name: Spell Check with Typos runs-on: ubuntu-latest steps: - name: Checkout Actions Repository uses: actions/checkout@v3 - name: Check spelling with custom config file uses: crate-ci/typos@v1.16.2 with: config: ./typos.toml test: name: test strategy: matrix: os: [ubuntu-latest] dir: ["agent", "autoscaler", "ingress-operator", "mdz", "modelzetes"] runs-on: ${{ matrix.os }} steps: - name: Check out code uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v4 with: go-version: 1.19 - uses: actions/cache@v3 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: test run: | cd ${{ matrix.dir }} make fmt git diff --exit-code || (echo 'Please run "make fmt" to format code' && exit 1); make go test -race -coverprofile=${{ matrix.dir }}.out -covermode=atomic ./... - name: Upload coverage report uses: actions/upload-artifact@v3 with: name: ${{ matrix.dir }}-out path: ${{ matrix.dir }}/${{ matrix.dir }}.out report: needs: - test - typos-check runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v4 with: go-version: 1.19 - name: Install bins run: | go install github.com/mattn/goveralls@latest - name: Get agent coverage report uses: actions/download-artifact@v3 with: name: agent-out path: merge - name: Get autoscaler coverage report uses: actions/download-artifact@v3 with: name: autoscaler-out path: merge - name: Get ingress-operator coverage report uses: actions/download-artifact@v3 with: name: ingress-operator-out path: merge - name: Get mdz coverage report uses: actions/download-artifact@v3 with: name: mdz-out path: merge - name: Get modelzetes coverage report uses: actions/download-artifact@v3 with: name: modelzetes-out path: merge - name: Merge all coverage reports uses: cutecutecat/go-cover-merge@v1 with: input_dir: merge output_file: final.out - name: Send coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | goveralls -coverprofile=final.out -service=github ================================================ FILE: .github/workflows/publish.yaml ================================================ name: release on: release: types: [published] pull_request: paths: - '.github/workflows/release.yml' - '.goreleaser/' - '.goreleaser.yaml' jobs: goreleaser: if: github.repository == 'tensorchord/openmodelz' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.19 - name: Docker Login uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERIO_USERNAME }} password: ${{ secrets.DOCKERIO_TOKEN }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: distribution: goreleaser version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: upload gobin uses: actions/upload-artifact@v3 with: name: gobin_${{ github.event.release.tag_name }} retention-days: 1 path: | dist/mdz_linux_amd64_v1/mdz if-no-files-found: error pypi_publish: needs: goreleaser # only trigger on main repo when tag starts with v if: github.repository == 'tensorchord/openmodelz' && startsWith(github.ref, 'refs/tags/v') runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: matrix: os: [ubuntu-20.04] steps: - uses: actions/checkout@v3 - name: Get gobin uses: actions/download-artifact@v3 with: name: gobin_${{ github.event.release.tag_name }} path: dist/ - name: Configure linux build environment if: runner.os == 'Linux' run: | mkdir -p mdz/bin mv dist/mdz mdz/bin/mdz chmod +x mdz/bin/mdz - name: Build wheels uses: pypa/cibuildwheel@v2.14.1 - name: Build source distribution if: runner.os == 'Linux' # Only release source under linux to avoid conflict run: | python -m pip install wheel setuptools_scm python setup.py sdist mv dist/*.tar.gz wheelhouse/ - name: Upload to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python -m pip install --upgrade pip python -m pip install twine python -m twine upload wheelhouse/* ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST _version.txt _version.py wheelhouse/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache .ruff_cache/ nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work ================================================ FILE: .goreleaser.yaml ================================================ project_name: openmodelz builds: - env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 id: modelzetes main: ./cmd/modelzetes/main.go dir: ./modelzetes binary: modelzetes ldflags: - -s -w - -X github.com/tensorchord/openmodelz/modelzetes/pkg/version.version={{ .Version }} - -X github.com/tensorchord/openmodelz/modelzetes/pkg/version.buildDate={{ .Date }} - -X github.com/tensorchord/openmodelz/modelzetes/pkg/version.gitCommit={{ .Commit }} - -X github.com/tensorchord/openmodelz/modelzetes/pkg/version.gitTreeState=clean - -X github.com/tensorchord/openmodelz/modelzetes/pkg/version.gitTag={{ .Tag }} - env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 id: agent main: ./cmd/agent/main.go dir: ./agent binary: agent ldflags: - -s -w - -X github.com/tensorchord/openmodelz/agent/pkg/version.version={{ .Version }} - -X github.com/tensorchord/openmodelz/agent/pkg/version.buildDate={{ .Date }} - -X github.com/tensorchord/openmodelz/agent/pkg/version.gitCommit={{ .Commit }} - -X github.com/tensorchord/openmodelz/agent/pkg/version.gitTreeState=clean - -X github.com/tensorchord/openmodelz/agent/pkg/version.gitTag={{ .Tag }} - env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 id: mdz main: ./cmd/mdz/main.go dir: ./mdz binary: mdz ldflags: - -s -w - -X github.com/tensorchord/openmodelz/mdz/pkg/version.version={{ .Version }} - -X github.com/tensorchord/openmodelz/mdz/pkg/version.buildDate={{ .Date }} - -X github.com/tensorchord/openmodelz/mdz/pkg/version.gitCommit={{ .Commit }} - -X github.com/tensorchord/openmodelz/mdz/pkg/version.gitTreeState=clean - -X github.com/tensorchord/openmodelz/mdz/pkg/version.gitTag={{ .Tag }} - env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 id: autoscaler main: ./cmd/autoscaler/main.go dir: ./autoscaler binary: autoscaler ldflags: - -s -w - -X github.com/tensorchord/openmodelz/autoscaler/pkg/version.version={{ .Version }} - -X github.com/tensorchord/openmodelz/autoscaler/pkg/version.buildDate={{ .Date }} - -X github.com/tensorchord/openmodelz/autoscaler/pkg/version.gitCommit={{ .Commit }} - -X github.com/tensorchord/openmodelz/autoscaler/pkg/version.gitTreeState=clean - -X github.com/tensorchord/openmodelz/autoscaler/pkg/version.gitTag={{ .Tag }} - env: - CGO_ENABLED=0 goos: - linux goarch: - amd64 id: ingress-operator main: ./cmd/ingress-operator/main.go dir: ./ingress-operator binary: ingress-operator ldflags: - -s -w - -X github.com/tensorchord/openmodelz/ingress-operator/pkg/version.version={{ .Version }} - -X github.com/tensorchord/openmodelz/ingress-operator/pkg/version.buildDate={{ .Date }} - -X github.com/tensorchord/openmodelz/ingress-operator/pkg/version.gitCommit={{ .Commit }} - -X github.com/tensorchord/openmodelz/ingress-operator/pkg/version.gitTreeState=clean - -X github.com/tensorchord/openmodelz/ingress-operator/pkg/version.gitTag={{ .Tag }} archives: - id: mdz format: binary builds: - mdz name_template: >- {{ .Binary }}_{{ .Version }}_{{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ incpatch .Version }}-next" changelog: use: github sort: asc groups: - title: 'Exciting New Features 🎉' regexp: "^.*feat.*" order: 0 - title: 'Bug Fix 🛠' regexp: "^.*(Fix|fix|bug).*" order: 1 - title: 'Refactor 🏗️' regexp: "^.*refact.*" order: 2 - title: 'Documentation 🖊️' regexp: "^.*docs.*" order: 3 - title: 'Others:' order: 999 dockers: - image_templates: - "modelzai/openmodelz-modelzetes:v{{ .Version }}-amd64" use: buildx dockerfile: modelzetes/Dockerfile ids: - modelzetes build_flag_templates: - "--platform=linux/amd64" # - image_templates: # - "modelzai/modelzetes:v{{ .Version }}-arm64v8" # use: buildx # goarch: arm64 # ids: # - modelzetes # dockerfile: modelzetes/Dockerfile # build_flag_templates: # - "--platform=linux/arm64/v8" - image_templates: - "modelzai/openmodelz-agent:v{{ .Version }}-amd64" use: buildx dockerfile: agent/Dockerfile ids: - agent build_flag_templates: - "--platform=linux/amd64" # - image_templates: # - "modelzai/modelz-agent:v{{ .Version }}-arm64v8" # use: buildx # goarch: arm64 # ids: # - agent # dockerfile: agent/Dockerfile # build_flag_templates: # - "--platform=linux/arm64/v8" - image_templates: - "modelzai/openmodelz-autoscaler:v{{ .Version }}-amd64" use: buildx dockerfile: autoscaler/Dockerfile ids: - autoscaler build_flag_templates: - "--platform=linux/amd64" # - image_templates: # - "modelzai/modelz-autoscaler:v{{ .Version }}-arm64v8" # use: buildx # goarch: arm64 # ids: # - autoscaler # dockerfile: autoscaler/Dockerfile # build_flag_templates: # - "--platform=linux/arm64/v8" - image_templates: - "modelzai/openmodelz-ingress-operator:v{{ .Version }}-amd64" use: buildx dockerfile: ingress-operator/Dockerfile ids: - ingress-operator build_flag_templates: - "--platform=linux/amd64" docker_manifests: - name_template: modelzai/openmodelz-modelzetes:v{{ .Version }} image_templates: - modelzai/openmodelz-modelzetes:v{{ .Version }}-amd64 # - modelzai/modelzetes:v{{ .Version }}-arm64v8 - name_template: modelzai/openmodelz-agent:v{{ .Version }} image_templates: - modelzai/openmodelz-agent:v{{ .Version }}-amd64 # - modelzai/modelz-agent:v{{ .Version }}-arm64v8 - name_template: modelzai/openmodelz-autoscaler:v{{ .Version }} image_templates: - modelzai/openmodelz-autoscaler:v{{ .Version }}-amd64 # - modelzai/modelz-autoscaler:v{{ .Version }}-arm64v8 - name_template: modelzai/openmodelz-ingress-operator:v{{ .Version }} image_templates: - modelzai/openmodelz-ingress-operator:v{{ .Version }}-amd64 # - modelzai/ingress-operator:v{{ .Version }}-arm64v8 ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MANIFEST.in ================================================ prune autoscaler prune ingress-operator prune modelzetes prune .github include LICENSE include README.md include .goreleaser.yaml include mdz/Makefile mdz/go.mod mdz/go.sum mdz/LICENSE graft mdz/pkg graft mdz/cmd prune mdz/bin prune mdz/docs prune mdz/examples graft agent/pkg prune agent/bin ================================================ FILE: README.md ================================================
# OpenModelZ

discord invitation link trackgit-views docs all-contributors CI PyPI version Coverage Status

## What is OpenModelZ? OpenModelZ ( `mdz` ) is tool to deploy your models to any cluster (GCP, AWS, Lambda labs, your home lab, or even a single machine). Getting models into production is hard for data scientists and SREs. You need to configure the monitoring, logging, and scaling infrastructure, with the right security and permissions. And then setup the domain, SSL, and load balancer. This can take weeks or months of work even for a single model deployment. You can now use mdz deploy to effortlessly deploy your models. OpenModelZ handles all the infrastructure setup for you. Each deployment gets a public subdomain, like `http://jupyter-9pnxd.2.242.22.143.modelz.live`, making it easily accessible.

OpenModelZ

## Benefits OpenModelZ provides the following features out-of-the-box: - 📈 **Auto-scaling from 0**: The number of inference servers could be scaled based on the workload. You could start from 0 and scale it up to 10+ replicas easily. - 📦 **Support any machine learning framework**: You could deploy any machine learning framework (e.g. [vLLM](https://github.com/vllm-project/vllm)/[triton-inference-server](https://github.com/triton-inference-server/server)/[mosec](https://github.com/mosecorg/mosec) etc.) with a single command. Besides, you could also deploy your own custom inference server. - 🔬 **Gradio/Streamlit/Jupyter support**: We provide a robust prototyping environment with support for [Gradio](https://gradio.app), [Streamlit](https://streamlit.io/), [jupyter](https://jupyter.org/) and so on. You could visualize your model's performance and debug it easily in the notebook, or deploy a web app for your model with a single command. - 🏃 **Start from a single machine to a cluster of machines**: You could start from a single machine and scale it up to a cluster of machines without any hassle, with a single command `mdz server start`. - 🚀 **Public accessible subdomain for each deployment** ( optional ) : We provision a separate subdomain for each deployment without any extra cost and effort, making each deployment easily accessible from the outside. OpenModelZ is the foundational component of the ModelZ platform available at [modelz.ai](https://modelz.ai). ## How it works Get a server (could be a cloud VM, a home lab, or even a single machine) and run the `mdz server start` command. OpenModelZ will bootstrap the server for you. ```text $ mdz server start 🚧 Creating the server... 🚧 Initializing the load balancer... 🚧 Initializing the GPU resource... 🚧 Initializing the server... 🚧 Waiting for the server to be ready... 🐋 Checking if the server is running... 🐳 The server is running at http://146.235.213.84.modelz.live 🎉 You could set the environment variable to get started! export MDZ_URL=http://146.235.213.84.modelz.live $ export MDZ_URL=http://146.235.213.84.modelz.live ``` Then you could deploy your model with a single command `mdz deploy` and get the endpoint: ``` $ mdz deploy --image modelzai/gradio-stable-diffusion:23.03 --name sdw --port 7860 --gpu 1 Inference sd is created $ mdz list NAME ENDPOINT STATUS INVOCATIONS REPLICAS sdw http://sdw-qh2n0y28ybqc36oc.146.235.213.84.modelz.live Ready 174 1/1 http://146.235.213.84.modelz.live/inference/sdw.default ``` ## Quick Start 🚀 ### Install `mdz` You can install OpenModelZ using the following command: ```text copy pip install openmodelz ``` You could verify the installation by running the following command: ```text copy mdz ``` Once you've installed the `mdz` you can start deploying models and experimenting with them. ### Bootstrap `mdz` It's super easy to bootstrap the `mdz` server. You just need to find a server (could be a cloud VM, a home lab, or even a single machine) and run the `mdz server start` command. > Notice: We may require the root permission to bootstrap the `mdz` server on port 80. ``` $ mdz server start 🚧 Creating the server... 🚧 Initializing the load balancer... 🚧 Initializing the GPU resource... 🚧 Initializing the server... 🚧 Waiting for the server to be ready... 🐋 Checking if the server is running... Agent: Version: v0.0.13 Build Date: 2023-07-19T09:12:55Z Git Commit: 84d0171640453e9272f78a63e621392e93ef6bbb Git State: clean Go Version: go1.19.10 Compiler: gc Platform: linux/amd64 🐳 The server is running at http://192.168.71.93.modelz.live 🎉 You could set the environment variable to get started! export MDZ_URL=http://192.168.71.93.modelz.live ``` The internal IP address will be used as the default endpoint of your deployments. You could provide the public IP address of your server to the `mdz server start` command to make it accessible from the outside world. ```bash # Provide the public IP as an argument $ mdz server start 1.2.3.4 ``` You could also specify the registry mirror to speed up the image pulling process. Here is an example: ```bash /--mirror-endpoints/ $ mdz server start --mirror-endpoints https://docker.mirrors.sjtug.sjtu.edu.cn ``` ### Create your first UI-based deployment Once you've bootstrapped the `mdz` server, you can start deploying your first applications. We will use jupyter notebook as an example in this tutorial. You could use any docker image as your deployment. ```text $ mdz deploy --image jupyter/minimal-notebook:lab-4.0.3 --name jupyter --port 8888 --command "jupyter notebook --ip='*' --NotebookApp.token='' --NotebookApp.password=''" Inference jupyter is created $ mdz list NAME ENDPOINT STATUS INVOCATIONS REPLICAS jupyter http://jupyter-9pnxdkeb6jsfqkmq.192.168.71.93.modelz.live Ready 488 1/1 http://192.168.71.93/inference/jupyter.default ``` You could access the deployment by visiting the endpoint URL. The endpoint will be automatically generated for each deployment with the following format: `-..modelz.live`. It is `http://jupyter-9pnxdkeb6jsfqkmq.192.168.71.93.modelz.live` in this case. The endpoint could be accessed from the outside world as well if you've provided the public IP address of your server to the `mdz server start` command. ![jupyter notebook](./images/jupyter.png) ### Create your first OpenAI compatible API server You could also create API-based deployments. We will use [OpenAI compatible API server with Bloomz 560M](https://github.com/tensorchord/modelz-llm#run-the-self-hosted-api-server) as an example in this tutorial. ```text $ mdz deploy --image modelzai/llm-bloomz-560m:23.07.4 --name simple-server Inference simple-server is created $ mdz list NAME ENDPOINT STATUS INVOCATIONS REPLICAS jupyter http://jupyter-9pnxdkeb6jsfqkmq.192.168.71.93.modelz.live Ready 488 1/1 http://192.168.71.93/inference/jupyter.default simple-server http://simple-server-lagn8m9m8648q6kx.192.168.71.93.modelz.live Ready 0 1/1 http://192.168.71.93/inference/simple-server.default ``` You could use OpenAI python package and the endpoint `http://simple-server-lagn8m9m8648q6kx.192.168.71.93.modelz.live` in this case, to interact with the deployment. ```python import openai openai.api_base="http://simple-server-lagn8m9m8648q6kx.192.168.71.93.modelz.live" openai.api_key="any" # create a chat completion chat_completion = openai.ChatCompletion.create(model="bloomz", messages=[ {"role": "user", "content": "Who are you?"}, {"role": "assistant", "content": "I am a student"}, {"role": "user", "content": "What do you learn?"}, ], max_tokens=100) ``` ### Scale your deployment You could scale your deployment by using the `mdz scale` command. ```text /scale/ $ mdz scale simple-server --replicas 3 ``` The requests will be load balanced between the replicas of your deployment. You could also tell the `mdz` to **autoscale your deployment** based on the inflight requests. Please check out the [Autoscaling](https://docs.open.modelz.ai/deployment/autoscale) documentation for more details. ### Debug your deployment Sometimes you may want to debug your deployment. You could use the `mdz logs` command to get the logs of your deployment. ```text /logs/ $ mdz logs simple-server simple-server-6756dd67ff-4bf4g: 10.42.0.1 - - [27/Jul/2023 02:32:16] "GET / HTTP/1.1" 200 - simple-server-6756dd67ff-4bf4g: 10.42.0.1 - - [27/Jul/2023 02:32:16] "GET / HTTP/1.1" 200 - simple-server-6756dd67ff-4bf4g: 10.42.0.1 - - [27/Jul/2023 02:32:17] "GET / HTTP/1.1" 200 - ``` You could also use the `mdz exec` command to execute a command in the container of your deployment. You do not need to ssh into the server to do that. ```text /exec/ $ mdz exec simple-server ps PID USER TIME COMMAND 1 root 0:00 /usr/bin/dumb-init /bin/sh -c python3 -m http.server 80 7 root 0:00 /bin/sh -c python3 -m http.server 80 8 root 0:00 python3 -m http.server 80 9 root 0:00 ps ``` ```text /exec/ $ mdz exec simple-server -ti bash bash-4.4# ``` Or you could port-forward the deployment to your local machine and debug it locally. ```text /port-forward/ $ mdz port-forward simple-server 7860 Forwarding inference simple-server to local port 7860 ``` ### Add more servers You could add more servers to your cluster by using the `mdz server join` command. The `mdz` server will be bootstrapped on the server and join the cluster automatically. ```text /join/ $ mdz server join $ mdz server list NAME PHASE ALLOCATABLE CAPACITY node1 Ready cpu: 16 cpu: 16 mem: 32784748Ki mem: 32784748Ki gpu: 1 gpu: 1 node2 Ready cpu: 16 cpu: 16 mem: 32784748Ki mem: 32784748Ki gpu: 1 gpu: 1 ``` ### Label your servers You could label your servers to deploy your models to specific servers. For example, you could label your servers with `gpu=true` and deploy your models to servers with GPUs. ```text /--node-labels gpu=true,type=nvidia-a100/ $ mdz server label node3 gpu=true type=nvidia-a100 $ mdz deploy ... --node-labels gpu=true,type=nvidia-a100 ``` ## Architecture OpenModelZ is inspired by the [k3s](https://github.com/k3s-io/k3s) and [OpenFaaS](https://github.com/openfaas), but designed specifically for machine learning deployment. We keep the core of the system **simple, and easy to extend**. You do not need to read this section if you just want to deploy your models. But if you want to understand how OpenModelZ works, this section is for you.

OpenModelZ

OpenModelZ is composed of two components: - Data Plane: The data plane is responsible for the servers. You could use `mdz server` to manage the servers. The data plane is designed to be **stateless** and **scalable**. You could easily scale the data plane by adding more servers to the cluster. It uses k3s under the hood, to support VMs, bare-metal, and IoT devices (in the future). You could also deploy OpenModelZ on a existing kubernetes cluster. - Control Plane: The control plane is responsible for the deployments. It manages the deployments and the underlying resources. A request will be routed to the inference servers by the load balancer. And the autoscaler will scale the number of inference servers based on the workload. We provide a domain `*.modelz.live` by default, with the help of a [wildcard DNS server](https://github.com/cunnie/sslip.io) to support the public accessible subdomain for each deployment. You could also use your own domain. You could check out the [architecture](https://docs.open.modelz.ai/architecture) documentation for more details. ## Roadmap 🗂️ Please checkout [ROADMAP](https://docs.open.modelz.ai/community). ## Contribute 😊 We welcome all kinds of contributions from the open-source community, individuals, and partners. - Join our [discord community](https://discord.gg/KqswhpVgdU)! ## Contributors ✨
Ce Gao
Ce Gao

💻 👀
Jinjing Zhou
Jinjing Zhou

💬 🐛 🤔
Keming
Keming

💻 🎨 🚇
Nadeshiko Manju
Nadeshiko Manju

🐛 🎨 🤔
Teddy Xinyuan Chen
Teddy Xinyuan Chen

📖
Wei Zhang
Wei Zhang

💻
Xuanwo
Xuanwo

🖋 🎨 🤔
cutecutecat
cutecutecat

🤔
xieydd
xieydd

🤔
## Acknowledgements 🙏 - [K3s](https://github.com/k3s-io/k3s) for the single control-plane binary and process. - [OpenFaaS](https://github.com/openfaas) for their work on serverless function services. It laid the foundation for OpenModelZ. - [sslip.io](https://github.com/cunnie/sslip.io) for the wildcard DNS service. It makes it possible to access the server from the outside world without any setup. ================================================ FILE: agent/.gitignore ================================================ # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out *.report # Dependency directories (remove the comment below to include it) vendor/ # Go workspace file go.work .vscode/* .idea # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix __debug_bin bin/ debug-bin/ /build.envd .ipynb_checkpoints/ cover.html cmd/test/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ wheelhouse/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST .demo/ pkg/docs/swagger.* ================================================ FILE: agent/Dockerfile ================================================ FROM ubuntu:22.04 LABEL maintainer="modelz-support@tensorchord.ai" RUN apt-get -qq update \ && apt-get -qq install -y --no-install-recommends ca-certificates curl COPY agent /usr/bin/agent ENTRYPOINT ["/usr/bin/agent"] ================================================ FILE: agent/Makefile ================================================ # Copyright 2022 TensorChord Inc. # # The old school Makefile, following are required targets. The Makefile is written # to allow building multiple binaries. You are free to add more targets or change # existing implementations, as long as the semantics are preserved. # # make - default to 'build' target # make lint - code analysis # make test - run unit test (or plus integration test) # make build - alias to build-local target # make build-local - build local binary targets # make build-linux - build linux binary targets # make container - build containers # $ docker login registry -u username -p xxxxx # make push - push containers # make clean - clean up targets # # Not included but recommended targets: # make e2e-test # # The makefile is also responsible to populate project version information. # # # Tweak the variables based on your project. # # This repo's root import path (under GOPATH). ROOT := github.com/tensorchord/openmodelz/agent # Target binaries. You can build multiple binaries for a single project. TARGETS := agent # Container image prefix and suffix added to targets. # The final built images are: # $[REGISTRY]/$[IMAGE_PREFIX]$[TARGET]$[IMAGE_SUFFIX]:$[VERSION] # $[REGISTRY] is an item from $[REGISTRIES], $[TARGET] is an item from $[TARGETS]. IMAGE_PREFIX ?= $(strip ) IMAGE_SUFFIX ?= $(strip ) # Container registries. REGISTRY ?= ghcr.io/tensorchord # Container registry for base images. BASE_REGISTRY ?= docker.io BASE_REGISTRY_USER ?= modelzai # Disable CGO by default. CGO_ENABLED ?= 0 # # These variables should not need tweaking. # # It's necessary to set this because some environments don't link sh -> bash. export SHELL := bash # It's necessary to set the errexit flags for the bash shell. export SHELLOPTS := errexit PACKAGE_NAME := github.com/tensorchord/openmodelz/agent GOLANG_CROSS_VERSION ?= v1.17.6 # Project main package location (can be multiple ones). CMD_DIR := ./cmd # Project output directory. OUTPUT_DIR := ./bin DEBUG_DIR := ./debug-bin # Build directory. BUILD_DIR := ./build # Current version of the project. VERSION ?= $(shell git describe --match 'v[0-9]*' --always --tags --abbrev=0) BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') GIT_COMMIT=$(shell git rev-parse HEAD) GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi) GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi) GITSHA ?= $(shell git rev-parse --short HEAD) # Track code version with Docker Label. DOCKER_LABELS ?= git-describe="$(shell date -u +v%Y%m%d)-$(shell git describe --tags --always --dirty)" # Golang standard bin directory. GOPATH ?= $(shell go env GOPATH) GOROOT ?= $(shell go env GOROOT) BIN_DIR := $(GOPATH)/bin GOLANGCI_LINT := $(BIN_DIR)/golangci-lint # check if we need embed the dashboard DASHBOARD_BUILD ?= debug # Default golang flags used in build and test # -mod=vendor: force go to use the vendor files instead of using the `$GOPATH/pkg/mod` # -p: the number of programs that can be run in parallel # -count: run each test and benchmark 1 times. Set this flag to disable test cache export GOFLAGS ?= -count=1 # # Define all targets. At least the following commands are required: # # All targets. .PHONY: help lint test build container push addlicense debug debug-local build-local generate clean test-local addlicense-install release build-image .DEFAULT_GOAL:=build build: build-local ## Build the release version help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) debug: debug-local ## Build the debug version # more info about `GOGC` env: https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint lint: $(GOLANGCI_LINT) ## Lint GO code @$(GOLANGCI_LINT) run $(GOLANGCI_LINT): curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin mockgen-install: go install github.com/golang/mock/mockgen@v1.6.0 addlicense-install: go install github.com/google/addlicense@latest # https://github.com/swaggo/swag/pull/1322, we should use master instead of latest for now. swag-install: go install github.com/swaggo/swag/cmd/swag@v1.8.7 build-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) go build -tags $(DASHBOARD_BUILD) -trimpath -v -o $(OUTPUT_DIR)/$${target} \ -ldflags "-s -w -X $(ROOT)/pkg/version.version=$(VERSION) -X $(ROOT)/pkg/version.buildDate=$(BUILD_DATE) -X $(ROOT)/pkg/version.gitCommit=$(GIT_COMMIT) -X $(ROOT)/pkg/version.gitTreeState=$(GIT_TREE_STATE)" \ $(CMD_DIR)/$${target}; \ done # It is used by vscode to attach into the process. debug-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) go build -tags $(DASHBOARD_BUILD) -trimpath \ -v -o $(DEBUG_DIR)/$${target} \ -gcflags='all=-N -l' \ $(CMD_DIR)/$${target}; \ done addlicense: addlicense-install ## Add license to GO code files addlicense -l mpl -c "TensorChord Inc." $$(find . -type f -name '*.go' | grep -v pkg/docs/docs.go) test-local: @go test -tags=$(DASHBOARD_BUILD) -v -race -coverprofile=coverage.out ./... test: ## Run the tests @go test -tags=$(DASHBOARD_BUILD) -race -coverprofile=coverage.out ./... @go tool cover -func coverage.out | tail -n 1 | awk '{ print "Total coverage: " $$3 }' clean: ## Clean the outputs and artifacts @-rm -vrf ${OUTPUT_DIR} @-rm -vrf ${DEBUG_DIR} @-rm -vrf build dist .eggs *.egg-info fmt: swag-install ## Run go fmt against code. go fmt ./... swag fmt vet: ## Run go vet against code. go vet ./... swag: swag-install swag init -g ./cmd/agent/main.go --parseDependency --output ./pkg/docs build-image: build-local docker build -t ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/openmodelz-agent:dev -f Dockerfile ./bin docker push ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/openmodelz-agent:dev release: @if [ ! -f ".release-env" ]; then \ echo "\033[91m.release-env is required for release\033[0m";\ exit 1;\ fi docker run \ --rm \ --privileged \ -e CGO_ENABLED=1 \ --env-file .release-env \ -v /var/run/docker.sock:/var/run/docker.sock \ -v `pwd`:/go/src/$(PACKAGE_NAME) \ -v `pwd`/sysroot:/sysroot \ -w /go/src/$(PACKAGE_NAME) \ goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ release --rm-dist generate: mockgen-install swag @mockgen -source pkg/runtime/runtime.go -destination pkg/runtime/mock/mock.go -package mock ================================================ FILE: agent/README.md ================================================
# OpenModelZ Agent

discord invitation link trackgit-views

## Installation ``` pip install openmodelz ``` ## Architecture Please check out [Architecture](https://docs.open.modelz.ai/architecture) documentation. ================================================ FILE: agent/api/types/build.go ================================================ package types type Build struct { Spec BuildSpec `json:"spec"` Status BuildStatus `json:"status,omitempty"` } type BuildSpec struct { Name string `json:"name,omitempty"` Namespace string `json:"namespace,omitempty"` GitRepositorySource `json:",inline,omitempty"` DockerSource `json:",inline,omitempty"` BuildTarget BuildTarget `json:",inline,omitempty"` } type DockerSource struct { ArtifactImage string `json:"image,omitempty"` ArtifactImageTag string `json:"image_tag,omitempty"` AuthN AuthN `json:"authn,omitempty"` SecretID string `json:"secret_id,omitempty"` } type BuildTarget struct { // directory is the target directory name. // Must not contain or start with '..'. If '.' is supplied, the volume directory will be the // git repository. Otherwise, if specified, the volume will contain the git repository in // the subdirectory with the given name. // +optional Directory string `json:"directory,omitempty"` Builder BuilderType `json:"builder,omitempty"` ArtifactImage string `json:"image,omitempty"` ArtifactImageTag string `json:"image_tag,omitempty"` Digest string `json:"digest,omitempty"` Duration string `json:"duration,omitempty"` Registry string `json:"registry,omitempty"` RegistryToken string `json:"registry_token,omitempty"` } type AuthN struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Token string `json:"token,omitempty"` } type BuildStatus struct { Phase BuildPhase `json:"phase,omitempty"` } type BuildPhase string const ( BuildPhasePending BuildPhase = "Pending" BuildPhaseRunning BuildPhase = "Running" BuildPhaseSucceeded BuildPhase = "Succeeded" BuildPhaseFailed BuildPhase = "Failed" ) type BuilderType string const ( BuilderTypeDockerfile BuilderType = "Dockerfile" BuilderTypeENVD BuilderType = "envd" BuilderTypeImage BuilderType = "image" ) type GitRepositorySource struct { // repository is the URL Repository string `json:"repository"` Branch string `json:"branch,omitempty"` // revision is the commit hash for the specified revision. // +optional Revision string `json:"revision,omitempty"` } ================================================ FILE: agent/api/types/error.go ================================================ package types type ErrorResponse struct { Message string `json:"message"` } ================================================ FILE: agent/api/types/event.go ================================================ package types import "time" const ( DeploymentCreateEvent = "deployment-create" DeploymentUpdateEvent = "deployment-update" DeploymentDeleteEvent = "deployment-delete" DeploymentScaleUpEvent = "deployment-scale-up" DeploymentScaleDownEvent = "deployment-scale-down" DeploymentScaleBlockEvent = "deployment-scale-block" PodCreateEvent = "pod-create" PodReadyEvent = "pod-ready" PodTimeoutEvent = "pod-timeout" ) type DeploymentEvent struct { ID string `json:"id"` CreatedAt time.Time `json:"created_at"` UserID string `json:"user_id"` DeploymentID string `json:"deployment_id"` EventType string `json:"event_type"` Message string `json:"message"` } ================================================ FILE: agent/api/types/inference_deployment.go ================================================ package types // InferenceDeployment represents a request to create or update a Model. type InferenceDeployment struct { Spec InferenceDeploymentSpec `json:"spec"` Status InferenceDeploymentStatus `json:"status,omitempty"` } type InferenceDeploymentSpec struct { // Name is the name of the inference. Name string `json:"name"` // Namespace for the inference. Namespace string `json:"namespace,omitempty"` // Scaling is the scaling configuration for the inference. Scaling *ScalingConfig `json:"scaling,omitempty"` // Framework is the inference framework. Framework Framework `json:"framework,omitempty"` // Image is a fully-qualified container image Image string `json:"image"` // Port is the port exposed by the inference. Port *int32 `json:"port,omitempty"` // HTTPProbePath is the path of the http probe. HTTPProbePath *string `json:"http_probe_path,omitempty"` // Command to run when starting the Command *string `json:"command,omitempty"` // EnvVars can be provided to set environment variables for the inference runtime. EnvVars map[string]string `json:"envVars,omitempty"` // Constraints are the constraints for the inference. Constraints []string `json:"constraints,omitempty"` // Secrets list of secrets to be made available to inference. Secrets []string `json:"secrets,omitempty"` // Labels are key-value pairs that may be attached to the inference. Labels map[string]string `json:"labels,omitempty"` // Annotations are key-value pairs that may be attached to the inference. Annotations map[string]string `json:"annotations,omitempty"` // Resources are the compute resource requirements. Resources *ResourceRequirements `json:"resources,omitempty"` } // Framework is the inference framework. It is only used to set the default port // and command. For example, if the framework is "gradio", the default port is // 7860 and the default command is "python app.py". You could override these // defaults by setting the port and command fields and framework to `other`. type Framework string const ( FrameworkGradio Framework = "gradio" FrameworkStreamlit Framework = "streamlit" FrameworkMosec Framework = "mosec" FrameworkOther Framework = "other" ) type ScalingConfig struct { // MinReplicas is the lower limit for the number of replicas to which the // autoscaler can scale down. It defaults to 0. MinReplicas *int32 `json:"min_replicas,omitempty"` // MaxReplicas is the upper limit for the number of replicas to which the // autoscaler can scale up. It cannot be less that minReplicas. It defaults // to 1. MaxReplicas *int32 `json:"max_replicas,omitempty"` // TargetLoad is the target load. In capacity mode, it is the expected number of the inflight requests per replica. TargetLoad *int32 `json:"target_load,omitempty"` // Type is the scaling type. It can be either "capacity" or "rps". Default is "capacity". Type *ScalingType `json:"type,omitempty"` // ZeroDuration is the duration (in seconds) of zero load before scaling down to zero. Default is 5 minutes. ZeroDuration *int32 `json:"zero_duration,omitempty"` // StartupDuration is the duration (in seconds) of startup time. StartupDuration *int32 `json:"startup_duration,omitempty"` } type ScalingType string const ( ScalingTypeCapacity ScalingType = "capacity" ScalingTypeRPS ScalingType = "rps" ) // ResourceRequirements describes the compute resource requirements. type ResourceRequirements struct { // Limits describes the maximum amount of compute resources allowed. // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ // +optional Limits ResourceList `json:"limits,omitempty" protobuf:"bytes,1,rep,name=limits,casttype=ResourceList,castkey=ResourceName"` // Requests describes the minimum amount of compute resources required. // If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, // otherwise to an implementation-defined value. // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ // +optional Requests ResourceList `json:"requests,omitempty" protobuf:"bytes,2,rep,name=requests,casttype=ResourceList,castkey=ResourceName"` } // ResourceList is a set of (resource name, quantity) pairs. type ResourceList map[ResourceName]Quantity type ResourceName string const ( ResourceCPU ResourceName = "cpu" ResourceMemory ResourceName = "memory" ResourceGPU ResourceName = "gpu" ) type Quantity string const ( RuntimeClassNvidia string = "nvidia" ) type ImageCache struct { // Name is the name of the inference. Name string `json:"name"` Namespace string `json:"namespace"` Image string `json:"image"` ForceFullCache bool `json:"force_full_cache"` NodeSelector string `json:"node_selector"` } ================================================ FILE: agent/api/types/inference_deployment_instance.go ================================================ package types import "time" type InferenceDeploymentInstance struct { Spec InferenceDeploymentInstanceSpec `json:"spec,omitempty"` Status InferenceDeploymentInstanceStatus `json:"status,omitempty"` } type InferenceDeploymentInstanceSpec struct { Namespace string `json:"namespace,omitempty"` Name string `json:"name,omitempty"` OwnerReference string `json:"owner_reference,omitempty"` } type InferenceDeploymentInstanceStatus struct { Phase InstancePhase `json:"phase,omitempty"` StartTime time.Time `json:"createdAt,omitempty"` Reason string `json:"reason,omitempty"` Message string `json:"message,omitempty"` } type InstancePhase string const ( InstancePhaseScheduling InstancePhase = "Scheduling" InstancePhasePending InstancePhase = "Pending" InstancePhaseRunning InstancePhase = "Running" InstancePhaseFailed InstancePhase = "Failed" InstancePhaseSucceeded InstancePhase = "Succeeded" InstancePhaseUnknown InstancePhase = "Unknown" InstancePhaseCreating InstancePhase = "Creating" InstancePhaseInitializing InstancePhase = "Initializing" ) ================================================ FILE: agent/api/types/inference_status.go ================================================ package types import "time" // InferenceDeploymentStatus exported for system/inferences endpoint type InferenceDeploymentStatus struct { Phase Phase `json:"phase,omitempty"` // InvocationCount count of invocations InvocationCount int32 `json:"invocationCount,omitempty"` // Replicas desired within the cluster Replicas int32 `json:"replicas,omitempty"` // AvailableReplicas is the count of replicas ready to receive // invocations as reported by the faas-provider AvailableReplicas int32 `json:"availableReplicas,omitempty"` // CreatedAt is the time read back from the faas backend's // data store for when the function or its container was created. CreatedAt *time.Time `json:"createdAt,omitempty"` // Usage represents CPU and RAM used by all of the // functions' replicas. Divide by AvailableReplicas for an // average value per replica. Usage *InferenceUsage `json:"usage,omitempty"` // EventMessage record human readable message indicating details about the event of deployment. EventMessage string `json:"eventMessage,omitempty"` } type Phase string const ( // PhaseReady is the state of an inference when it is ready to // receive invocations. PhaseReady Phase = "Ready" // PhaseScaling is the state of an inference when scales. PhaseScaling Phase = "Scaling" PhaseTerminating Phase = "Terminating" PhaseNoReplicas Phase = "NoReplicas" PhaseNotReady Phase = "NotReady" PhaseBuilding Phase = "Building" PhaseOptimizing Phase = "Optimizing" ) // InferenceUsage represents CPU and RAM used by all of the // functions' replicas. // // CPU is measured in seconds consumed since the last measurement // RAM is measured in total bytes consumed type InferenceUsage struct { // CPU is the increase in CPU usage since the last measurement // equivalent to Kubernetes' concept of millicores. CPU float64 `json:"cpu,omitempty"` //TotalMemoryBytes is the total memory usage in bytes. TotalMemoryBytes float64 `json:"totalMemoryBytes,omitempty"` GPU float64 `json:"gpu,omitempty"` } ================================================ FILE: agent/api/types/info.go ================================================ package types // ProviderInfo provides information about the configured provider type ProviderInfo struct { Name string `json:"provider"` Version *VersionInfo `json:"version"` Orchestration string `json:"orchestration"` } // VersionInfo provides the commit message, sha and release version number type VersionInfo struct { Version string `json:"version,omitempty"` BuildDate string `json:"build_date,omitempty"` GitCommit string `json:"git_commit,omitempty"` GitTag string `json:"git_tag,omitempty"` GitTreeState string `json:"git_tree_state,omitempty"` GoVersion string `json:"go_version,omitempty"` Compiler string `json:"compiler,omitempty"` Platform string `json:"platform,omitempty"` } ================================================ FILE: agent/api/types/log.go ================================================ package types import "time" type LogRequest struct { Namespace string `form:"namespace" json:"namespace,omitempty"` Name string `form:"name" json:"name,omitempty"` // Instance is the optional pod name, that allows you to request logs from a specific instance Instance string `form:"instance" json:"instance,omitempty"` // Follow is allows the user to request a stream of logs until the timeout Follow bool `form:"follow" json:"follow,omitempty"` // Tail sets the maximum number of log messages to return, <=0 means unlimited Tail int `form:"tail" json:"tail,omitempty"` Since string `form:"since" json:"since,omitempty"` // End is the end time of the log stream End string `form:"end" json:"end,omitempty"` } // Message is a specific log message from a function container log stream type Message struct { // Name is the function name Name string `json:"name"` Namespace string `json:"namespace"` // instance is the name/id of the specific function instance Instance string `json:"instance"` // Timestamp is the timestamp of when the log message was recorded Timestamp time.Time `json:"timestamp"` // Text is the raw log message content Text string `json:"text"` } ================================================ FILE: agent/api/types/modelz_cloud.go ================================================ package types import "time" const ( ClusterStatusInit = "init" ClusterStatusActive = "active" ClusterStatusUnknown = "unknown" ) const ( DailEndPointSuffix = "/api/v1/clusteragent/connect" ) type AgentToken struct { UID string `json:"uid,omitempty"` Token string `json:"token,omitempty"` ClusterName string `json:"cluster_name,omitempty"` } type ManagedCluster struct { Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` TokenID string `json:"token_id,omitempty"` Version string `json:"version,omitempty"` KubernetesVersion string `json:"kubernetes_version,omitempty"` Platform string `json:"platform,omitempty"` Status string `json:"status,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` Region string `json:"region,omitempty"` ServerResources string `json:"server_resources,omitempty"` PrometheusURL string `json:"prometheus_url,omitempty"` } type APIKeyMap map[string]string type NamespaceList struct { Items []string `json:"items,omitempty"` } ================================================ FILE: agent/api/types/namespace.go ================================================ package types type NamespaceRequest struct { Name string `json:"name,omitempty"` } ================================================ FILE: agent/api/types/queue.go ================================================ package types import ( "net/http" "net/url" ) // Request for asynchronous processing type QueueRequest struct { // Header from HTTP request Header http.Header // Host from HTTP request Host string // Body from HTTP request to use for invocation Body []byte // Method from HTTP request Method string // Path from HTTP request Path string // QueryString from HTTP request QueryString string // Function name to invoke Function string // QueueName to publish the request to, leave blank // for default. QueueName string // Used by queue worker to submit a result CallbackURL *url.URL `json:"CallbackUrl"` } // RequestQueuer can public a request to be executed asynchronously type RequestQueuer interface { Queue(req *QueueRequest) error } ================================================ FILE: agent/api/types/requests.go ================================================ // Copyright (c) Alex Ellis 2017. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. package types // ScaleServiceRequest scales the service to the requested replcia count. type ScaleServiceRequest struct { ServiceName string `json:"serviceName"` Replicas uint64 `json:"replicas"` EventMessage string `json:"eventMessage"` Attempt int `json:"attempt"` } // DeleteFunctionRequest delete a deployed function type DeleteFunctionRequest struct { FunctionName string `json:"functionName"` } ================================================ FILE: agent/api/types/secret.go ================================================ package types // Secret for underlying orchestrator type Secret struct { // Name of the secret Name string `json:"name"` // Namespace if applicable for the secret Namespace string `json:"namespace,omitempty"` // Value is a string representing the string's value Value string `json:"value,omitempty"` // RawValue can be used to provide binary data when // Value is not set RawValue []byte `json:"rawValue,omitempty"` } ================================================ FILE: agent/api/types/server.go ================================================ package types type Server struct { Spec ServerSpec `json:"spec,omitempty"` Status ServerStatus `json:"status,omitempty"` } type ServerSpec struct { Name string `json:"name,omitempty"` Labels map[string]string `json:"labels,omitempty"` } type ServerStatus struct { Allocatable ResourceList `json:"allocatable,omitempty"` Capacity ResourceList `json:"capacity,omitempty"` Phase string `json:"phase,omitempty"` System NodeSystemInfo `json:"system,omitempty"` } // NodeSystemInfo is a set of ids/uuids to uniquely identify the node. type NodeSystemInfo struct { // MachineID reported by the node. For unique machine identification // in the cluster this field is preferred. Learn more from man(5) // machine-id: http://man7.org/linux/man-pages/man5/machine-id.5.html MachineID string `json:"machineID" protobuf:"bytes,1,opt,name=machineID"` // Kernel Version reported by the node from 'uname -r' (e.g. 3.16.0-0.bpo.4-amd64). KernelVersion string `json:"kernelVersion" protobuf:"bytes,4,opt,name=kernelVersion"` // OS Image reported by the node from /etc/os-release (e.g. Debian GNU/Linux 7 (wheezy)). OSImage string `json:"osImage" protobuf:"bytes,5,opt,name=osImage"` // The Operating System reported by the node OperatingSystem string `json:"operatingSystem" protobuf:"bytes,9,opt,name=operatingSystem"` // The Architecture reported by the node Architecture string `json:"architecture" protobuf:"bytes,10,opt,name=architecture"` // The Resource Type reported by the node ResourceType string `json:"resourceType" protobuf:"bytes,11,opt,name=resourceType"` } ================================================ FILE: agent/client/build.go ================================================ package client import ( "context" "encoding/json" "fmt" "net/url" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" ) func (cli *Client) BuildCreate(ctx context.Context, namespace string, build types.Build) error { build.Spec.Namespace = namespace logrus.Debugf("create new build: %s", build) val := url.Values{} resp, err := cli.post(ctx, gatewayBuildControlPlanePath, val, build, nil) defer ensureReaderClosed(resp) if err != nil { return wrapResponseError(err, resp, "build", build.Spec.Name) } return nil } func (cli *Client) BuildGet(ctx context.Context, namespace, name string) (types.Build, error) { val := url.Values{} val.Add("namespace", namespace) build := types.Build{} resp, err := cli.get( ctx, fmt.Sprintf(gatewayBuildInstanceControlPlanePath, name), val, nil) defer ensureReaderClosed(resp) if err != nil { logrus.Infof("failed to query build.get: %s", err) return build, wrapResponseError(err, resp, "build", name) } err = json.NewDecoder(resp.body).Decode(&build) if err != nil { logrus.Infof("failed to decode build: %s", err) return build, wrapResponseError(err, resp, "build", name) } return build, nil } func (cli *Client) BuildList(ctx context.Context, namespace string) ([]types.Build, error) { val := url.Values{} val.Add("namespace", namespace) resp, err := cli.get(ctx, gatewayBuildControlPlanePath, val, nil) defer ensureReaderClosed(resp) if err != nil { logrus.Infof("failed to query build.list: %s", err) return nil, wrapResponseError(err, resp, "build", namespace) } var builds []types.Build err = json.NewDecoder(resp.body).Decode(&builds) if err != nil { logrus.Infof("failed to decode builds: %s", err) return nil, wrapResponseError(err, resp, "build", namespace) } return builds, nil } ================================================ FILE: agent/client/client.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "net/http" "net/url" "path" "strings" "github.com/cockroachdb/errors" "github.com/docker/go-connections/sockets" ) // Refer to github.com/docker/docker/client // ErrRedirect is the error returned by checkRedirect when the request is non-GET. var ErrRedirect = errors.New("unexpected redirect in response") // Client is the API client that performs all operations // against a docker server. type Client struct { // scheme sets the scheme for the client scheme string // host holds the server address to connect to host string // proto holds the client protocol i.e. unix. proto string // addr holds the client address. addr string // basePath holds the path to prepend to the requests. basePath string // client used to send and receive http requests. client *http.Client // version of the server to talk to. version string // custom http headers configured by users. customHTTPHeaders map[string]string // manualOverride is set to true when the version was set by users. manualOverride bool // negotiateVersion indicates if the client should automatically negotiate // the API version to use when making requests. API version negotiation is // performed on the first request, after which negotiated is set to "true" // so that subsequent requests do not re-negotiate. negotiateVersion bool // negotiated indicates that API version negotiation took place negotiated bool } // NewClientWithOpts initializes a new API client with a default HTTPClient, and // default API host and version. It also initializes the custom HTTP headers to // add to each request. // // It takes an optional list of Opt functional arguments, which are applied in // the order they're provided, which allows modifying the defaults when creating // the client. For example, the following initializes a client that configures // itself with values from environment variables (client.FromEnv), and has // automatic API version negotiation enabled (client.WithAPIVersionNegotiation()). // // cli, err := client.NewClientWithOpts( // client.FromEnv, // client.WithAPIVersionNegotiation(), // ) func NewClientWithOpts(ops ...Opt) (*Client, error) { client, err := defaultHTTPClient(DefaultModelzGatewayHost) if err != nil { return nil, err } c := &Client{ host: DefaultModelzGatewayHost, version: "", client: client, proto: defaultProto, addr: defaultAddr, basePath: apiBasePath, } for _, op := range ops { if err := op(c); err != nil { return nil, err } } if c.scheme == "" { c.scheme = "http" tlsConfig := resolveTLSConfig(c.client.Transport) if tlsConfig != nil { // TODO(stevvooe): This isn't really the right way to write clients in Go. // `NewClient` should probably only take an `*http.Client` and work from there. // Unfortunately, the model of having a host-ish/url-thingy as the connection // string has us confusing protocol and transport layers. We continue doing // this to avoid breaking existing clients but this should be addressed. c.scheme = "https" } } return c, nil } func defaultHTTPClient(host string) (*http.Client, error) { hostURL, err := ParseHostURL(host) if err != nil { return nil, err } transport := &http.Transport{} _ = sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host) return &http.Client{ Transport: transport, CheckRedirect: CheckRedirectKeepHeader, }, nil } // CheckRedirect specifies the policy for dealing with redirect responses: // If the request is non-GET return ErrRedirect, otherwise use the last response. // // Go 1.8 changes behavior for HTTP redirects (specifically 301, 307, and 308) // in the client. The envd client (and by extension envd API client) can be // made to send a request like POST /containers//start where what would normally // be in the name section of the URL is empty. This triggers an HTTP 301 from // the daemon. // // In go 1.8 this 301 will be converted to a GET request, and ends up getting // a 404 from the daemon. This behavior change manifests in the client in that // before, the 301 was not followed and the client did not generate an error, // but now results in a message like Error response from daemon: page not found. func CheckRedirect(req *http.Request, via []*http.Request) error { if via[0].Method == http.MethodGet { return http.ErrUseLastResponse } return ErrRedirect } func CheckRedirectKeepHeader(req *http.Request, via []*http.Request) error { req.Header = via[0].Header.Clone() return nil } // DaemonHost returns the host address used by the client func (cli *Client) DaemonHost() string { return cli.host } // HTTPClient returns a copy of the HTTP client bound to the server func (cli *Client) HTTPClient() *http.Client { c := *cli.client return &c } // ParseHostURL parses a url string, validates the string is a host url, and // returns the parsed URL func ParseHostURL(host string) (*url.URL, error) { protoAddrParts := strings.SplitN(host, "://", 2) if len(protoAddrParts) == 1 { return nil, errors.Errorf("unable to parse docker host `%s`", host) } var basePath string proto, addr := protoAddrParts[0], protoAddrParts[1] if proto == "tcp" { parsed, err := url.Parse("tcp://" + addr) if err != nil { return nil, err } addr = parsed.Host basePath = parsed.Path } return &url.URL{ Scheme: proto, Host: addr, Path: basePath, }, nil } // Close the transport used by the client func (cli *Client) Close() error { if t, ok := cli.client.Transport.(*http.Transport); ok { t.CloseIdleConnections() } return nil } // getAPIPath returns the versioned request path to call the api. // It appends the query parameters to the path if they are not empty. func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string { var apiPath string if cli.version != "" { v := strings.TrimPrefix(cli.version, "v") apiPath = path.Join(cli.basePath, "/v"+v, p) } else { apiPath = path.Join(cli.basePath, p) } return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String() } ================================================ FILE: agent/client/const.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client const DefaultModelzGatewayHost = "http://0.0.0.0:8080" const defaultProto = "http" const defaultAddr = "0.0.0.0:8080" // Base path for api, distinguish from frontend pages const apiBasePath = "" const ( gatewayInferControlPlanePath = "/system/inferences" gatewayInferScaleControlPath = "/system/scale-inference" gatewayInferInstanceControlPlanePath = "/system/inference/%s/instances" gatewayInferInstanceExecControlPlanePath = "/system/inference/%s/instance/%s/exec" gatewayServerControlPlanePath = "/system/servers" gatewayServerLabelCreateControlPlanePath = "/system/server/%s/labels" gatewayServerNodeDeleteControlPlanePath = "/system/server/%s/delete" gatewayNamespaceControlPlanePath = "/system/namespaces" gatewayBuildControlPlanePath = "/system/build" gatewayBuildInstanceControlPlanePath = "/system/build/%s" gatewayImageCacheControlPlanePath = "/system/image-cache" modelzCloudClusterControlPlanePath = "/api/v1/users/%s/clusters/%s" modelzCloudClusterWithUserControlPlanePath = "/api/v1/users/%s/clusters" modelzCloudClusterAPIKeyControlPlanePath = "/api/v1/users/%s/clusters/%s/api_keys" modelzCloudClusterNamespaceControlPlanePath = "/api/v1/users/%s/clusters/%s/namespaces" modelzCloudClusterDeploymentControlPlanePath = "/api/v1/users/%s/clusters/%s/deployments/%s/agent" modelzCloudClusterDeploymentEventControlPlanePath = "/api/v1/users/%s/clusters/%s/deployments/%s/event" ) const ( // EnvOverrideHost is the name of the environment variable that can be used // to override the default host to connect to (DefaultEnvdServerHost). // // This env-var is read by FromEnv and WithHostFromEnv and when set to a // non-empty value, takes precedence over the default host (which is platform // specific), or any host already set. EnvOverrideHost = "MODELZ_GATEWAY_HOST" // EnvOverrideCertPath is the name of the environment variable that can be // used to specify the directory from which to load the TLS certificates // (ca.pem, cert.pem, key.pem) from. These certificates are used to configure // the Client for a TCP connection protected by TLS client authentication. // // TLS certificate verification is enabled by default if the Client is configured // to use a TLS connection. Refer to EnvTLSVerify below to learn how to // disable verification for testing purposes. // // // For local access to the API, it is recommended to connect with the daemon // using the default local socket connection (on Linux), or the named pipe // (on Windows). // // If you need to access the API of a remote daemon, consider using an SSH // (ssh://) connection, which is easier to set up, and requires no additional // configuration if the host is accessible using ssh. EnvOverrideCertPath = "ENVD_SERVER_CERT_PATH" // EnvTLSVerify is the name of the environment variable that can be used to // enable or disable TLS certificate verification. When set to a non-empty // value, TLS certificate verification is enabled, and the client is configured // to use a TLS connection, using certificates from the default directories // (within `~/.envd`); refer to EnvOverrideCertPath above for additional // details. // // // Before setting up your client and daemon to use a TCP connection with TLS // client authentication, consider using one of the alternatives mentioned // in EnvOverrideCertPath above. // // Disabling TLS certificate verification (for testing purposes) // // TLS certificate verification is enabled by default if the Client is configured // to use a TLS connection, and it is highly recommended to keep verification // enabled to prevent machine-in-the-middle attacks. // // Set the "ENVD_SERVER_TLS_VERIFY" environment to an empty string ("") to // disable TLS certificate verification. Disabling verification is insecure, // so should only be done for testing purposes. From the Go documentation // (https://pkg.go.dev/crypto/tls#Config): // // InsecureSkipVerify controls whether a client verifies the server's // certificate chain and host name. If InsecureSkipVerify is true, crypto/tls // accepts any certificate presented by the server and any host name in that // certificate. In this mode, TLS is susceptible to machine-in-the-middle // attacks unless custom verification is used. This should be used only for // testing or in combination with VerifyConnection or VerifyPeerCertificate. EnvTLSVerify = "ENVD_SERVER_TLS_VERIFY" ) ================================================ FILE: agent/client/errors.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client // import "github.com/docker/docker/client" import ( "fmt" "net/http" "github.com/cockroachdb/errors" "github.com/tensorchord/openmodelz/agent/errdefs" ) // errConnectionFailed implements an error returned when connection failed. type errConnectionFailed struct { host string } // Error returns a string representation of an errConnectionFailed func (err errConnectionFailed) Error() string { if err.host == "" { return "Cannot connect to the backend" } return fmt.Sprintf("Cannot connect at %s", err.host) } // IsErrConnectionFailed returns true if the error is caused by connection failed. func IsErrConnectionFailed(err error) bool { return errors.As(err, &errConnectionFailed{}) } // ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed. func ErrorConnectionFailed(host string) error { return errConnectionFailed{host: host} } // Deprecated: use the errdefs.NotFound() interface instead. Kept for backward compatibility type notFound interface { error NotFound() bool } // IsErrNotFound returns true if the error is a NotFound error, which is returned // by the API when some object is not found. func IsErrNotFound(err error) bool { if errdefs.IsNotFound(err) { return true } var e notFound return errors.As(err, &e) } type objectNotFoundError struct { object string id string } func (e objectNotFoundError) NotFound() {} func (e objectNotFoundError) Error() string { return fmt.Sprintf("Error: No such %s: %s", e.object, e.id) } // IsErrUnauthorized returns true if the error is caused // when a remote registry authentication fails // // Deprecated: use errdefs.IsUnauthorized func IsErrUnauthorized(err error) bool { return errdefs.IsUnauthorized(err) } type pluginPermissionDenied struct { name string } func (e pluginPermissionDenied) Error() string { return "Permission denied while installing plugin " + e.name } // IsErrNotImplemented returns true if the error is a NotImplemented error. // This is returned by the API when a requested feature has not been // implemented. // // Deprecated: use errdefs.IsNotImplemented func IsErrNotImplemented(err error) bool { return errdefs.IsNotImplemented(err) } func wrapResponseError(err error, resp serverResponse, object, id string) error { switch { case err == nil: return nil case resp.statusCode == http.StatusNotFound: return objectNotFoundError{object: object, id: id} case resp.statusCode == http.StatusNotImplemented: return errdefs.NotImplemented(err) default: return err } } ================================================ FILE: agent/client/hijack.go ================================================ package client // import "docker.io/go-docker" import ( "net/url" "github.com/gorilla/websocket" "golang.org/x/net/context" ) // HijackedResponse holds connection information for a hijacked request. type HijackedResponse struct { Conn *websocket.Conn } // Close closes the hijacked connection and reader. func (h *HijackedResponse) Close() { h.Conn.Close() } // postHijacked sends a POST request and hijacks the connection. func (cli *Client) websocket(ctx context.Context, path string, query url.Values, headers map[string][]string) (HijackedResponse, error) { apiPath := cli.getAPIPath(ctx, path, nil) scheme := "ws" if cli.scheme == "https" { scheme = "wss" } apiURL := url.URL{ Scheme: scheme, Host: cli.addr, Path: apiPath, RawQuery: query.Encode(), } c, _, err := websocket.DefaultDialer.DialContext(ctx, apiURL.String(), nil) if err != nil { return HijackedResponse{}, err } return HijackedResponse{Conn: c}, err } func (h HijackedResponse) Read(p []byte) (int, error) { // Read message from websocket connection. tm := &TerminalMessage{} if err := h.Conn.ReadJSON(tm); err != nil { return 0, err } if tm.Op != "stdout" { return 0, nil } return copy(p, tm.Data), nil } func (h HijackedResponse) Write(p []byte) (int, error) { // Write message to websocket connection. tm := &TerminalMessage{ Op: "stdin", Data: string(p), } if err := h.Conn.WriteJSON(tm); err != nil { return 0, err } return len(p), nil } // TerminalMessage is the messaging protocol between ShellController and TerminalSession. // // OP DIRECTION FIELD(S) USED DESCRIPTION // --------------------------------------------------------------------- // bind fe->be SessionID Id sent back from TerminalResponse // stdin fe->be Data Keystrokes/paste buffer // resize fe->be Rows, Cols New terminal size // stdout be->fe Data Output from the process // toast be->fe Data OOB message to be shown to the user type TerminalMessage struct { ID string `json:"id,omitempty"` Op string `json:"op,omitempty"` Data string `json:"data,omitempty"` Rows uint16 `json:"rows,omitempty"` Cols uint16 `json:"cols,omitempty"` } ================================================ FILE: agent/client/image_cache_create.go ================================================ package client import ( "context" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) func (cli *Client) ImageCacheCreate(ctx context.Context, namespace string, imageCache *types.ImageCache) error { urlValues := url.Values{} urlValues.Add("namespace", namespace) resp, err := cli.post(ctx, gatewayImageCacheControlPlanePath, urlValues, imageCache, nil) defer ensureReaderClosed(resp) if err != nil { return wrapResponseError(err, resp, "imagecache", imageCache.Name) } return wrapResponseError(err, resp, "imagecache", imageCache.Name) } ================================================ FILE: agent/client/inference_create.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // InferenceCreate creates the inference. func (cli *Client) InferenceCreate(ctx context.Context, namespace string, inference types.InferenceDeployment) (types.InferenceDeployment, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) resp, err := cli.post(ctx, gatewayInferControlPlanePath, urlValues, inference, nil) defer ensureReaderClosed(resp) if err != nil { return inference, wrapResponseError(err, resp, "inference", inference.Spec.Name) } return inference, wrapResponseError(err, resp, "inference", inference.Spec.Name) } ================================================ FILE: agent/client/inference_get.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "encoding/json" "fmt" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // InferenceGet gets the inference. func (cli *Client) InferenceGet(ctx context.Context, namespace, name string) (types.InferenceDeployment, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) url := fmt.Sprintf("/system/inference/%s", name) resp, err := cli.get(ctx, url, urlValues, nil) defer ensureReaderClosed(resp) if err != nil { return types.InferenceDeployment{}, wrapResponseError(err, resp, "inference", name) } var inference types.InferenceDeployment err = json.NewDecoder(resp.body).Decode(&inference) if err != nil { return types.InferenceDeployment{}, wrapResponseError(err, resp, "inference", name) } return inference, wrapResponseError(err, resp, "inference", name) } ================================================ FILE: agent/client/inference_list.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "encoding/json" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // InferenceList lists the inferences. func (cli *Client) InferenceList(ctx context.Context, namespace string) ([]types.InferenceDeployment, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) resp, err := cli.get(ctx, gatewayInferControlPlanePath, urlValues, nil) defer ensureReaderClosed(resp) if err != nil { return nil, wrapResponseError(err, resp, "inferences with namespace", namespace) } var inferences []types.InferenceDeployment err = json.NewDecoder(resp.body).Decode(&inferences) return inferences, err } ================================================ FILE: agent/client/inference_remove.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // InferenceRemove removes the inference. func (cli *Client) InferenceRemove(ctx context.Context, namespace string, name string) error { urlValues := url.Values{} urlValues.Add("namespace", namespace) req := types.DeleteFunctionRequest{ FunctionName: name, } resp, err := cli.delete(ctx, gatewayInferControlPlanePath, urlValues, req, nil) defer ensureReaderClosed(resp) return wrapResponseError(err, resp, "inference", name) } ================================================ FILE: agent/client/inference_scale.go ================================================ package client import ( "context" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // InferenceScale scales the inference. func (cli *Client) InferenceScale(ctx context.Context, namespace string, name string, replicas int, eventMessage string) error { urlValues := url.Values{} urlValues.Add("namespace", namespace) req := types.ScaleServiceRequest{ ServiceName: name, Replicas: uint64(replicas), EventMessage: eventMessage, } resp, err := cli.post(ctx, gatewayInferScaleControlPath, urlValues, req, nil) defer ensureReaderClosed(resp) return wrapResponseError(err, resp, "inference", name) } ================================================ FILE: agent/client/inference_update.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // DeploymentUpdate creates the deployment. func (cli *Client) DeploymentUpdate(ctx context.Context, namespace string, inference types.InferenceDeployment) (types.InferenceDeployment, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) resp, err := cli.put(ctx, gatewayInferControlPlanePath, urlValues, inference, nil) defer ensureReaderClosed(resp) if err != nil { return inference, wrapResponseError(err, resp, "inference", inference.Spec.Name) } return inference, wrapResponseError(err, resp, "inference", inference.Spec.Name) } ================================================ FILE: agent/client/info.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "encoding/json" "github.com/tensorchord/openmodelz/agent/api/types" ) // InfoGet gets the agent info. func (cli *Client) InfoGet(ctx context.Context) (types.ProviderInfo, error) { resp, err := cli.get(ctx, "/system/info", nil, nil) defer ensureReaderClosed(resp) if err != nil { return types.ProviderInfo{}, wrapResponseError(err, resp, "info", "system") } var info types.ProviderInfo err = json.NewDecoder(resp.body).Decode(&info) if err != nil { return types.ProviderInfo{}, wrapResponseError(err, resp, "info", "system") } return info, wrapResponseError(err, resp, "info", "system") } ================================================ FILE: agent/client/instance_exec.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "fmt" "io" "net/url" "strings" ) // InstanceExec executes command in the instance. func (cli *Client) InstanceExec(ctx context.Context, namespace, inferenceName, instance string, command []string, tty bool) (string, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) urlValues.Add("tty", fmt.Sprintf("%v", tty)) urlValues.Add("command", strings.Join(command, ",")) urlPath := fmt.Sprintf(gatewayInferInstanceExecControlPlanePath, inferenceName, instance) resp, err := cli.get(ctx, urlPath, urlValues, nil) defer ensureReaderClosed(resp) if err != nil { return "", wrapResponseError(err, resp, "instances with namespace", namespace) } res, err := io.ReadAll(resp.body) if err != nil { return "", wrapResponseError(err, resp, "instances with namespace", namespace) } return string(res), wrapResponseError(err, resp, "instances with namespace", namespace) } // InstanceExec executes command in the instance. func (cli *Client) InstanceExecTTY(ctx context.Context, namespace, inferenceName, instance string, command []string, ) (HijackedResponse, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) urlValues.Add("tty", "true") urlValues.Add("command", strings.Join(command, ",")) urlPath := fmt.Sprintf(gatewayInferInstanceExecControlPlanePath, inferenceName, instance) resp, err := cli.websocket(ctx, urlPath, urlValues, nil) if err != nil { return HijackedResponse{}, wrapResponseError(err, serverResponse{}, "instances with namespace", namespace) } return resp, nil } ================================================ FILE: agent/client/instance_list.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "encoding/json" "fmt" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // InstanceList lists the deployment instances. func (cli *Client) InstanceList(ctx context.Context, namespace, inferenceName string) ([]types.InferenceDeploymentInstance, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) urlPath := fmt.Sprintf(gatewayInferInstanceControlPlanePath, inferenceName) resp, err := cli.get(ctx, urlPath, urlValues, nil) defer ensureReaderClosed(resp) if err != nil { return nil, wrapResponseError(err, resp, "instances with namespace", namespace) } var instances []types.InferenceDeploymentInstance err = json.NewDecoder(resp.body).Decode(&instances) return instances, wrapResponseError(err, resp, "instances with namespace", namespace) } ================================================ FILE: agent/client/log.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "bufio" "context" "encoding/json" "fmt" "net/url" "strings" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" ) const LogBufferSize = 128 // DeploymentLogGet gets the deployment logs. func (cli *Client) DeploymentLogGet(ctx context.Context, namespace, name string, since string, tail int, end string, follow bool) ( <-chan types.Message, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) urlValues.Add("name", name) if since != "" { urlValues.Add("since", since) } if end != "" { urlValues.Add("end", end) } if tail != 0 { urlValues.Add("tail", fmt.Sprintf("%d", tail)) } if follow { urlValues.Add("follow", "true") } resp, err := cli.get(ctx, "/system/logs/inference", urlValues, nil) if err != nil { return nil, wrapResponseError(err, resp, "deployment logs", name) } stream := make(chan types.Message, LogBufferSize) var log types.Message scanner := bufio.NewScanner(resp.body) go func() { defer ensureReaderClosed(resp) defer close(stream) for scanner.Scan() { err = json.Unmarshal(scanner.Bytes(), &log) if err != nil { logrus.Warnf("failed to decode %s log: %v | %s | [%s]", name, err, scanner.Text(), scanner.Err()) return // continue } stream <- log } }() return stream, err } func (cli *Client) BuildLogGet(ctx context.Context, namespace, name, since string, tail int) ([]types.Message, error) { urlValues := url.Values{} urlValues.Add("namespace", namespace) urlValues.Add("name", name) if since != "" { urlValues.Add("since", since) } if tail != 0 { urlValues.Add("tail", fmt.Sprintf("%d", tail)) } resp, err := cli.get(ctx, "/system/logs/build", urlValues, nil) defer ensureReaderClosed(resp) if err != nil { return nil, wrapResponseError(err, resp, "build logs", name) } var log types.Message logs := []types.Message{} scanner := bufio.NewScanner(resp.body) for scanner.Scan() { err = json.NewDecoder(strings.NewReader(scanner.Text())).Decode(&log) if err != nil { return nil, wrapResponseError(err, resp, "build logs", name) } logs = append(logs, log) } return logs, err } ================================================ FILE: agent/client/modelz_cloud.go ================================================ package client import ( "context" "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/consts" "k8s.io/apimachinery/pkg/util/wait" ) func (cli *Client) WaitForAPIServerReady() error { err := wait.PollImmediateWithContext(context.Background(), time.Second, consts.DefaultAPIServerReadyTimeout, func(ctx context.Context) (bool, error) { err, healthStatus := cli.waitForAPIServerReady(ctx) if err != nil || healthStatus != http.StatusOK { logrus.Warn("APIServer isn't ready yet, Waiting a little while.") return false, err } return true, nil }) if err != nil { return fmt.Errorf("failed to wait for apiserver ready, %v", err) } return nil } func (cli *Client) waitForAPIServerReady(ctx context.Context) (error, int) { urlValues := url.Values{} resp, err := cli.get(ctx, "/healthz", urlValues, nil) if err != nil { return wrapResponseError(err, resp, "check apiserver is ready", ""), resp.statusCode } defer ensureReaderClosed(resp) return nil, resp.statusCode } func (cli *Client) RegisterAgent(ctx context.Context, token string, cluster *types.ManagedCluster) error { urlValues := url.Values{} agentToken, err := ParseAgentToken(token) if err != nil { return err } urlPath := fmt.Sprintf(modelzCloudClusterWithUserControlPlanePath, agentToken.UID) headers := make(map[string][]string) headers["Authorization"] = []string{"Bearer " + agentToken.Token} cluster.Name = agentToken.ClusterName resp, err := cli.post(ctx, urlPath, urlValues, cluster, headers) if err != nil { return wrapResponseError(err, resp, "register agent to modelz cloud", agentToken.UID) } defer ensureReaderClosed(resp) err = json.NewDecoder(resp.body).Decode(&cluster) if err != nil { return err } return nil } func (cli *Client) UpdateAgentStatus(ctx context.Context, apiServerReady <-chan struct{}, token string, cluster types.ManagedCluster) error { <-apiServerReady urlValues := url.Values{} agentToken, err := ParseAgentToken(token) if err != nil { return err } urlPath := fmt.Sprintf(modelzCloudClusterControlPlanePath, agentToken.UID, cluster.ID) headers := make(map[string][]string) headers["Authorization"] = []string{"Bearer " + agentToken.Token} resp, err := cli.put(ctx, urlPath, urlValues, cluster, headers) if err != nil { return wrapResponseError(err, resp, "update agent status to modelz cloud", agentToken.UID) } defer ensureReaderClosed(resp) if resp.statusCode == 200 { return nil } return fmt.Errorf("failed to update agent status to modelz cloud, status code: %d", resp.statusCode) } func (cli *Client) GetAPIKeys(ctx context.Context, apiServerReady <-chan struct{}, token string, cluster string) (types.APIKeyMap, error) { <-apiServerReady urlValues := url.Values{} agentToken, err := ParseAgentToken(token) keys := types.APIKeyMap{} if err != nil { return keys, err } headers := make(map[string][]string) headers["Authorization"] = []string{"Bearer " + agentToken.Token} urlPath := fmt.Sprintf(modelzCloudClusterAPIKeyControlPlanePath, agentToken.UID, cluster) resp, err := cli.get(ctx, urlPath, urlValues, headers) if err != nil { return keys, wrapResponseError(err, resp, "get api keys from modelz cloud", agentToken.UID) } defer ensureReaderClosed(resp) err = json.NewDecoder(resp.body).Decode(&keys) if err != nil { return keys, err } return keys, nil } func (cli *Client) GetNamespaces(ctx context.Context, apiServerReady <-chan struct{}, token string, cluster string) (types.NamespaceList, error) { <-apiServerReady urlValues := url.Values{} agentToken, err := ParseAgentToken(token) ns := types.NamespaceList{} if err != nil { return ns, err } urlValues.Add("login_name", agentToken.UID) headers := make(map[string][]string) headers["Authorization"] = []string{"Bearer " + agentToken.Token} resp, err := cli.get(ctx, fmt.Sprintf(modelzCloudClusterNamespaceControlPlanePath, agentToken.UID, cluster), urlValues, headers) if err != nil { return ns, wrapResponseError(err, resp, "get namespaces from modelz cloud", agentToken.UID) } defer ensureReaderClosed(resp) err = json.NewDecoder(resp.body).Decode(&ns) if err != nil { return ns, err } ns.Items = append(ns.Items, GetNamespaceByUserID(agentToken.UID)) return ns, nil } func (cli *Client) GetUIDFromDeploymentID(ctx context.Context, token string, cluster string, deployment string) (string, error) { urlValues := url.Values{} agentToken, err := ParseAgentToken(token) if err != nil { return "", err } headers := make(map[string][]string) headers["Authorization"] = []string{"Bearer " + agentToken.Token} urlPath := fmt.Sprintf(modelzCloudClusterDeploymentControlPlanePath, agentToken.UID, cluster, deployment) resp, err := cli.get(ctx, urlPath, urlValues, headers) if err != nil { return "", err } defer ensureReaderClosed(resp) var uid string err = json.NewDecoder(resp.body).Decode(&uid) if err != nil { return "", err } if resp.statusCode == 200 { return uid, nil } return "", fmt.Errorf("failed to get uid from deployment id, status code: %d", resp.statusCode) } func (cli *Client) CreateDeploymentEvent(ctx context.Context, token string, event types.DeploymentEvent) error { urlValues := url.Values{} agentToken, err := ParseAgentToken(token) if err != nil { return err } headers := make(map[string][]string) headers["Authorization"] = []string{"Bearer " + agentToken.Token} urlPath := fmt.Sprintf(modelzCloudClusterDeploymentEventControlPlanePath, agentToken.UID, agentToken.ClusterName, event.DeploymentID) resp, err := cli.post(ctx, urlPath, urlValues, event, headers) if err != nil { return err } defer ensureReaderClosed(resp) if resp.statusCode == http.StatusCreated { return nil } return fmt.Errorf("failed to create deployment event, status code: %d", resp.statusCode) } ================================================ FILE: agent/client/namespace_create.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // NamespaceCreate creates the namespace. func (cli *Client) NamespaceCreate(ctx context.Context, namespace string) error { req := types.NamespaceRequest{ Name: namespace, } urlValues := url.Values{} resp, err := cli.post(ctx, gatewayNamespaceControlPlanePath, urlValues, req, nil) defer ensureReaderClosed(resp) return wrapResponseError(err, resp, "namespace", namespace) } ================================================ FILE: agent/client/namespace_delete.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // NamespaceDelete deletes the namespace. func (cli *Client) NamespaceDelete(ctx context.Context, namespace string) error { req := types.NamespaceRequest{ Name: namespace, } urlValues := url.Values{} resp, err := cli.delete(ctx, gatewayNamespaceControlPlanePath, urlValues, req, nil) defer ensureReaderClosed(resp) return wrapResponseError(err, resp, "namespace", namespace) } ================================================ FILE: agent/client/options.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "net" "net/http" "os" "path/filepath" "time" "github.com/cockroachdb/errors" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" ) // Opt is a configuration option to initialize a client type Opt func(*Client) error // FromEnv configures the client with values from environment variables. // // FromEnv uses the following environment variables: // // ENVD_SERVER_HOST (EnvOverrideHost) to set the URL to the docker server. // // ENVD_SERVER_CERT_PATH (EnvOverrideCertPath) to specify the directory from which to // load the TLS certificates (ca.pem, cert.pem, key.pem). // // ENVD_SERVER_TLS_VERIFY (EnvTLSVerify) to enable or disable TLS verification (off by // default). func FromEnv(c *Client) error { // TODO(gaocegege): Support: // ENVD_SERVER_API_VERSION (EnvOverrideAPIVersion) to set the version of the API to // use, leave empty for latest. // ops := []Opt{ WithTLSClientConfigFromEnv(), WithHostFromEnv(), } for _, op := range ops { if err := op(c); err != nil { return err } } return nil } // WithDialContext applies the dialer to the client transport. This can be // used to set the Timeout and KeepAlive settings of the client. func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Opt { return func(c *Client) error { if transport, ok := c.client.Transport.(*http.Transport); ok { transport.DialContext = dialContext return nil } return errors.Errorf("cannot apply dialer to transport: %T", c.client.Transport) } } // WithHost overrides the client host with the specified one. func WithHost(host string) Opt { return func(c *Client) error { hostURL, err := ParseHostURL(host) if err != nil { return err } c.host = host c.proto = hostURL.Scheme c.addr = hostURL.Host c.basePath = hostURL.Path if transport, ok := c.client.Transport.(*http.Transport); ok { return sockets.ConfigureTransport(transport, c.proto, c.addr) } return errors.Errorf("cannot apply host to transport: %T", c.client.Transport) } } // WithHostFromEnv overrides the client host with the host specified in the // DOCKER_HOST (EnvOverrideHost) environment variable. If DOCKER_HOST is not set, // or set to an empty value, the host is not modified. func WithHostFromEnv() Opt { return func(c *Client) error { if host := os.Getenv(EnvOverrideHost); host != "" { return WithHost(host)(c) } return nil } } // WithHTTPClient overrides the client http client with the specified one func WithHTTPClient(client *http.Client) Opt { return func(c *Client) error { if client != nil { c.client = client } return nil } } // WithTimeout configures the time limit for requests made by the HTTP client func WithTimeout(timeout time.Duration) Opt { return func(c *Client) error { c.client.Timeout = timeout return nil } } // WithHTTPHeaders overrides the client default http headers func WithHTTPHeaders(headers map[string]string) Opt { return func(c *Client) error { c.customHTTPHeaders = headers return nil } } // WithScheme overrides the client scheme with the specified one func WithScheme(scheme string) Opt { return func(c *Client) error { c.scheme = scheme return nil } } // WithTLSClientConfig applies a tls config to the client transport. func WithTLSClientConfig(cacertPath, certPath, keyPath string) Opt { return func(c *Client) error { opts := tlsconfig.Options{ CAFile: cacertPath, CertFile: certPath, KeyFile: keyPath, ExclusiveRootPools: true, } config, err := tlsconfig.Client(opts) if err != nil { return errors.Wrap(err, "failed to create tls config") } if transport, ok := c.client.Transport.(*http.Transport); ok { transport.TLSClientConfig = config return nil } return errors.Errorf("cannot apply tls config to transport: %T", c.client.Transport) } } // WithTLSClientConfigFromEnv configures the client's TLS settings with the // settings in the DOCKER_CERT_PATH and DOCKER_TLS_VERIFY environment variables. // If DOCKER_CERT_PATH is not set or empty, TLS configuration is not modified. // // WithTLSClientConfigFromEnv uses the following environment variables: // // DOCKER_CERT_PATH (EnvOverrideCertPath) to specify the directory from which to // load the TLS certificates (ca.pem, cert.pem, key.pem). // // DOCKER_TLS_VERIFY (EnvTLSVerify) to enable or disable TLS verification (off by // default). func WithTLSClientConfigFromEnv() Opt { return func(c *Client) error { dockerCertPath := os.Getenv(EnvOverrideCertPath) if dockerCertPath == "" { return nil } options := tlsconfig.Options{ CAFile: filepath.Join(dockerCertPath, "ca.pem"), CertFile: filepath.Join(dockerCertPath, "cert.pem"), KeyFile: filepath.Join(dockerCertPath, "key.pem"), InsecureSkipVerify: os.Getenv(EnvTLSVerify) == "", } tlsc, err := tlsconfig.Client(options) if err != nil { return err } c.client = &http.Client{ Transport: &http.Transport{TLSClientConfig: tlsc}, CheckRedirect: CheckRedirect, } return nil } } // WithVersion overrides the client version with the specified one. If an empty // version is specified, the value will be ignored to allow version negotiation. func WithVersion(version string) Opt { return func(c *Client) error { if version != "" { c.version = version c.manualOverride = true } return nil } } ================================================ FILE: agent/client/request.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "bytes" "context" "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "strings" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/errdefs" ) // serverResponse is a wrapper for http API responses. type serverResponse struct { body io.ReadCloser header http.Header statusCode int reqURL *url.URL } // head sends an http request to the docker API using the method HEAD. func (cli *Client) head(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { return cli.sendRequest(ctx, http.MethodHead, path, query, nil, headers) } // get sends an http request to the docker API using the method GET with a specific Go context. func (cli *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { return cli.sendRequest(ctx, http.MethodGet, path, query, nil, headers) } // post sends an http request to the docker API using the method POST with a specific Go context. func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { body, headers, err := encodeBody(obj, headers) if err != nil { return serverResponse{}, err } return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) } func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) } func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { body, headers, err := encodeBody(obj, headers) if err != nil { return serverResponse{}, err } return cli.sendRequest(ctx, http.MethodPut, path, query, body, headers) } // putRaw sends an http request to the docker API using the method PUT. func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) { return cli.sendRequest(ctx, http.MethodPut, path, query, body, headers) } // delete sends an http request to the docker API using the method DELETE. func (cli *Client) delete(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { body, headers, err := encodeBody(obj, headers) if err != nil { return serverResponse{}, err } return cli.sendRequest(ctx, http.MethodDelete, path, query, body, headers) } type headers map[string][]string func encodeBody(obj interface{}, headers headers) (io.Reader, headers, error) { if obj == nil { return nil, headers, nil } body, err := encodeData(obj) if err != nil { return nil, headers, err } if headers == nil { headers = make(map[string][]string) } headers["Content-Type"] = []string{"application/json"} return body, headers, nil } func (cli *Client) buildRequest(method, path string, body io.Reader, headers headers) (*http.Request, error) { expectedPayload := (method == http.MethodPost || method == http.MethodPut) if expectedPayload && body == nil { body = bytes.NewReader([]byte{}) } req, err := http.NewRequest(method, path, body) if err != nil { return nil, err } req = cli.addHeaders(req, headers) if cli.proto == "unix" || cli.proto == "npipe" { // For local communications, it doesn't matter what the host is. We just // need a valid and meaningful host name. req.Host = "modelz" } req.URL.Host = cli.addr req.URL.Scheme = cli.scheme if expectedPayload && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "text/plain") } logrus.Debugf("Sending HTTP request to %s\n", req.URL.String()) logrus.Debugf("Request Headers: %v\n", req.Header) logrus.Debugf("Request Body: %v\n", body) return req, nil } func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) { req, err := cli.buildRequest(method, cli.getAPIPath(ctx, path, query), body, headers) if err != nil { return serverResponse{}, errors.Wrap(err, "failed to build request") } resp, err := cli.doRequest(ctx, req) switch { case errors.Is(err, context.Canceled): return serverResponse{}, errdefs.Cancelled(err) case errors.Is(err, context.DeadlineExceeded): return serverResponse{}, errdefs.Deadline(err) case err == nil: err = cli.checkResponseErr(resp) } return resp, errdefs.FromStatusCode(err, resp.statusCode) } func (cli *Client) doRequest(ctx context.Context, req *http.Request) (serverResponse, error) { serverResp := serverResponse{statusCode: -1, reqURL: req.URL} req = req.WithContext(ctx) resp, err := cli.client.Do(req) if err != nil { if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) } if cli.scheme == "https" && strings.Contains(err.Error(), "bad certificate") { return serverResp, errors.Wrap(err, "the server probably has client authentication (--tlsverify) enabled; check your TLS client certification settings") } // Don't decorate context sentinel errors; users may be comparing to // them directly. if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return serverResp, err } if nErr, ok := err.(*url.Error); ok { if nErr, ok := nErr.Err.(*net.OpError); ok { if os.IsPermission(nErr.Err) { return serverResp, errors.Wrapf(err, "permission denied while trying to connect to the modelz agent server socket at %v", cli.host) } } } if err, ok := err.(net.Error); ok { if err.Timeout() { return serverResp, ErrorConnectionFailed(cli.host) } if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { return serverResp, ErrorConnectionFailed(cli.host) } } return serverResp, errors.Wrap(err, "error during connect") } if resp != nil { serverResp.statusCode = resp.StatusCode serverResp.body = resp.Body serverResp.header = resp.Header } return serverResp, nil } func (cli *Client) checkResponseErr(serverResp serverResponse) error { if serverResp.statusCode >= 200 && serverResp.statusCode < 400 { return nil } var body []byte var err error if serverResp.body != nil { bodyMax := 1 * 1024 * 1024 // 1 MiB bodyR := &io.LimitedReader{ R: serverResp.body, N: int64(bodyMax), } body, err = io.ReadAll(bodyR) if err != nil { return err } if bodyR.N == 0 { return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), bodyMax, serverResp.reqURL) } } if len(body) == 0 { return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), serverResp.reqURL) } errorMessage := strings.TrimSpace(string(body)) return errors.Wrap(errors.New(errorMessage), "Error response from gateway") } func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request { // Add CLI Config's HTTP Headers BEFORE we set the Docker headers // then the user can't change OUR headers for k, v := range cli.customHTTPHeaders { req.Header.Set(k, v) } for k, v := range headers { req.Header[http.CanonicalHeaderKey(k)] = v } return req } func encodeData(data interface{}) (*bytes.Buffer, error) { params := bytes.NewBuffer(nil) if data != nil { if err := json.NewEncoder(params).Encode(data); err != nil { return nil, err } } return params, nil } func ensureReaderClosed(response serverResponse) { if response.body != nil { // Drain up to 512 bytes and close the body to let the Transport reuse the connection io.CopyN(io.Discard, response.body, 512) response.body.Close() } } ================================================ FILE: agent/client/server_label_create.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "fmt" "net/url" "github.com/tensorchord/openmodelz/agent/api/types" ) // ServerLabelCreate create the labels for the servers. func (cli *Client) ServerLabelCreate(ctx context.Context, name string, labels map[string]string) error { req := types.ServerSpec{ Name: name, Labels: labels, } urlValues := url.Values{} resp, err := cli.post(ctx, fmt.Sprintf(gatewayServerLabelCreateControlPlanePath, name), urlValues, req, nil) defer ensureReaderClosed(resp) if err != nil { return wrapResponseError(err, resp, "server", name) } return wrapResponseError(err, resp, "server", name) } ================================================ FILE: agent/client/server_list.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "encoding/json" "github.com/tensorchord/openmodelz/agent/api/types" ) // ServerList lists the servers. func (cli *Client) ServerList(ctx context.Context) ([]types.Server, error) { resp, err := cli.get(ctx, gatewayServerControlPlanePath, nil, nil) defer ensureReaderClosed(resp) if err != nil { return nil, wrapResponseError(err, resp, "servers", "") } var servers []types.Server err = json.NewDecoder(resp.body).Decode(&servers) return servers, wrapResponseError(err, resp, "servers", "") } ================================================ FILE: agent/client/server_node_delete.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client import ( "context" "fmt" "net/url" ) // ServerLabelCreate create the labels for the servers. func (cli *Client) ServerNodeDelete(ctx context.Context, name string) error { urlValues := url.Values{} resp, err := cli.delete(ctx, fmt.Sprintf(gatewayServerNodeDeleteControlPlanePath, name), urlValues, nil, nil) defer ensureReaderClosed(resp) if err != nil { return wrapResponseError(err, resp, "server-delete", name) } return wrapResponseError(err, resp, "server-delete", name) } ================================================ FILE: agent/client/transport.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package client // import "github.com/docker/docker/client" import ( "crypto/tls" "net/http" ) // resolveTLSConfig attempts to resolve the TLS configuration from the // RoundTripper. func resolveTLSConfig(transport http.RoundTripper) *tls.Config { switch tr := transport.(type) { case *http.Transport: return tr.TLSClientConfig default: return nil } } ================================================ FILE: agent/client/utils.go ================================================ package client import ( "fmt" "strings" "github.com/cockroachdb/errors" "github.com/tensorchord/openmodelz/agent/api/types" ) const ( DefaultPrefix = "modelz-" ) func ParseAgentToken(token string) (types.AgentToken, error) { agentToken := types.AgentToken{} if token == "" { return agentToken, errors.New("agent token is empty") } strings := strings.Split(token, ":") if len(strings) != 3 { return agentToken, errors.New("invalid agent token") } agentToken.ClusterName = strings[0] agentToken.UID = strings[1] agentToken.Token = strings[2] return agentToken, nil } func GetNamespaceByUserID(uid string) string { return fmt.Sprintf("%s%s", DefaultPrefix, uid) } func GetUserIDFromNamespace(ns string) (string, error) { if len(ns) < 8 { return "", fmt.Errorf("namespace too short") } if ns[:len(DefaultPrefix)] != DefaultPrefix { return "", fmt.Errorf("namespace does not start with ") } return ns[7:], nil } ================================================ FILE: agent/cmd/agent/main.go ================================================ package main import ( "fmt" "os" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "github.com/tensorchord/openmodelz/agent/pkg/app" "github.com/tensorchord/openmodelz/agent/pkg/version" ) func run(args []string) error { cli.VersionPrinter = func(c *cli.Context) { fmt.Println(c.App.Name, version.Package, c.App.Version, version.Revision) } app := app.New() return app.Run(args) } func handleErr(err error) { if err == nil { return } logrus.Error(err) os.Exit(1) } // @title modelz cluster agent // @version v0.0.23 // @description modelz kubernetes cluster agent // @contact.name modelz support // @contact.url https://github.com/tensorchord/openmodelz // @contact.email modelz-support@tensorchord.ai // @host localhost:8081 // @BasePath / // @schemes http func main() { err := run(os.Args) handleErr(err) } ================================================ FILE: agent/errdefs/defs.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package errdefs // import "github.com/docker/docker/errdefs" // ErrNotFound signals that the requested object doesn't exist type ErrNotFound interface { NotFound() } // ErrInvalidParameter signals that the user input is invalid type ErrInvalidParameter interface { InvalidParameter() } // ErrConflict signals that some internal state conflicts with the requested action and can't be performed. // A change in state should be able to clear this error. type ErrConflict interface { Conflict() } // ErrUnauthorized is used to signify that the user is not authorized to perform a specific action type ErrUnauthorized interface { Unauthorized() } // ErrUnavailable signals that the requested action/subsystem is not available. type ErrUnavailable interface { Unavailable() } // ErrForbidden signals that the requested action cannot be performed under any circumstances. // When a ErrForbidden is returned, the caller should never retry the action. type ErrForbidden interface { Forbidden() } // ErrSystem signals that some internal error occurred. // An example of this would be a failed mount request. type ErrSystem interface { System() } // ErrNotModified signals that an action can't be performed because it's already in the desired state type ErrNotModified interface { NotModified() } // ErrNotImplemented signals that the requested action/feature is not implemented on the system as configured. type ErrNotImplemented interface { NotImplemented() } // ErrUnknown signals that the kind of error that occurred is not known. type ErrUnknown interface { Unknown() } // ErrCancelled signals that the action was cancelled. type ErrCancelled interface { Cancelled() } // ErrDeadline signals that the deadline was reached before the action completed. type ErrDeadline interface { DeadlineExceeded() } // ErrDataLoss indicates that data was lost or there is data corruption. type ErrDataLoss interface { DataLoss() } ================================================ FILE: agent/errdefs/doc.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // Package errdefs defines a set of error interfaces that packages should use for communicating classes of errors. // Errors that cross the package boundary should implement one (and only one) of these interfaces. // // Packages should not reference these interfaces directly, only implement them. // To check if a particular error implements one of these interfaces, there are helper // functions provided (e.g. `Is`) which can be used rather than asserting the interfaces directly. // If you must assert on these interfaces, be sure to check the causal chain (`err.Cause()`). package errdefs // import "github.com/docker/docker/errdefs" ================================================ FILE: agent/errdefs/helpers.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package errdefs // import "github.com/docker/docker/errdefs" import "context" type errNotFound struct{ error } func (errNotFound) NotFound() {} func (e errNotFound) Cause() error { return e.error } func (e errNotFound) Unwrap() error { return e.error } // NotFound is a helper to create an error of the class with the same name from any error type func NotFound(err error) error { if err == nil || IsNotFound(err) { return err } return errNotFound{err} } type errInvalidParameter struct{ error } func (errInvalidParameter) InvalidParameter() {} func (e errInvalidParameter) Cause() error { return e.error } func (e errInvalidParameter) Unwrap() error { return e.error } // InvalidParameter is a helper to create an error of the class with the same name from any error type func InvalidParameter(err error) error { if err == nil || IsInvalidParameter(err) { return err } return errInvalidParameter{err} } type errConflict struct{ error } func (errConflict) Conflict() {} func (e errConflict) Cause() error { return e.error } func (e errConflict) Unwrap() error { return e.error } // Conflict is a helper to create an error of the class with the same name from any error type func Conflict(err error) error { if err == nil || IsConflict(err) { return err } return errConflict{err} } type errUnauthorized struct{ error } func (errUnauthorized) Unauthorized() {} func (e errUnauthorized) Cause() error { return e.error } func (e errUnauthorized) Unwrap() error { return e.error } // Unauthorized is a helper to create an error of the class with the same name from any error type func Unauthorized(err error) error { if err == nil || IsUnauthorized(err) { return err } return errUnauthorized{err} } type errUnavailable struct{ error } func (errUnavailable) Unavailable() {} func (e errUnavailable) Cause() error { return e.error } func (e errUnavailable) Unwrap() error { return e.error } // Unavailable is a helper to create an error of the class with the same name from any error type func Unavailable(err error) error { if err == nil || IsUnavailable(err) { return err } return errUnavailable{err} } type errForbidden struct{ error } func (errForbidden) Forbidden() {} func (e errForbidden) Cause() error { return e.error } func (e errForbidden) Unwrap() error { return e.error } // Forbidden is a helper to create an error of the class with the same name from any error type func Forbidden(err error) error { if err == nil || IsForbidden(err) { return err } return errForbidden{err} } type errSystem struct{ error } func (errSystem) System() {} func (e errSystem) Cause() error { return e.error } func (e errSystem) Unwrap() error { return e.error } // System is a helper to create an error of the class with the same name from any error type func System(err error) error { if err == nil || IsSystem(err) { return err } return errSystem{err} } type errNotModified struct{ error } func (errNotModified) NotModified() {} func (e errNotModified) Cause() error { return e.error } func (e errNotModified) Unwrap() error { return e.error } // NotModified is a helper to create an error of the class with the same name from any error type func NotModified(err error) error { if err == nil || IsNotModified(err) { return err } return errNotModified{err} } type errNotImplemented struct{ error } func (errNotImplemented) NotImplemented() {} func (e errNotImplemented) Cause() error { return e.error } func (e errNotImplemented) Unwrap() error { return e.error } // NotImplemented is a helper to create an error of the class with the same name from any error type func NotImplemented(err error) error { if err == nil || IsNotImplemented(err) { return err } return errNotImplemented{err} } type errUnknown struct{ error } func (errUnknown) Unknown() {} func (e errUnknown) Cause() error { return e.error } func (e errUnknown) Unwrap() error { return e.error } // Unknown is a helper to create an error of the class with the same name from any error type func Unknown(err error) error { if err == nil || IsUnknown(err) { return err } return errUnknown{err} } type errCancelled struct{ error } func (errCancelled) Cancelled() {} func (e errCancelled) Cause() error { return e.error } func (e errCancelled) Unwrap() error { return e.error } // Cancelled is a helper to create an error of the class with the same name from any error type func Cancelled(err error) error { if err == nil || IsCancelled(err) { return err } return errCancelled{err} } type errDeadline struct{ error } func (errDeadline) DeadlineExceeded() {} func (e errDeadline) Cause() error { return e.error } func (e errDeadline) Unwrap() error { return e.error } // Deadline is a helper to create an error of the class with the same name from any error type func Deadline(err error) error { if err == nil || IsDeadline(err) { return err } return errDeadline{err} } type errDataLoss struct{ error } func (errDataLoss) DataLoss() {} func (e errDataLoss) Cause() error { return e.error } func (e errDataLoss) Unwrap() error { return e.error } // DataLoss is a helper to create an error of the class with the same name from any error type func DataLoss(err error) error { if err == nil || IsDataLoss(err) { return err } return errDataLoss{err} } // FromContext returns the error class from the passed in context func FromContext(ctx context.Context) error { e := ctx.Err() if e == nil { return nil } if e == context.Canceled { return Cancelled(e) } if e == context.DeadlineExceeded { return Deadline(e) } return Unknown(e) } ================================================ FILE: agent/errdefs/http_helpers.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package errdefs // import "github.com/docker/docker/errdefs" import ( "net/http" "github.com/sirupsen/logrus" ) // FromStatusCode creates an errdef error, based on the provided HTTP status-code func FromStatusCode(err error, statusCode int) error { if err == nil { return err } switch statusCode { case http.StatusNotFound: err = NotFound(err) case http.StatusBadRequest: err = InvalidParameter(err) case http.StatusConflict: err = Conflict(err) case http.StatusUnauthorized: err = Unauthorized(err) case http.StatusServiceUnavailable: err = Unavailable(err) case http.StatusForbidden: err = Forbidden(err) case http.StatusNotModified: err = NotModified(err) case http.StatusNotImplemented: err = NotImplemented(err) case http.StatusInternalServerError: if !IsSystem(err) && !IsUnknown(err) && !IsDataLoss(err) && !IsDeadline(err) && !IsCancelled(err) { err = System(err) } default: logrus.WithError(err).WithFields(logrus.Fields{ "module": "api", "status_code": statusCode, }).Debug("FIXME: Got an status-code for which error does not match any expected type!!!") switch { case statusCode >= 200 && statusCode < 400: // it's a client error case statusCode >= 400 && statusCode < 500: err = InvalidParameter(err) case statusCode >= 500 && statusCode < 600: err = System(err) default: err = Unknown(err) } } return err } ================================================ FILE: agent/errdefs/is.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package errdefs // import "github.com/docker/docker/errdefs" type causer interface { Cause() error } func getImplementer(err error) error { switch e := err.(type) { case ErrNotFound, ErrInvalidParameter, ErrConflict, ErrUnauthorized, ErrUnavailable, ErrForbidden, ErrSystem, ErrNotModified, ErrNotImplemented, ErrCancelled, ErrDeadline, ErrDataLoss, ErrUnknown: return err case causer: return getImplementer(e.Cause()) default: return err } } // IsNotFound returns if the passed in error is an ErrNotFound func IsNotFound(err error) bool { _, ok := getImplementer(err).(ErrNotFound) return ok } // IsInvalidParameter returns if the passed in error is an ErrInvalidParameter func IsInvalidParameter(err error) bool { _, ok := getImplementer(err).(ErrInvalidParameter) return ok } // IsConflict returns if the passed in error is an ErrConflict func IsConflict(err error) bool { _, ok := getImplementer(err).(ErrConflict) return ok } // IsUnauthorized returns if the passed in error is an ErrUnauthorized func IsUnauthorized(err error) bool { _, ok := getImplementer(err).(ErrUnauthorized) return ok } // IsUnavailable returns if the passed in error is an ErrUnavailable func IsUnavailable(err error) bool { _, ok := getImplementer(err).(ErrUnavailable) return ok } // IsForbidden returns if the passed in error is an ErrForbidden func IsForbidden(err error) bool { _, ok := getImplementer(err).(ErrForbidden) return ok } // IsSystem returns if the passed in error is an ErrSystem func IsSystem(err error) bool { _, ok := getImplementer(err).(ErrSystem) return ok } // IsNotModified returns if the passed in error is a NotModified error func IsNotModified(err error) bool { _, ok := getImplementer(err).(ErrNotModified) return ok } // IsNotImplemented returns if the passed in error is an ErrNotImplemented func IsNotImplemented(err error) bool { _, ok := getImplementer(err).(ErrNotImplemented) return ok } // IsUnknown returns if the passed in error is an ErrUnknown func IsUnknown(err error) bool { _, ok := getImplementer(err).(ErrUnknown) return ok } // IsCancelled returns if the passed in error is an ErrCancelled func IsCancelled(err error) bool { _, ok := getImplementer(err).(ErrCancelled) return ok } // IsDeadline returns if the passed in error is an ErrDeadline func IsDeadline(err error) bool { _, ok := getImplementer(err).(ErrDeadline) return ok } // IsDataLoss returns if the passed in error is an ErrDataLoss func IsDataLoss(err error) bool { _, ok := getImplementer(err).(ErrDataLoss) return ok } ================================================ FILE: agent/pkg/app/config.go ================================================ package app import ( "github.com/urfave/cli/v2" "github.com/tensorchord/openmodelz/agent/pkg/config" ) func configFromCLI(c *cli.Context) config.Config { cfg := config.New() // server cfg.Server.Dev = c.Bool(flagDev) cfg.Server.ServerPort = c.Int(flagServerPort) cfg.Server.ReadTimeout = c.Duration(flagServerReadTimeout) cfg.Server.WriteTimeout = c.Duration(flagServerWriteTimeout) // kubernetes cfg.KubeConfig.Kubeconfig = c.String(flagKubeConfig) cfg.KubeConfig.MasterURL = c.String(flagMasterURL) cfg.KubeConfig.QPS = c.Int(flagQPS) cfg.KubeConfig.Burst = c.Int(flagBurst) cfg.KubeConfig.ResyncPeriod = c.Duration(flagResyncPeriod) // inference ingress cfg.Ingress.IngressEnabled = c.Bool(flagIngressEnabled) cfg.Ingress.Domain = c.String(flagIngressDomain) cfg.Ingress.AnyIPToDomain = c.Bool(flagIngressAnyIPToDomain) cfg.Ingress.Namespace = c.String(flagIngressNamespace) cfg.Ingress.TLSEnabled = c.Bool(flagIngressTLSEnabled) // inference cfg.Inference.LogTimeout = c.Duration(flagInferenceLogTimeout) cfg.Inference.CacheTTL = c.Duration(flagInferenceCacheTTL) // build cfg.Build.BuildEnabled = c.Bool(flagBuildEnabled) cfg.Build.BuilderImage = c.String(flagBuilderImage) cfg.Build.BuildkitdAddress = c.String(flagBuildkitdAddress) cfg.Build.BuildCtlBin = c.String(flagBuildCtlBin) cfg.Build.BuildRegistry = c.String(flagBuildRegistry) cfg.Build.BuildRegistryToken = c.String(flagBuildRegistryToken) cfg.Build.BuildImagePullSecret = c.String(flagBuildImagePullSecret) // loki cfg.Logs.Timeout = c.Duration(flagLogsTimeout) cfg.Logs.LokiURL = c.String(flagLogsLokiURL) cfg.Logs.LokiUser = c.String(flaglogsLokiUser) cfg.Logs.LokiToken = c.String(flagLogsLokiToken) // metrics cfg.Metrics.PollingInterval = c.Duration(flagMetricsPollingInterval) cfg.Metrics.ServerPort = c.Int(flagMetricsPort) cfg.Metrics.PrometheusHost = c.String(flagMetricsPrometheusHost) cfg.Metrics.PrometheusPort = c.Int(flagMetricsPrometheusPort) // modelz cloud cfg.ModelZCloud.Enabled = c.Bool(flagModelZCloudEnabled) cfg.ModelZCloud.URL = c.String(flagModelZCloudURL) cfg.ModelZCloud.AgentToken = c.String(flagModelZCloudAgentToken) cfg.ModelZCloud.HeartbeatInterval = c.Duration(flagModelZCloudAgentHeartbeatInterval) cfg.ModelZCloud.Region = c.String(flagModelZCloudRegion) cfg.ModelZCloud.UnifiedAPIKey = c.String(flagModelZCloudUnifiedAPIKey) cfg.ModelZCloud.UpstreamTimeout = c.Duration(flagModelZCloudUpstreamTimeout) cfg.ModelZCloud.MaxIdleConnections = c.Int(flagModelZCloudMaxIdleConnections) cfg.ModelZCloud.MaxIdleConnectionsPerHost = c.Int(flagModelZCloudMaxIdleConnectionsPerHost) cfg.ModelZCloud.EventEnabled = c.Bool(flagModelZCloudEventEnabled) return cfg } ================================================ FILE: agent/pkg/app/root.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package app import ( "time" "github.com/cockroachdb/errors" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" cli "github.com/urfave/cli/v2" "github.com/tensorchord/openmodelz/agent/pkg/server" "github.com/tensorchord/openmodelz/agent/pkg/version" ) const ( flagDebug = "debug" flagDev = "dev" // server flagServerPort = "server-port" flagServerReadTimeout = "server-read-timeout" flagServerWriteTimeout = "server-write-timeout" // kubernetes flagMasterURL = "master-url" flagKubeConfig = "kube-config" flagQPS = "kube-qps" flagBurst = "kube-burst" flagResyncPeriod = "kube-resync-period" // inference ingress flagIngressEnabled = "ingress-enabled" flagIngressDomain = "ingress-domain" flagIngressNamespace = "ingress-namespace" flagIngressAnyIPToDomain = "ingress-any-ip-to-domain" flagIngressTLSEnabled = "ingress-tls-enabled" // inference flagInferenceLogTimeout = "inference-log-timeout" flagInferenceCacheTTL = "inference-cache-ttl" // build flagBuildEnabled = "build-enabled" flagBuilderImage = "builder-image" flagBuildkitdAddress = "buildkitd-address" flagBuildCtlBin = "buildctl-bin" flagBuildRegistry = "build-registry" flagBuildRegistryToken = "build-registry-token" flagBuildImagePullSecret = "build-image-pull-secret" // metrics flagMetricsPollingInterval = "metrics-polling-interval" flagMetricsPort = "metrics-port" flagMetricsPrometheusHost = "metrics-prometheus-host" flagMetricsPrometheusPort = "metrics-prometheus-port" // logs flagLogsTimeout = "logs-timeout" flagLogsLokiURL = "logs-loki-url" flaglogsLokiUser = "logs-loki-user" flagLogsLokiToken = "logs-loki-token" // modelz cloud flagModelZCloudEnabled = "modelz-cloud-enabled" flagModelZCloudURL = "modelz-cloud-url" flagModelZCloudAgentToken = "modelz-cloud-agent-token" flagModelZCloudAgentHeartbeatInterval = "modelz-cloud-agent-heartbeat-interval" flagModelZCloudRegion = "modelz-cloud-region" flagModelZCloudUnifiedAPIKey = "modelz-cloud-unified-api-key" flagModelZCloudUpstreamTimeout = "modelz-cloud-upstream-timeout" flagModelZCloudMaxIdleConnections = "modelz-cloud-max-idle-connections" flagModelZCloudMaxIdleConnectionsPerHost = "modelz-cloud-max-idle-connections-per-host" flagModelZCloudEventEnabled = "modelz-cloud-event-enabled" ) type App struct { *cli.App } func New() App { internalApp := cli.NewApp() internalApp.EnableBashCompletion = true internalApp.Name = "modelz-agent" internalApp.Usage = "Cluster agent for modelz" internalApp.HideHelpCommand = true internalApp.HideVersion = false internalApp.Version = version.GetVersion().String() internalApp.Flags = []cli.Flag{ &cli.BoolFlag{ Name: flagDebug, Usage: "enable debug output in logs", }, &cli.BoolFlag{ Name: flagDev, Usage: "enable development mode", }, &cli.IntFlag{ Name: flagServerPort, Value: 8080, Usage: "port to listen on", EnvVars: []string{"MODELZ_AGENT_SERVER_PORT"}, Aliases: []string{"p"}, }, &cli.DurationFlag{ Name: flagServerReadTimeout, Usage: "maximum duration before timing out read of the request, " + "including the body", Value: 305 * time.Second, EnvVars: []string{"MODELZ_AGENT_SERVER_READ_TIMEOUT"}, Aliases: []string{"srt"}, }, &cli.DurationFlag{ Name: flagServerWriteTimeout, Usage: "maximum duration before timing out write of the response, " + "including the body", Value: 305 * time.Second, EnvVars: []string{"MODELZ_AGENT_SERVER_WRITE_TIMEOUT"}, Aliases: []string{"swt"}, }, &cli.StringFlag{ Name: flagMasterURL, Usage: "URL to master for kubernetes cluster", EnvVars: []string{"MODELZ_AGENT_MASTER_URL"}, Aliases: []string{"mu"}, }, &cli.StringFlag{ Name: flagKubeConfig, Usage: "Path to kubeconfig file. If not provided, will use in-cluster config", EnvVars: []string{"MODELZ_AGENT_KUBE_CONFIG"}, Aliases: []string{"kc"}, }, &cli.IntFlag{ Name: flagQPS, Usage: "QPS for kubernetes client", Value: 100, EnvVars: []string{"MODELZ_AGENT_KUBE_QPS"}, Aliases: []string{"kq"}, }, &cli.IntFlag{ Name: flagBurst, Value: 250, Usage: "Burst for kubernetes client", EnvVars: []string{"MODELZ_AGENT_KUBE_BURST"}, Aliases: []string{"kb"}, }, &cli.DurationFlag{ Name: flagResyncPeriod, Value: time.Hour, Usage: "Resync period for kubernetes client", EnvVars: []string{"MODELZ_AGENT_KUBE_RESYNC_PERIOD"}, Aliases: []string{"kr"}, }, &cli.BoolFlag{ Name: flagIngressEnabled, Usage: "Enable inference ingress. " + "If enabled, the agent will create ingress for each inference", Value: false, EnvVars: []string{"MODELZ_AGENT_INGRESS_ENABLED"}, Aliases: []string{"ie"}, }, &cli.StringFlag{ Name: flagIngressDomain, Usage: "Domain for inference ingress", Value: "cloud.modelz.dev", EnvVars: []string{"MODELZ_AGENT_INGRESS_DOMAIN"}, Aliases: []string{"id"}, }, &cli.StringFlag{ Name: flagIngressNamespace, Usage: "Namespace for inference ingress", Value: "default", EnvVars: []string{"MODELZ_AGENT_INGRESS_NAMESPACE"}, Aliases: []string{"in"}, }, &cli.BoolFlag{ Name: flagIngressAnyIPToDomain, Usage: "Enable any ip to domain. " + "If enabled, the agent will create ingress for each inference", Value: false, EnvVars: []string{"MODELZ_AGENT_INGRESS_ANY_IP_TO_DOMAIN"}, Aliases: []string{"iad"}, }, &cli.BoolFlag{ Name: flagIngressTLSEnabled, Usage: "Enable TLS for inference ingress. ", Value: true, EnvVars: []string{"MODELZ_AGENT_INGRESS_TLS_ENABLED"}, Aliases: []string{"it"}, }, &cli.DurationFlag{ Name: flagInferenceLogTimeout, Usage: "Timeout for inference log streaming. " + "If the inference log has not been updated in this time, " + "the connection will be closed.", Value: time.Minute, EnvVars: []string{"MODELZ_AGENT_INFERENCE_LOG_TIMEOUT"}, Aliases: []string{"ilt"}, }, &cli.DurationFlag{ Name: flagInferenceCacheTTL, Usage: "Time to live for inference cache. ", Value: time.Millisecond * 500, EnvVars: []string{"MODELZ_AGENT_INFERENCE_CACHE_TTL"}, Aliases: []string{"ict"}, }, &cli.BoolFlag{ Name: flagBuildEnabled, Hidden: true, Usage: "Enable model build. " + "If enabled, the agent will build inference server image", Value: false, EnvVars: []string{"MODELZ_AGENT_BUILD_ENABLED"}, Aliases: []string{"be"}, }, &cli.StringFlag{ Name: flagBuilderImage, Hidden: true, Usage: "Image to use for building models. " + "Must be a valid docker image reference.", EnvVars: []string{"MODELZ_AGENT_BUILDER_IMAGE"}, Aliases: []string{"bi"}, }, &cli.StringFlag{ Name: flagBuildkitdAddress, Hidden: true, Usage: "Address of buildkitd server. " + "Must be a valid tcp address.", EnvVars: []string{"MODELZ_AGENT_BUILDKITD_ADDRESS"}, Aliases: []string{"ba"}, }, &cli.StringFlag{ Name: flagBuildCtlBin, Hidden: true, Usage: "Path to buildctl binary. " + "Must be a valid path to a binary.", EnvVars: []string{"MODELZ_AGENT_BUILDCTL_BIN"}, Aliases: []string{"bb"}, }, &cli.StringFlag{ Name: flagBuildRegistry, Hidden: true, Usage: "Registry to use for building models. ", EnvVars: []string{"MODELZ_AGENT_BUILD_REGISTRY"}, Aliases: []string{"br"}, }, &cli.StringFlag{ Name: flagBuildRegistryToken, Hidden: true, Usage: "Token to use for building models. ", EnvVars: []string{"MODELZ_AGENT_BUILD_REGISTRY_TOKEN"}, Aliases: []string{"bt"}, }, &cli.StringFlag{ Name: flagBuildImagePullSecret, Hidden: true, Usage: "Image pull secret to use for building models.", EnvVars: []string{"MODELZ_AGENT_BUILD_IMAGE_PULL_SECRET"}, Aliases: []string{"bp"}, Value: "dockerhub-secret", }, &cli.DurationFlag{ Name: flagMetricsPollingInterval, Usage: "Interval to poll metrics from kubernetes", Value: time.Second * 5, EnvVars: []string{"MODELZ_AGENT_METRICS_POLLING_INTERVAL"}, Aliases: []string{"mpi"}, }, &cli.IntFlag{ Name: flagMetricsPort, Usage: "Port to expose metrics on. ", Value: 8082, EnvVars: []string{"MODELZ_AGENT_METRICS_PORT"}, Aliases: []string{"mp"}, }, &cli.StringFlag{ Name: flagMetricsPrometheusHost, Value: "localhost", Usage: "Host to expose prometheus metrics on. ", EnvVars: []string{"MODELZ_AGENT_METRICS_PROMETHEUS_HOST"}, Aliases: []string{"mph"}, }, &cli.IntFlag{ Name: flagMetricsPrometheusPort, Usage: "Port to expose prometheus metrics on. ", Value: 9090, EnvVars: []string{"MODELZ_AGENT_METRICS_PROMETHEUS_PORT"}, Aliases: []string{"mpp"}, }, &cli.DurationFlag{ Name: flagLogsTimeout, Usage: "request timeout to query the logs", Value: time.Second * 5, EnvVars: []string{"MODELZ_AGENT_LOGS_TIMEOUT"}, }, &cli.StringFlag{ Name: flagLogsLokiURL, Hidden: true, Usage: "Loki service URL", EnvVars: []string{"MODELZ_AGENT_LOGS_LOKI_URL"}, }, &cli.StringFlag{ Name: flaglogsLokiUser, Hidden: true, Usage: "Loki service auth user", EnvVars: []string{"MODELZ_AGENT_LOGS_LOKI_USER"}, }, &cli.StringFlag{ Name: flagLogsLokiToken, Hidden: true, Usage: "Loki service auth token", EnvVars: []string{"MODELZ_AGENT_LOGS_LOKI_TOKEN"}, }, &cli.BoolFlag{ Name: flagModelZCloudEnabled, Usage: "Enable modelz cloud, agent as modelz cloud agent", Value: false, EnvVars: []string{"MODELZ_AGENT_MODELZ_CLOUD_ENABLED"}, Aliases: []string{"mzc"}, }, &cli.StringFlag{ Name: flagModelZCloudURL, Usage: "Modelz cloud URL", EnvVars: []string{"MODELZ_AGENT_MODELZ_CLOUD_URL"}, Aliases: []string{"mzu"}, Value: "https://cloud.modelz.ai", }, &cli.StringFlag{ Name: flagModelZCloudAgentToken, Usage: "Modelz cloud agent token", EnvVars: []string{"MODELZ_CLOUD_AGENT_TOKEN"}, Aliases: []string{"mzt"}, }, &cli.DurationFlag{ Name: flagModelZCloudAgentHeartbeatInterval, Usage: "Modelz cloud agent heartbeat interval", EnvVars: []string{"MODELZ_CLOUD_AGENT_HEARTBEAT_INTERVAL"}, Aliases: []string{"mzh"}, Value: time.Minute * 1, }, &cli.StringFlag{ Name: flagModelZCloudRegion, Usage: "Modelz cloud agent region", EnvVars: []string{"MODELZ_CLOUD_AGENT_REGION"}, Aliases: []string{"mzr"}, Value: "us-central1", }, &cli.StringFlag{ Name: flagModelZCloudUnifiedAPIKey, Usage: "Modelz cloud agent unified api key", EnvVars: []string{"MODELZ_CLOUD_AGENT_UNIFIED_API_KEY"}, Aliases: []string{"mzua"}, }, &cli.DurationFlag{ Name: flagModelZCloudUpstreamTimeout, Usage: "upstream timeout", EnvVars: []string{"MODELZ_UPSTREAM_TIMEOUT"}, Aliases: []string{"ut"}, Value: 300 * time.Second, }, &cli.IntFlag{ Name: flagModelZCloudMaxIdleConnections, Usage: "max idle connections", EnvVars: []string{"MODELZ_MAX_IDLE_CONNECTIONS"}, Aliases: []string{"mic"}, Value: 1024, }, &cli.IntFlag{ Name: flagModelZCloudMaxIdleConnectionsPerHost, Usage: "max idle connections per host", EnvVars: []string{"MODELZ_MAX_IDLE_CONNECTIONS_PER_HOST"}, Aliases: []string{"mich"}, Value: 1024, }, &cli.BoolFlag{ Name: flagModelZCloudEventEnabled, Usage: "Enable event logging for modelz cloud.", Value: false, EnvVars: []string{"MODELZ_AGENT_MODELZ_CLOUD_EVENT_ENABLED"}, Aliases: []string{"mze"}, }, } internalApp.Action = runServer // Deal with debug flag. var debugEnabled bool internalApp.Before = func(context *cli.Context) error { debugEnabled = context.Bool(flagDebug) if debugEnabled { logrus.SetReportCaller(true) logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) logrus.SetLevel(logrus.DebugLevel) gin.SetMode(gin.DebugMode) } else { logrus.SetFormatter(&logrus.JSONFormatter{}) } return nil } return App{ App: internalApp, } } func runServer(clicontext *cli.Context) error { c := configFromCLI(clicontext) if clicontext.Bool(flagDebug) { logrus.Debug("debug mode enabled") cfgString, _ := c.GetString() logrus.WithField("config", cfgString).Debug("config") } if err := c.Validate(); err != nil { if clicontext.Bool(flagDebug) { logrus.WithError(err).Error("invalid config") } else { return errors.Wrap(err, "invalid config") } } s, err := server.New(c) if err != nil { return errors.Wrap(err, "failed to create server") } return s.Run() } ================================================ FILE: agent/pkg/config/config.go ================================================ package config import ( "encoding/json" "errors" "time" ) type Config struct { Server ServerConfig `json:"server,omitempty"` KubeConfig KubeConfig `json:"kube_config,omitempty"` Ingress IngressConfig `json:"ingress,omitempty"` Inference InferenceConfig `json:"inference,omitempty"` Build BuildConfig `json:"build,omitempty"` Metrics MetricsConfig `json:"metrics,omitempty"` Logs LogsConfig `json:"logs,omitempty"` ModelZCloud ModelZCloudConfig `json:"modelz_cloud,omitempty"` } type ModelZCloudConfig struct { Enabled bool `json:"enabled,omitempty"` // URL of apiserver URL string `json:"url,omitempty"` AgentToken string `json:"agent_token,omitempty"` HeartbeatInterval time.Duration `json:"heartbeat_interval,omitempty"` ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` TokenID string `json:"token_id,omitempty"` Region string `json:"region,omitempty"` APIKeys map[string]string `json:"api_keys,omitempty"` UserNamespaces []string `json:"user_namespaces,omitempty"` UnifiedAPIKey string `json:"unified_api_key,omitempty"` UpstreamTimeout time.Duration `json:"upstream_timeout,omitempty"` MaxIdleConnections int `json:"max_idle_connections,omitempty"` MaxIdleConnectionsPerHost int `json:"max_idle_connections_per_host,omitempty"` EventEnabled bool `json:"event_enabled,omitempty"` } type LogsConfig struct { Timeout time.Duration `json:"timeout,omitempty"` LokiURL string `json:"loki_url,omitempty"` LokiUser string `json:"loki_user,omitempty"` LokiToken string `json:"loki_token,omitempty"` } type ServerConfig struct { Dev bool `json:"dev,omitempty"` ServerPort int `json:"server_port,omitempty"` ReadTimeout time.Duration `json:"read_timeout,omitempty"` WriteTimeout time.Duration `json:"write_timeout,omitempty"` } type MetricsConfig struct { PollingInterval time.Duration `json:"polling_interval,omitempty"` ServerPort int `json:"server_port,omitempty"` PrometheusPort int `json:"prometheus_port,omitempty"` PrometheusHost string `json:"prometheus_host,omitempty"` } type BuildConfig struct { BuildEnabled bool `json:"build_enabled,omitempty"` BuilderImage string `json:"builder_image,omitempty"` BuildkitdAddress string `json:"buildkitd_address,omitempty"` BuildCtlBin string `json:"build_ctl_bin,omitempty"` BuildRegistry string `json:"build_registry,omitempty"` BuildRegistryToken string `json:"build_registry_token,omitempty"` BuildImagePullSecret string `json:"build_image_pull_secret,omitempty"` } type InferenceConfig struct { LogTimeout time.Duration `json:"log_timeout,omitempty"` CacheTTL time.Duration `json:"cache_ttl,omitempty"` } type IngressConfig struct { IngressEnabled bool `json:"ingress_enabled,omitempty"` Domain string `json:"domain,omitempty"` Namespace string `json:"namespace,omitempty"` AnyIPToDomain bool `json:"any_ip_to_domain,omitempty"` TLSEnabled bool `json:"tls_enabled,omitempty"` } type KubeConfig struct { Kubeconfig string `json:"kubeconfig,omitempty"` MasterURL string `json:"master_url,omitempty"` QPS int `json:"qps,omitempty"` Burst int `json:"burst,omitempty"` ResyncPeriod time.Duration `json:"resync_period,omitempty"` } func New() Config { return Config{ KubeConfig: KubeConfig{}, Ingress: IngressConfig{}, Inference: InferenceConfig{}, Build: BuildConfig{}, Metrics: MetricsConfig{}, Logs: LogsConfig{}, } } func (c Config) GetString() (string, error) { bytes, err := json.Marshal(c) return string(bytes), err } func (c Config) Validate() error { if c.Server.ServerPort == 0 || c.Server.ReadTimeout == 0 || c.Server.WriteTimeout == 0 { return errors.New("server config is required") } if c.Inference.LogTimeout == 0 { return errors.New("inference log timeout is required") } if c.Build.BuildEnabled { if c.Build.BuildkitdAddress == "" || c.Build.BuilderImage == "" || c.Build.BuildRegistryToken == "" || c.Build.BuildRegistry == "" || c.Build.BuildCtlBin == "" || c.Build.BuildImagePullSecret == "" { return errors.New("build config is required") } } if c.Metrics.ServerPort == 0 || c.Metrics.PollingInterval == 0 || c.Metrics.PrometheusHost == "" || c.Metrics.PrometheusPort == 0 { return errors.New("metrics config is required") } if c.Ingress.IngressEnabled { if c.Ingress.Namespace == "" { return errors.New("ingress namespace is required") } if !c.Ingress.AnyIPToDomain && c.Ingress.Domain == "" { return errors.New("ingress domain is required") } } if c.ModelZCloud.Enabled { if c.ModelZCloud.URL == "" || c.ModelZCloud.AgentToken == "" || c.ModelZCloud.HeartbeatInterval == 0 { return errors.New("modelz cloud config is required") } } return nil } ================================================ FILE: agent/pkg/consts/consts.go ================================================ package consts import "time" const ( Domain = "modelz.live" DefaultPrefix = "modelz-" APIKEY_PREFIX = "mzi-" ) const DefaultAPIServerReadyTimeout = 15 * time.Minute ================================================ FILE: agent/pkg/docs/docs.go ================================================ // Package docs GENERATED BY SWAG; DO NOT EDIT // This file was generated by swaggo/swag package docs import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": { "name": "modelz support", "url": "https://github.com/tensorchord/openmodelz", "email": "modelz-support@tensorchord.ai" }, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/gradio/{id}": { "get": { "description": "Reverse proxy to the backend gradio.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Reverse proxy to the backend gradio.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } }, "post": { "description": "Reverse proxy to the backend gradio.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Reverse proxy to the backend gradio.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } } }, "/healthz": { "get": { "description": "Healthz", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "system" ], "summary": "Healthz", "responses": { "200": { "description": "OK" } } } }, "/inference/{name}": { "get": { "description": "Inference proxy.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference-proxy" ], "summary": "Inference.", "parameters": [ { "type": "string", "description": "inference id", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK" }, "303": { "description": "See Other" }, "400": { "description": "Bad Request" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } }, "put": { "description": "Inference proxy.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference-proxy" ], "summary": "Inference.", "parameters": [ { "type": "string", "description": "inference id", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK" }, "303": { "description": "See Other" }, "400": { "description": "Bad Request" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } }, "post": { "description": "Inference proxy.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference-proxy" ], "summary": "Inference.", "parameters": [ { "type": "string", "description": "inference id", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK" }, "303": { "description": "See Other" }, "400": { "description": "Bad Request" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } }, "delete": { "description": "Inference proxy.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference-proxy" ], "summary": "Inference.", "parameters": [ { "type": "string", "description": "inference id", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK" }, "303": { "description": "See Other" }, "400": { "description": "Bad Request" }, "404": { "description": "Not Found" }, "500": { "description": "Internal Server Error" } } } }, "/mosec/{id}": { "get": { "description": "Proxy to the backend mosec.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Proxy to the backend mosec.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } } }, "/mosec/{id}/inference": { "post": { "description": "Proxy to the backend mosec.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Proxy to the backend mosec.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } } }, "/mosec/{id}/metrics": { "get": { "description": "Proxy to the backend mosec.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Proxy to the backend mosec.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } } }, "/other/{id}": { "get": { "description": "Reverse proxy to the backend other.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Reverse proxy to the backend other.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } }, "post": { "description": "Reverse proxy to the backend other.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Reverse proxy to the backend other.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } } }, "/streamlit/{id}": { "get": { "description": "Reverse proxy to streamlit.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Reverse proxy to streamlit.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } }, "post": { "description": "Reverse proxy to streamlit.", "consumes": [ "*/*" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Reverse proxy to streamlit.", "parameters": [ { "type": "string", "description": "Deployment ID", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created" } } } }, "/system/build": { "get": { "description": "List the builds.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "build" ], "summary": "List the builds.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.Build" } } } } }, "post": { "description": "Create the build.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "build" ], "summary": "Create the build.", "parameters": [ { "description": "build", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.Build" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/types.Build" } } } } }, "/system/build/{name}": { "get": { "description": "Get the build by name.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "build" ], "summary": "Get the build by name.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true }, { "type": "string", "description": "inference id", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/types.Build" } } } } }, "/system/image-cache": { "post": { "description": "Create the image cache.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "image-cache" ], "summary": "Create the image cache.", "parameters": [ { "description": "image-cache", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.ImageCache" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/types.ImageCache" } } } } }, "/system/inference/{name}": { "get": { "description": "Get the inference by name.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Get the inference by name.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true }, { "type": "string", "description": "inference id", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/types.InferenceDeployment" } } } } }, "/system/inference/{name}/instance/{instance}": { "post": { "description": "Attach to the inference instance.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Attach to the inference instance.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true }, { "type": "string", "description": "Name", "name": "name", "in": "path", "required": true }, { "type": "string", "description": "Instance name", "name": "instance", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.InferenceDeployment" } } } } } }, "/system/inference/{name}/instances": { "get": { "description": "List the inference instances.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "List the inference instances.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true }, { "type": "string", "description": "Name", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.InferenceDeployment" } } } } } }, "/system/inferences": { "get": { "description": "List the inferences.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "List the inferences.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.InferenceDeployment" } } } } }, "put": { "description": "Update the inferences.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Update the inferences.", "parameters": [ { "description": "query params", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.InferenceDeployment" } }, { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true } ], "responses": { "202": { "description": "Accepted", "schema": { "$ref": "#/definitions/types.InferenceDeployment" } } } }, "post": { "description": "Create the inferences.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Create the inferences.", "parameters": [ { "description": "query params", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.InferenceDeployment" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/types.InferenceDeployment" } } } }, "delete": { "description": "Delete the inferences.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Delete the inferences.", "parameters": [ { "description": "query params", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.DeleteFunctionRequest" } }, { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true } ], "responses": { "202": { "description": "Accepted", "schema": { "$ref": "#/definitions/types.DeleteFunctionRequest" } } } } }, "/system/info": { "get": { "description": "Get system info.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "system" ], "summary": "Get system info.", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/types.ProviderInfo" } } } } }, "/system/logs/build": { "get": { "description": "Get the build logs.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "log" ], "summary": "Get the build logs.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true }, { "type": "string", "description": "Build Name", "name": "name", "in": "query", "required": true }, { "type": "string", "description": "Instance", "name": "instance", "in": "query" }, { "type": "integer", "description": "Tail", "name": "tail", "in": "query" }, { "type": "boolean", "description": "Follow", "name": "follow", "in": "query" }, { "type": "string", "description": "Since", "name": "since", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.Message" } } } } } }, "/system/logs/inference": { "get": { "description": "Get the inference logs.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "log" ], "summary": "Get the inference logs.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true }, { "type": "string", "description": "Name", "name": "name", "in": "query", "required": true }, { "type": "string", "description": "Instance", "name": "instance", "in": "query" }, { "type": "integer", "description": "Tail", "name": "tail", "in": "query" }, { "type": "boolean", "description": "Follow", "name": "follow", "in": "query" }, { "type": "string", "description": "Since", "name": "since", "in": "query" }, { "type": "string", "description": "End", "name": "end", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.Message" } } } } } }, "/system/namespaces": { "get": { "description": "List the namespaces.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "namespace" ], "summary": "List the namespaces.", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } } } }, "post": { "description": "Create the namespace.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "namespace" ], "summary": "Create the namespace.", "parameters": [ { "description": "Namespace name", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.NamespaceRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/types.NamespaceRequest" } } } }, "delete": { "description": "Delete the namespace.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "namespace" ], "summary": "Delete the namespace.", "parameters": [ { "description": "Namespace name", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.NamespaceRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/types.NamespaceRequest" } } } } }, "/system/scale-inference": { "post": { "description": "Scale the inferences.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "inference" ], "summary": "Scale the inferences.", "parameters": [ { "type": "string", "description": "Namespace", "name": "namespace", "in": "query", "required": true }, { "description": "query params", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.ScaleServiceRequest" } } ], "responses": { "202": { "description": "Accepted", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.ScaleServiceRequest" } } }, "400": { "description": "Bad Request" } } } }, "/system/server/{name}/delete": { "delete": { "description": "Delete a node.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "namespace" ], "summary": "Delete a node from the cluster.", "parameters": [ { "type": "string", "description": "Server Name", "name": "name", "in": "path", "required": true } ], "responses": { "200": { "description": "OK" } } } }, "/system/server/{name}/labels": { "post": { "description": "List the servers.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "namespace" ], "summary": "List the servers.", "parameters": [ { "type": "string", "description": "Server Name", "name": "name", "in": "path", "required": true }, { "description": "query params", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/types.ServerSpec" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } } } } }, "/system/servers": { "get": { "description": "List the servers.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "namespace" ], "summary": "List the servers.", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/types.Server" } } } } } } }, "definitions": { "types.AuthN": { "type": "object", "properties": { "password": { "type": "string" }, "token": { "type": "string" }, "username": { "type": "string" } } }, "types.Build": { "type": "object", "properties": { "spec": { "$ref": "#/definitions/types.BuildSpec" }, "status": { "$ref": "#/definitions/types.BuildStatus" } } }, "types.BuildSpec": { "type": "object", "properties": { "authn": { "$ref": "#/definitions/types.AuthN" }, "branch": { "type": "string" }, "buildTarget": { "$ref": "#/definitions/types.BuildTarget" }, "image": { "type": "string" }, "image_tag": { "type": "string" }, "name": { "type": "string" }, "namespace": { "type": "string" }, "repository": { "description": "repository is the URL", "type": "string" }, "revision": { "description": "revision is the commit hash for the specified revision.\n+optional", "type": "string" }, "secret_id": { "type": "string" } } }, "types.BuildStatus": { "type": "object", "properties": { "phase": { "type": "string" } } }, "types.BuildTarget": { "type": "object", "properties": { "builder": { "type": "string" }, "digest": { "type": "string" }, "directory": { "description": "directory is the target directory name.\nMust not contain or start with '..'. If '.' is supplied, the volume directory will be the\ngit repository. Otherwise, if specified, the volume will contain the git repository in\nthe subdirectory with the given name.\n+optional", "type": "string" }, "duration": { "type": "string" }, "image": { "type": "string" }, "image_tag": { "type": "string" }, "registry": { "type": "string" }, "registry_token": { "type": "string" } } }, "types.DeleteFunctionRequest": { "type": "object", "properties": { "functionName": { "type": "string" } } }, "types.ImageCache": { "type": "object", "properties": { "force_full_cache": { "type": "boolean" }, "image": { "type": "string" }, "name": { "description": "Name is the name of the inference.", "type": "string" }, "namespace": { "type": "string" }, "node_selector": { "type": "string" } } }, "types.InferenceDeployment": { "type": "object", "properties": { "spec": { "$ref": "#/definitions/types.InferenceDeploymentSpec" }, "status": { "$ref": "#/definitions/types.InferenceDeploymentStatus" } } }, "types.InferenceDeploymentSpec": { "type": "object", "properties": { "annotations": { "description": "Annotations are key-value pairs that may be attached to the inference.", "type": "object", "additionalProperties": { "type": "string" } }, "command": { "description": "Command to run when starting the", "type": "string" }, "constraints": { "description": "Constraints are the constraints for the inference.", "type": "array", "items": { "type": "string" } }, "envVars": { "description": "EnvVars can be provided to set environment variables for the inference runtime.", "type": "object", "additionalProperties": { "type": "string" } }, "framework": { "description": "Framework is the inference framework.", "type": "string" }, "http_probe_path": { "description": "HTTPProbePath is the path of the http probe.", "type": "string" }, "image": { "description": "Image is a fully-qualified container image", "type": "string" }, "labels": { "description": "Labels are key-value pairs that may be attached to the inference.", "type": "object", "additionalProperties": { "type": "string" } }, "name": { "description": "Name is the name of the inference.", "type": "string" }, "namespace": { "description": "Namespace for the inference.", "type": "string" }, "port": { "description": "Port is the port exposed by the inference.", "type": "integer" }, "resources": { "description": "Resources are the compute resource requirements.", "$ref": "#/definitions/types.ResourceRequirements" }, "scaling": { "description": "Scaling is the scaling configuration for the inference.", "$ref": "#/definitions/types.ScalingConfig" }, "secrets": { "description": "Secrets list of secrets to be made available to inference.", "type": "array", "items": { "type": "string" } } } }, "types.InferenceDeploymentStatus": { "type": "object", "properties": { "availableReplicas": { "description": "AvailableReplicas is the count of replicas ready to receive\ninvocations as reported by the faas-provider", "type": "integer" }, "createdAt": { "description": "CreatedAt is the time read back from the faas backend's\ndata store for when the function or its container was created.", "type": "string" }, "eventMessage": { "description": "EventMessage record human readable message indicating details about the event of deployment.", "type": "string" }, "invocationCount": { "description": "InvocationCount count of invocations", "type": "integer" }, "phase": { "type": "string" }, "replicas": { "description": "Replicas desired within the cluster", "type": "integer" }, "usage": { "description": "Usage represents CPU and RAM used by all of the\nfunctions' replicas. Divide by AvailableReplicas for an\naverage value per replica.", "$ref": "#/definitions/types.InferenceUsage" } } }, "types.InferenceUsage": { "type": "object", "properties": { "cpu": { "description": "CPU is the increase in CPU usage since the last measurement\nequivalent to Kubernetes' concept of millicores.", "type": "number" }, "gpu": { "type": "number" }, "totalMemoryBytes": { "description": "TotalMemoryBytes is the total memory usage in bytes.", "type": "number" } } }, "types.Message": { "type": "object", "properties": { "instance": { "description": "instance is the name/id of the specific function instance", "type": "string" }, "name": { "description": "Name is the function name", "type": "string" }, "namespace": { "type": "string" }, "text": { "description": "Text is the raw log message content", "type": "string" }, "timestamp": { "description": "Timestamp is the timestamp of when the log message was recorded", "type": "string" } } }, "types.NamespaceRequest": { "type": "object", "properties": { "name": { "type": "string" } } }, "types.NodeSystemInfo": { "type": "object", "properties": { "architecture": { "description": "The Architecture reported by the node", "type": "string" }, "kernelVersion": { "description": "Kernel Version reported by the node from 'uname -r' (e.g. 3.16.0-0.bpo.4-amd64).", "type": "string" }, "machineID": { "description": "MachineID reported by the node. For unique machine identification\nin the cluster this field is preferred. Learn more from man(5)\nmachine-id: http://man7.org/linux/man-pages/man5/machine-id.5.html", "type": "string" }, "operatingSystem": { "description": "The Operating System reported by the node", "type": "string" }, "osImage": { "description": "OS Image reported by the node from /etc/os-release (e.g. Debian GNU/Linux 7 (wheezy)).", "type": "string" } } }, "types.ProviderInfo": { "type": "object", "properties": { "orchestration": { "type": "string" }, "provider": { "type": "string" }, "version": { "$ref": "#/definitions/types.VersionInfo" } } }, "types.ResourceList": { "type": "object", "additionalProperties": { "type": "string" } }, "types.ResourceRequirements": { "type": "object", "properties": { "limits": { "description": "Limits describes the maximum amount of compute resources allowed.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/\n+optional", "$ref": "#/definitions/types.ResourceList" }, "requests": { "description": "Requests describes the minimum amount of compute resources required.\nIf Requests is omitted for a container, it defaults to Limits if that is explicitly specified,\notherwise to an implementation-defined value.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/\n+optional", "$ref": "#/definitions/types.ResourceList" } } }, "types.ScaleServiceRequest": { "type": "object", "properties": { "attempt": { "type": "integer" }, "eventMessage": { "type": "string" }, "replicas": { "type": "integer" }, "serviceName": { "type": "string" } } }, "types.ScalingConfig": { "type": "object", "properties": { "max_replicas": { "description": "MaxReplicas is the upper limit for the number of replicas to which the\nautoscaler can scale up. It cannot be less that minReplicas. It defaults\nto 1.", "type": "integer" }, "min_replicas": { "description": "MinReplicas is the lower limit for the number of replicas to which the\nautoscaler can scale down. It defaults to 0.", "type": "integer" }, "startup_duration": { "description": "StartupDuration is the duration (in seconds) of startup time.", "type": "integer" }, "target_load": { "description": "TargetLoad is the target load. In capacity mode, it is the expected number of the inflight requests per replica.", "type": "integer" }, "type": { "description": "Type is the scaling type. It can be either \"capacity\" or \"rps\". Default is \"capacity\".", "type": "string" }, "zero_duration": { "description": "ZeroDuration is the duration (in seconds) of zero load before scaling down to zero. Default is 5 minutes.", "type": "integer" } } }, "types.Server": { "type": "object", "properties": { "spec": { "$ref": "#/definitions/types.ServerSpec" }, "status": { "$ref": "#/definitions/types.ServerStatus" } } }, "types.ServerSpec": { "type": "object", "properties": { "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "name": { "type": "string" } } }, "types.ServerStatus": { "type": "object", "properties": { "allocatable": { "$ref": "#/definitions/types.ResourceList" }, "capacity": { "$ref": "#/definitions/types.ResourceList" }, "phase": { "type": "string" }, "system": { "$ref": "#/definitions/types.NodeSystemInfo" } } }, "types.VersionInfo": { "type": "object", "properties": { "build_date": { "type": "string" }, "compiler": { "type": "string" }, "git_commit": { "type": "string" }, "git_tag": { "type": "string" }, "git_tree_state": { "type": "string" }, "go_version": { "type": "string" }, "platform": { "type": "string" }, "version": { "type": "string" } } } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "v0.0.23", Host: "localhost:8081", BasePath: "/", Schemes: []string{"http"}, Title: "modelz cluster agent", Description: "modelz kubernetes cluster agent", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } ================================================ FILE: agent/pkg/event/event.go ================================================ package event import ( "context" "fmt" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/client" ) type Interface interface { CreateDeploymentEvent(namespace, deployment, event, message string) error } type EventRecorder struct { Client *client.Client AgentToken string } func NewEventRecorder(client *client.Client, token string) Interface { return &EventRecorder{ Client: client, AgentToken: token, } } func (e *EventRecorder) CreateDeploymentEvent(namespace, deployment, event, message string) error { user, err := client.GetUserIDFromNamespace(namespace) if err != nil { return err } else if user == "" { return fmt.Errorf("user id is empty") } deploymentEvent := types.DeploymentEvent{ UserID: user, DeploymentID: deployment, EventType: event, Message: message, } err = e.Client.CreateDeploymentEvent(context.TODO(), e.AgentToken, deploymentEvent) if err != nil { logrus.Errorf("failed to create deployment event: %v", err) return err } return nil } ================================================ FILE: agent/pkg/event/fake.go ================================================ package event type Fake struct { } func NewFake() Interface { return &Fake{} } func (f *Fake) CreateDeploymentEvent(namespace, deployment, event, message string) error { return nil } ================================================ FILE: agent/pkg/event/suite_test.go ================================================ package event import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestBuilder(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "event") } ================================================ FILE: agent/pkg/event/username.go ================================================ package event import "fmt" const ( DefaultPrefix = "modelz-" ) func getUserIDFromNamespace(ns string) (string, error) { if len(ns) < 8 { return "", fmt.Errorf("namespace too short") } if ns[:len(DefaultPrefix)] != DefaultPrefix { return "", fmt.Errorf("namespace does not start with %s", DefaultPrefix) } return ns[len(DefaultPrefix):], nil } ================================================ FILE: agent/pkg/event/util.go ================================================ package event import ( "database/sql" ) func NullStringBuilder(String string, Valid bool) sql.NullString { return sql.NullString{String: String, Valid: Valid} } ================================================ FILE: agent/pkg/k8s/convert_inference.go ================================================ package k8s import ( "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" ) func AsInferenceDeployment(inf *v2alpha1.Inference, item *appsv1.Deployment) *types.InferenceDeployment { if inf == nil { return nil } res := &types.InferenceDeployment{ Spec: types.InferenceDeploymentSpec{ Name: inf.Name, Framework: types.Framework(inf.Spec.Framework), Image: inf.Spec.Image, Namespace: inf.Namespace, EnvVars: inf.Spec.EnvVars, Secrets: inf.Spec.Secrets, Constraints: inf.Spec.Constraints, Labels: inf.Spec.Labels, Annotations: inf.Spec.Annotations, }, Status: types.InferenceDeploymentStatus{ Phase: types.PhaseNoReplicas, }, } if inf.Spec.Scaling != nil { res.Spec.Scaling = &types.ScalingConfig{ MinReplicas: inf.Spec.Scaling.MinReplicas, MaxReplicas: inf.Spec.Scaling.MaxReplicas, TargetLoad: inf.Spec.Scaling.TargetLoad, ZeroDuration: inf.Spec.Scaling.ZeroDuration, StartupDuration: inf.Spec.Scaling.StartupDuration, } if inf.Spec.Scaling.Type != nil { typ := types.ScalingType(*inf.Spec.Scaling.Type) res.Spec.Scaling.Type = &typ } } if inf.Spec.Port != nil { res.Spec.Port = inf.Spec.Port } var replicas int32 = 0 // Get status according to the deployment. if item != nil { if item.Spec.Replicas != nil { replicas = *item.Spec.Replicas } res.Status.Replicas = replicas res.Status.CreatedAt = &item.CreationTimestamp.Time res.Status.InvocationCount = 0 res.Status.AvailableReplicas = item.Status.AvailableReplicas res.Status.Phase = AsStatusPhase(item) } return res } func AsResourceList(resources v1.ResourceList) types.ResourceList { res := types.ResourceList{} gpuResource := resources[consts.ResourceNvidiaGPU] gpuPtr := &gpuResource if !resources.Cpu().IsZero() { res[types.ResourceCPU] = types.Quantity( resources.Cpu().String()) } if !resources.Memory().IsZero() { res[types.ResourceMemory] = types.Quantity( resources.Memory().String()) } if !gpuPtr.IsZero() { res[types.ResourceGPU] = types.Quantity( gpuPtr.String()) } return res } func AsStatusPhase(item *appsv1.Deployment) types.Phase { phase := types.PhaseNotReady for _, c := range item.Status.Conditions { if c.Type == appsv1.DeploymentAvailable && c.Status == v1.ConditionTrue { phase = types.PhaseReady } else if c.Type == appsv1.DeploymentProgressing && c.Status == v1.ConditionFalse { phase = types.PhaseScaling } } if item.Spec.Replicas != nil && *item.Spec.Replicas == 0 { phase = types.PhaseNoReplicas } if item.DeletionTimestamp != nil { phase = types.PhaseTerminating } return phase } ================================================ FILE: agent/pkg/k8s/convert_inference_test.go ================================================ package k8s import ( "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" v1types "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = Describe("agent/pkg/k8s/convert_inference", func() { It("function AsResourceList", func() { tcs := []struct { resource v1.ResourceList expect types.ResourceList }{ { resource: map[v1types.ResourceName]resource.Quantity{ v1types.ResourceCPU: resource.MustParse("0"), v1types.ResourceMemory: resource.MustParse("0"), consts.ResourceNvidiaGPU: resource.MustParse("0"), }, expect: types.ResourceList{}, }, { resource: map[v1types.ResourceName]resource.Quantity{ v1types.ResourceCPU: resource.MustParse("0"), v1types.ResourceMemory: resource.MustParse("500m"), consts.ResourceNvidiaGPU: resource.MustParse("0"), }, expect: types.ResourceList{ types.ResourceMemory: types.Quantity("500m"), }, }, { resource: map[v1types.ResourceName]resource.Quantity{ v1types.ResourceCPU: resource.MustParse("0"), v1types.ResourceMemory: resource.MustParse("0"), consts.ResourceNvidiaGPU: resource.MustParse("0.5"), }, expect: types.ResourceList{ types.ResourceGPU: types.Quantity("500m"), }, }, { resource: map[v1types.ResourceName]resource.Quantity{ v1types.ResourceCPU: resource.MustParse("0.1"), v1types.ResourceMemory: resource.MustParse("0"), consts.ResourceNvidiaGPU: resource.MustParse("0"), }, expect: types.ResourceList{ types.ResourceCPU: types.Quantity("100m"), }, }, } for _, tc := range tcs { value := AsResourceList(tc.resource) Expect(value).To(Equal(tc.expect)) } }) It("function AsInferenceDeployment", func() { mockTime, _ := time.Parse("2006-01-02", "2023-09-07") tcs := []struct { inf *v2alpha1.Inference deployment *appsv1.Deployment expect *types.InferenceDeployment }{ { inf: nil, deployment: nil, expect: nil, }, { inf: Ptr(v2alpha1.Inference{}), deployment: Ptr(appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Time{ Time: mockTime, }, }, }), expect: Ptr(types.InferenceDeployment{ Status: types.InferenceDeploymentStatus{ Phase: types.PhaseNotReady, CreatedAt: Ptr(mockTime), }, }), }, { inf: Ptr(v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Scaling: Ptr(v2alpha1.ScalingConfig{ Type: Ptr(v2alpha1.ScalingTypeCapacity), }), }, }), deployment: Ptr(appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ CreationTimestamp: metav1.Time{ Time: mockTime, }, }, }), expect: Ptr(types.InferenceDeployment{ Spec: types.InferenceDeploymentSpec{ Scaling: Ptr(types.ScalingConfig{ Type: Ptr(types.ScalingTypeCapacity), }), }, Status: types.InferenceDeploymentStatus{ Phase: types.PhaseNotReady, CreatedAt: Ptr(mockTime), }, }), }, } for _, tc := range tcs { value := AsInferenceDeployment(tc.inf, tc.deployment) Expect(value).To(Equal(tc.expect)) } }) }) ================================================ FILE: agent/pkg/k8s/convert_job.go ================================================ package k8s import ( v1 "k8s.io/api/batch/v1" "github.com/tensorchord/openmodelz/agent/api/types" ) func AsBuild(job v1.Job) (types.Build, error) { build := types.Build{ Spec: types.BuildSpec{ Name: job.Name, Namespace: job.Namespace, }, } if job.Status.Succeeded > 0 { build.Status.Phase = types.BuildPhaseSucceeded } else if job.Status.Failed > 0 { build.Status.Phase = types.BuildPhaseFailed } else if job.Status.Active > 0 { build.Status.Phase = types.BuildPhaseRunning } else { build.Status.Phase = types.BuildPhasePending } return build, nil } ================================================ FILE: agent/pkg/k8s/convert_pod.go ================================================ package k8s import ( "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" v1 "k8s.io/api/core/v1" "github.com/tensorchord/openmodelz/agent/api/types" ) func MakeLabelSelector(name string) map[string]string { return map[string]string{ "app": name, } } func InstanceFromPod(pod v1.Pod) *types.InferenceDeploymentInstance { i := &types.InferenceDeploymentInstance{ Spec: types.InferenceDeploymentInstanceSpec{ Namespace: pod.Namespace, Name: pod.Name, OwnerReference: pod.Labels[consts.LabelInferenceName], }, Status: types.InferenceDeploymentInstanceStatus{ Reason: pod.Status.Reason, Message: pod.Status.Message, }, } if pod.Status.StartTime != nil { i.Status.StartTime = pod.Status.StartTime.Time } switch pod.Status.Phase { case v1.PodRunning: i.Status.Phase = types.InstancePhaseRunning case v1.PodPending: i.Status.Phase = types.InstancePhasePending case v1.PodFailed: i.Status.Phase = types.InstancePhaseFailed case v1.PodSucceeded: i.Status.Phase = types.InstancePhaseSucceeded case v1.PodUnknown: i.Status.Phase = types.InstancePhaseUnknown } if pod.Status.Conditions != nil { for _, c := range pod.Status.Conditions { if c.Type == v1.PodScheduled && c.Status == v1.ConditionFalse { i.Status.Phase = types.InstancePhaseScheduling i.Status.Reason = c.Reason i.Status.Message = c.Message break } } } if len(pod.Status.ContainerStatuses) != 0 { if pod.Status.ContainerStatuses[0].Started != nil && !*pod.Status.ContainerStatuses[0].Started { i.Status.Phase = types.InstancePhaseCreating if pod.Status.ContainerStatuses[0].State.Waiting != nil { i.Status.Reason = pod.Status.ContainerStatuses[0].State.Waiting.Reason i.Status.Message = pod.Status.ContainerStatuses[0].State.Waiting.Message i.Status.Phase = types.InstancePhase( pod.Status.ContainerStatuses[0].State.Waiting.Reason) } else if pod.Status.ContainerStatuses[0].State.Running != nil { i.Status.Phase = types.InstancePhaseInitializing } else if pod.Status.ContainerStatuses[0].State.Terminated != nil { i.Status.Phase = types.InstancePhaseFailed i.Status.Reason = pod.Status.ContainerStatuses[0].State.Terminated.Reason i.Status.Message = pod.Status.ContainerStatuses[0].State.Terminated.Message } } } return i } ================================================ FILE: agent/pkg/k8s/convert_pod_test.go ================================================ package k8s import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/sirupsen/logrus" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" "github.com/tensorchord/openmodelz/agent/api/types" v1 "k8s.io/api/core/v1" ) var _ = Describe("agent/pkg/k8s/convert_pod", func() { It("function InstanceFromPod", func() { tcs := []struct { desc string pod v1.Pod expect *types.InferenceDeploymentInstance }{ { desc: "empty pod", pod: v1.Pod{}, expect: Ptr( types.InferenceDeploymentInstance{}, ), }, { desc: "running pod", pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseRunning, }, }, ), }, { desc: "pending pod", pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodPending, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhasePending, }, }, ), }, { desc: "scheduling pod", pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodPending, Conditions: []v1.PodCondition{ { Type: v1.PodScheduled, Status: v1.ConditionFalse, }, }, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseScheduling, }, }, ), }, { desc: "failed pod", pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodFailed, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseFailed, }, }, ), }, { desc: "succeed pod", pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodSucceeded, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseSucceeded, }, }, ), }, { desc: "unknown pod", pod: v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodUnknown, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseUnknown, }, }, ), }, { desc: "creating pod", pod: v1.Pod{ Status: v1.PodStatus{ ContainerStatuses: []v1.ContainerStatus{ { Started: Ptr(false), }, }, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseCreating, }, }, ), }, { desc: "waiting pod", pod: v1.Pod{ Status: v1.PodStatus{ ContainerStatuses: []v1.ContainerStatus{ { Started: Ptr(false), State: v1.ContainerState{ Waiting: Ptr(v1.ContainerStateWaiting{ Reason: "mock-status", }), }, }, }, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhase("mock-status"), Reason: "mock-status", }, }, ), }, { desc: "initializing pod", pod: v1.Pod{ Status: v1.PodStatus{ ContainerStatuses: []v1.ContainerStatus{ { Started: Ptr(false), State: v1.ContainerState{ Running: Ptr(v1.ContainerStateRunning{}), }, }, }, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseInitializing, }, }, ), }, { desc: "terminated pod", pod: v1.Pod{ Status: v1.PodStatus{ ContainerStatuses: []v1.ContainerStatus{ { Started: Ptr(false), State: v1.ContainerState{ Terminated: Ptr(v1.ContainerStateTerminated{}), }, }, }, }, }, expect: Ptr( types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseFailed, }, }, ), }, } for _, tc := range tcs { logrus.Info(tc.desc) value := InstanceFromPod(tc.pod) Expect(value).To(Equal(tc.expect)) } }) }) ================================================ FILE: agent/pkg/k8s/generate_image_cache.go ================================================ package k8s import ( "time" kubefledged "github.com/senthilrch/kube-fledged/pkg/apis/kubefledged/v1alpha3" "github.com/tensorchord/openmodelz/agent/api/types" modelzetes "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) func MakeImageCache(req types.ImageCache, inference *modelzetes.Inference) *kubefledged.ImageCache { nodeSlector := map[string]string{ consts.LabelServerResource: string(req.NodeSelector), } cache := &kubefledged.ImageCache{ ObjectMeta: v1.ObjectMeta{ Name: req.Name, Namespace: req.Namespace, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(inference, schema.GroupVersionKind{ Group: modelzetes.SchemeGroupVersion.Group, Version: modelzetes.SchemeGroupVersion.Version, Kind: modelzetes.Kind, }), }, }, Spec: kubefledged.ImageCacheSpec{ CacheSpec: []kubefledged.CacheSpecImages{ { Images: []kubefledged.Image{ { Name: req.Image, ForceFullCache: req.ForceFullCache, }, }, NodeSelector: nodeSlector, }, }, }, Status: kubefledged.ImageCacheStatus{ StartTime: &metav1.Time{Time: time.Now()}, }, } return cache } ================================================ FILE: agent/pkg/k8s/generate_job.go ================================================ package k8s import ( "time" "github.com/cockroachdb/errors" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) func MakeBuild(req types.Build, inference *v2alpha1.Inference, builderImage, buildkitdAddr, buildctlBin, secret string) (*batchv1.Job, error) { job := &batchv1.Job{} duration, err := time.ParseDuration(req.Spec.BuildTarget.Duration) if err != nil { return nil, errors.Wrap(err, "failed to parse duration") } seconds := int64(duration.Seconds()) defaultBackoffLimit := int32(0) defaultTTLSecondsAfterFinished := int32(60 * 60 * 24 * 7) // 7 days envs := []corev1.EnvVar{ { Name: "MODELZ_BUILD_NAME", Value: req.Spec.Name, }, { Name: "MODELZ_BUILDER", Value: string(req.Spec.BuildTarget.Builder), }, { Name: "MODELZ_BUILD_ARTIFACT_IMAGE", Value: req.Spec.BuildTarget.ArtifactImage, }, { Name: "MODELZ_BUILD_ARTIFACT_IMAGE_TAG", Value: req.Spec.BuildTarget.ArtifactImageTag, }, { Name: "MODELZ_REGISTRY", Value: req.Spec.BuildTarget.Registry, }, { Name: "MODELZ_REGISTRY_TOKEN", Value: req.Spec.BuildTarget.RegistryToken, }, } if req.Spec.BuildTarget.Builder != types.BuilderTypeImage { envs = append(envs, buildEnvsForDockerfileOrEnvd(req, buildkitdAddr, buildctlBin)...) } else { envs = append(envs, buildEnvsForImage(req)...) } ownerReference := []metav1.OwnerReference{ *metav1.NewControllerRef(inference, schema.GroupVersionKind{ Group: v2alpha1.SchemeGroupVersion.Group, Version: v2alpha1.SchemeGroupVersion.Version, Kind: v2alpha1.Kind, }), } job = &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: req.Spec.Name, Namespace: req.Spec.Namespace, OwnerReferences: ownerReference, Labels: map[string]string{ consts.LabelBuildName: req.Spec.Name, consts.AnnotationBuilding: "true", }, }, Spec: batchv1.JobSpec{ ActiveDeadlineSeconds: &seconds, BackoffLimit: &defaultBackoffLimit, TTLSecondsAfterFinished: &defaultTTLSecondsAfterFinished, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ consts.LabelBuildName: req.Spec.Name, }, }, Spec: corev1.PodSpec{ ImagePullSecrets: []corev1.LocalObjectReference{ {Name: secret}, }, Volumes: []corev1.Volume{ { Name: "workspace", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, }, RestartPolicy: corev1.RestartPolicyNever, Containers: []corev1.Container{ { Name: req.Spec.Name, Image: builderImage, ImagePullPolicy: corev1.PullAlways, VolumeMounts: []corev1.VolumeMount{ { Name: "workspace", MountPath: "/workspace", }, }, Env: envs, }, }, }, }, }, } return job, nil } func buildEnvsForImage(req types.Build) []corev1.EnvVar { envs := []corev1.EnvVar{} if req.Spec.DockerSource.AuthN.Username != "" { envs = append(envs, corev1.EnvVar{ Name: "MODELZ_SOURCE_REGISTRY_USERNAME", Value: req.Spec.DockerSource.AuthN.Username, }) } if req.Spec.DockerSource.AuthN.Password != "" { envs = append(envs, corev1.EnvVar{ Name: "MODELZ_SOURCE_REGISTRY_PASSWORD", Value: req.Spec.DockerSource.AuthN.Password, }) } if req.Spec.DockerSource.AuthN.Token != "" { envs = append(envs, corev1.EnvVar{ Name: "MODELZ_SOURCE_REGISTRY_TOKEN", Value: req.Spec.DockerSource.AuthN.Token, }) } if req.Spec.DockerSource.ArtifactImage != "" { envs = append(envs, corev1.EnvVar{ Name: "MODELZ_SOURCE_REGISTRY_IMAGE", Value: req.Spec.DockerSource.ArtifactImage, }) } if req.Spec.DockerSource.ArtifactImageTag != "" { envs = append(envs, corev1.EnvVar{ Name: "MODELZ_SOURCE_REGISTRY_IMAGE_TAG", Value: req.Spec.DockerSource.ArtifactImageTag, }) } return envs } func buildEnvsForDockerfileOrEnvd(req types.Build, buildkitdAddr, buildctlBin string) []corev1.EnvVar { return []corev1.EnvVar{ { Name: "MODELZ_BUILD_GIT_URL", Value: req.Spec.Repository, }, { Name: "MODELZ_BUILD_GIT_BRANCH", Value: req.Spec.Branch, }, { Name: "MODELZ_BUILD_GIT_COMMIT", Value: req.Spec.Revision, }, { Name: "MODELZ_BUILD_BASE_DIR", Value: req.Spec.BuildTarget.Directory, }, { Name: "MODELZ_WORKSPACE", Value: "/workspace", }, { Name: "MODELZ_BUILDKITD_ADDRESS", Value: buildkitdAddr, }, { Name: "MODELZ_BUILDER_BIN", Value: buildctlBin, }, } } ================================================ FILE: agent/pkg/k8s/managed_cluster.go ================================================ package k8s import ( "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/kubernetes" ) func GetKubernetesVersion(client kubernetes.Interface) (*version.Info, error) { return client.Discovery().ServerVersion() } ================================================ FILE: agent/pkg/k8s/resolver.go ================================================ package k8s import ( "context" "fmt" "math/rand" "net/url" "strconv" "github.com/anthhub/forwarder" "github.com/phayes/freeport" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" corelister "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/rest" "github.com/tensorchord/openmodelz/agent/errdefs" ) type Resolver interface { Resolve(namespace, name string) (url.URL, error) Close(url url.URL) } func NewPortForwardingResolver(cfg *rest.Config, cli kubernetes.Interface) Resolver { return &PortForwardingResolver{ config: cfg, cli: cli, results: make(map[int]*forwarder.Result), } } func NewEndpointResolver(lister corelister.EndpointsLister) Resolver { return &EndpointResolver{ EndpointLister: lister, } } type PortForwardingResolver struct { config *rest.Config cli kubernetes.Interface results map[int]*forwarder.Result } func (e *PortForwardingResolver) Resolve(namespace, name string) (url.URL, error) { port, err := freeport.GetFreePort() if err != nil { return url.URL{}, err } svc, err := e.cli.CoreV1().Services(namespace).Get(context.Background(), "mdz-"+name, metav1.GetOptions{}) if err != nil { return url.URL{}, err } if svc.Spec.Ports == nil || len(svc.Spec.Ports) == 0 { return url.URL{}, errdefs.System(fmt.Errorf("no ports found in service %s", svc.Name)) } options := []*forwarder.Option{ { // the local port for forwarding LocalPort: port, // the k8s pod port RemotePort: svc.Spec.Ports[0].TargetPort.IntValue(), // the forwarding service name ServiceName: "mdz-" + name, // namespace default is "default" Namespace: namespace, }, } ret, err := forwarder.WithRestConfig(context.Background(), options, e.config) if err != nil { return url.URL{}, err } e.results[port] = ret // wait forwarding ready // the remote and local ports are listed _, err = ret.Ready() if err != nil { return url.URL{}, err } // the ports are ready res, err := url.Parse("http://localhost:" + strconv.Itoa(port)) return *res, err } func (e *PortForwardingResolver) Close(url url.URL) { port, err := strconv.Atoi(url.Port()) if err != nil { panic(err) } logrus.Infof("close port forwarding %d\n", port) if e.results[port] == nil { logrus.Infof("port forwarding %d not found\n", port) return } logrus.Infof("pointer: %v", e.results[port]) e.results[port].Close() } type EndpointResolver struct { EndpointLister corelister.EndpointsLister } func (e EndpointResolver) Resolve(namespace, name string) (url.URL, error) { svcName := consts.DefaultServicePrefix + name svc, err := e.EndpointLister.Endpoints(namespace).Get(svcName) if err != nil { if k8serrors.IsNotFound(err) { return url.URL{}, errdefs.NotFound(err) } return url.URL{}, errdefs.System(err) } if len(svc.Subsets) == 0 { return url.URL{}, errdefs.NotFound( fmt.Errorf("no subsets for \"%s.%s\"", svcName, namespace)) } all := len(svc.Subsets[0].Addresses) if len(svc.Subsets[0].Addresses) == 0 { return url.URL{}, errdefs.NotFound( fmt.Errorf("no addresses for \"%s.%s\"", svcName, namespace)) } target := rand.Intn(all) serviceIP := svc.Subsets[0].Addresses[target].IP servicePort := svc.Subsets[0].Ports[0].Port urlStr := fmt.Sprintf("http://%s:%d", serviceIP, servicePort) urlRes, err := url.Parse(urlStr) if err != nil { return url.URL{}, errdefs.System(err) } return *urlRes, nil } func (e EndpointResolver) Close(url.URL) { // do nothing } ================================================ FILE: agent/pkg/k8s/suite_test.go ================================================ package k8s import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestBuilder(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "k8s") } ================================================ FILE: agent/pkg/log/factory.go ================================================ package log import ( "context" "github.com/tensorchord/openmodelz/agent/api/types" ) // Requester submits queries the logging system. type Requester interface { // Query submits a log request to the actual logging system. Query(ctx context.Context, req types.LogRequest) (<-chan types.Message, error) } ================================================ FILE: agent/pkg/log/k8s.go ================================================ package log import ( "bufio" "context" "fmt" "io" "strings" "time" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/informers" "k8s.io/client-go/informers/internalinterfaces" "k8s.io/client-go/kubernetes" v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" ) const ( // podInformerResync is the period between cache syncs in the pod informer podInformerResync = 5 * time.Second // defaultLogSince is the fallback log stream history defaultLogSince = 5 * time.Minute // LogBufferSize number of log messages that may be buffered LogBufferSize = 500 * 2 ) // K8sAPIRequestor implements the Requestor interface for k8s type K8sAPIRequestor struct { client kubernetes.Interface } func NewK8sAPIRequestor(client kubernetes.Interface) Requester { return &K8sAPIRequestor{ client: client, } } func (k *K8sAPIRequestor) Query(ctx context.Context, r types.LogRequest) (<-chan types.Message, error) { var sinceTime, endTime time.Time if r.Since != "" { var err error sinceTime, err = time.Parse(time.RFC3339, r.Since) if err != nil { return nil, errdefs.InvalidParameter(err) } } if r.End != "" { var err error endTime, err = time.Parse(time.RFC3339, r.End) if err != nil { return nil, errdefs.InvalidParameter(err) } } else if r.Follow { // avoid truncate endTime = time.Now().Add(time.Hour) } else { endTime = time.Now() } logStream, err := getLogs(ctx, k.client, r.Name, r.Namespace, int64(r.Tail), &sinceTime, r.Follow) if err != nil { return nil, err } msgStream := make(chan types.Message, LogBufferSize) go func() { defer close(msgStream) // here we depend on the fact that logStream will close when the context is cancelled, // this ensures that the go routine will resolve for msg := range logStream { // if we have an end time, we should stop streaming logs after that time if endTime.After(msg.Timestamp) { msgStream <- types.Message{ Timestamp: msg.Timestamp, Text: msg.Text, Name: msg.Name, Instance: msg.Instance, Namespace: msg.Namespace, } } } }() return msgStream, nil } // getLogs returns a channel of logs for the given function func getLogs(ctx context.Context, client kubernetes.Interface, functionName, namespace string, tail int64, since *time.Time, follow bool) ( <-chan types.Message, error) { added, err := startFunctionPodInformer(ctx, client, functionName, namespace) if err != nil { return nil, err } logs := make(chan types.Message, LogBufferSize) go func() { var watching uint defer close(logs) finished := make(chan error) for { select { case <-ctx.Done(): return case <-finished: watching-- if watching == 0 && !follow { return } case p := <-added: watching++ go func() { finished <- podLogs(ctx, client.CoreV1().Pods(namespace), p, functionName, namespace, tail, since, follow, logs) }() } } }() return logs, nil } // podLogs returns a stream of logs lines from the specified pod func podLogs(ctx context.Context, i v1.PodInterface, pod, container, namespace string, tail int64, since *time.Time, follow bool, dst chan<- types.Message) error { opts := &corev1.PodLogOptions{ Follow: follow, Timestamps: true, Container: container, } if tail > 0 { opts.TailLines = &tail } if opts.TailLines == nil || since != nil { opts.SinceSeconds = parseSince(since) } stream, err := i.GetLogs(pod, opts).Stream(ctx) if err != nil { return err } defer stream.Close() done := make(chan error) go func() { scanner := bufio.NewScanner(stream) for scanner.Scan() { msg, ts := extractTimestampAndMsg(scanner.Text()) dst <- types.Message{ Timestamp: ts, Text: msg, Instance: pod, Name: container, Namespace: namespace, } } if err := scanner.Err(); err != nil { done <- err return } }() select { case <-ctx.Done(): logrus.Debug("get-log context cancelled") return ctx.Err() case err := <-done: if err != io.EOF { logrus.Debugf("failed to read from pod log: %v", err) return err } return nil } } // startFunctionPodInformer will gather the list of existing Pods for the function, it will // watch for newly added or deleted function instances. func startFunctionPodInformer(ctx context.Context, client kubernetes.Interface, functionName, namespace string) (<-chan string, error) { functionSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{consts.LabelInferenceName: functionName}, } selector, err := metav1.LabelSelectorAsSelector(functionSelector) if err != nil { return nil, errdefs.InvalidParameter(err) } logrus.WithFields(logrus.Fields{ "selector": selector.String(), "namespace": namespace, }).Debugf("starting log pod informer") factory := informers.NewFilteredSharedInformerFactory( client, podInformerResync, namespace, withLabels(selector.String()), ) podInformer := factory.Core().V1().Pods() podsResp, err := client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()}) if err != nil { if k8serrors.IsNotFound(err) { return nil, errdefs.NotFound(err) } else { return nil, errdefs.System(err) } } pods := podsResp.Items if len(pods) == 0 { return nil, errdefs.NotFound( fmt.Errorf("no pods found for inference: %s", functionName)) } // prepare channel with enough space for the current instance set added := make(chan string, len(pods)) podInformer.Informer().AddEventHandler(&podLoggerEventHandler{ added: added, }) // will add existing pods to the chan and then listen for any new pods go podInformer.Informer().Run(ctx.Done()) go func() { <-ctx.Done() close(added) }() return added, nil } // parseSince returns the time.Duration of the requested Since value _or_ 5 minutes func parseSince(r *time.Time) *int64 { var since int64 if r == nil || r.IsZero() { since = int64(defaultLogSince.Seconds()) return &since } since = int64(time.Since(*r).Seconds()) return &since } func extractTimestampAndMsg(logText string) (string, time.Time) { // first 32 characters is the k8s timestamp parts := strings.SplitN(logText, " ", 2) ts, err := time.Parse(time.RFC3339Nano, parts[0]) if err != nil { logrus.WithField("logText", logText). Errorf("error parsing timestamp: %s", err) return "", time.Time{} } if len(parts) == 2 { return parts[1], ts } return "", ts } func withLabels(selector string) internalinterfaces.TweakListOptionsFunc { return func(opts *metav1.ListOptions) { opts.LabelSelector = selector } } type podLoggerEventHandler struct { cache.ResourceEventHandler added chan<- string deleted chan<- string } func (h *podLoggerEventHandler) OnAdd(obj interface{}, isInitialList bool) { pod := obj.(*corev1.Pod) logrus.WithField("pod", pod.Name).Debugf("log pod informer added a pod") h.added <- pod.Name } func (h *podLoggerEventHandler) OnUpdate(oldObj, newObj interface{}) { // purposefully empty, we don't need to do anything for logs on update } func (h *podLoggerEventHandler) OnDelete(obj interface{}) { // this may not be needed, the log stream Reader _should_ close on its own without // us needing to watch and close it // pod := obj.(*corev1.Pod) // h.deleted <- pod.Name } ================================================ FILE: agent/pkg/log/loki.go ================================================ package log import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strconv" "time" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" ) const ( // refer to https://grafana.com/docs/loki/latest/api/#query-loki-over-a-range-of-time lokiQueryRangePath = "/loki/api/v1/query_range" ) type RangeQueryResponse struct { Data struct { Result []struct { Stream struct { Cluster string `json:"cluster,omitempty"` Container string `json:"container,omitempty"` Namespace string `json:"namespace,omitempty"` Pod string `json:"pod,omitempty"` Job string `json:"job,omitempty"` } Values [][]string `json:"values,omitempty"` } ResultType string `json:"resultType,omitempty"` } Status string `json:"status,omitempty"` } type LokiAPIRequestor struct { client http.Client url string user string token string } func NewLokiAPIRequestor(url, user, token string) Requester { loki := LokiAPIRequestor{ url: url, user: user, token: token, client: http.Client{}, } return &loki } func (l *LokiAPIRequestor) Query(ctx context.Context, r types.LogRequest) (<-chan types.Message, error) { var sinceTime time.Time if r.Since != "" { var err error sinceTime, err = time.Parse(time.RFC3339, r.Since) if err != nil { return nil, errdefs.InvalidParameter(err) } } logs, err := l.getLogs(ctx, &sinceTime, r.Namespace, r.Name) return logs, err } func (l *LokiAPIRequestor) getLogs(ctx context.Context, since *time.Time, namespace, name string) (<-chan types.Message, error) { endpoint, err := url.JoinPath(l.url, lokiQueryRangePath) if err != nil { return nil, errors.Wrap(err, "failed to construct the query URL") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, errors.Wrap(err, "failed to construct the Loki request") } req.SetBasicAuth(l.user, l.token) query := url.Values{} if since != nil { query.Add("start", since.String()) if time.Since(*since) > time.Hour*24*30 { // max query range is 30 days query.Add("end", strconv.Itoa(int(since.Add(time.Hour*24*30).UnixNano()))) } } query.Add("query", fmt.Sprintf(`{namespace="%s",pod="%s"}`, namespace, name)) req.URL.RawQuery = query.Encode() logrus.Debugf("get log from %s", req.URL.String()) resp, err := l.client.Do(req) if err != nil { return nil, errors.Wrap(err, "failed to request the Loki service") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.Newf("failed to request the Loki, err[%s]", resp.Status) } var queryResp RangeQueryResponse err = json.NewDecoder(resp.Body).Decode(&queryResp) if err != nil { return nil, errors.Wrap(err, "failed to unmarshal json") } if len(queryResp.Data.Result) == 0 { return nil, errors.New("result contains ") } msgStream := make(chan types.Message, LogBufferSize) go func() { defer close(msgStream) for _, value := range queryResp.Data.Result[0].Values { timestamp, err := time.Parse(time.RFC3339, value[0]) if err != nil { logrus.Infof("failed to parse timestamp %s during parse log from %s:%s\n", value[0], namespace, name) continue } msgStream <- types.Message{ Timestamp: timestamp, Text: value[1], Name: name, Namespace: namespace, Instance: name, } } }() return msgStream, nil } ================================================ FILE: agent/pkg/metrics/exporter.go ================================================ // Copyright (c) Alex Ellis 2017 // Copyright (c) 2018 OpenFaaS Author(s) // Licensed under the MIT license. See LICENSE file in the project root for full license information. package metrics import ( "context" "fmt" "time" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/runtime" ) // Exporter is a prometheus metrics collector. // It is an implementation of https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#Collector. type Exporter struct { metricOptions MetricOptions runtime runtime.Runtime services []types.InferenceDeployment logger *logrus.Entry } // NewExporter creates a new exporter for the OpenFaaS gateway metrics func NewExporter(options MetricOptions, r runtime.Runtime) *Exporter { return &Exporter{ metricOptions: options, runtime: r, services: []types.InferenceDeployment{}, logger: logrus.WithField("component", "exporter"), } } // Describe is to describe the metrics for Prometheus func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { e.metricOptions.GatewayInferenceInvocation.Describe(ch) e.metricOptions.GatewayInferencesHistogram.Describe(ch) e.metricOptions.ServiceReplicasGauge.Describe(ch) e.metricOptions.ServiceAvailableReplicasGauge.Describe(ch) e.metricOptions.ServiceTargetLoad.Describe(ch) e.metricOptions.GatewayInferenceInvocationStarted.Describe(ch) e.metricOptions.GatewayInferenceInvocationInflight.Describe(ch) } // Collect collects data to be consumed by prometheus func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.metricOptions.GatewayInferenceInvocation.Collect(ch) e.metricOptions.GatewayInferencesHistogram.Collect(ch) e.metricOptions.ServiceReplicasGauge.Reset() e.metricOptions.ServiceAvailableReplicasGauge.Reset() e.metricOptions.ServiceTargetLoad.Reset() e.metricOptions.PodStartHistogram.Collect(ch) for _, service := range e.services { var serviceName string if len(service.Spec.Namespace) > 0 { serviceName = fmt.Sprintf("%s.%s", service.Spec.Name, service.Spec.Namespace) } else { serviceName = service.Spec.Name } // Initial services information if nil after recent deployment e.metricOptions.GatewayInferenceInvocationStarted.WithLabelValues(serviceName) e.metricOptions.GatewayInferenceInvocationInflight.WithLabelValues(serviceName) // Set current replica count e.metricOptions.ServiceReplicasGauge.WithLabelValues(serviceName). Set(float64(service.Status.Replicas)) // Set available replica count e.metricOptions.ServiceAvailableReplicasGauge.WithLabelValues(serviceName). Set(float64(service.Status.AvailableReplicas)) // Set target load if service.Spec.Scaling != nil { e.metricOptions.ServiceTargetLoad.WithLabelValues( serviceName, string(*service.Spec.Scaling.Type)). Set(float64(*service.Spec.Scaling.TargetLoad)) } } e.metricOptions.GatewayInferenceInvocationStarted.Collect(ch) e.metricOptions.GatewayInferenceInvocationInflight.Collect(ch) e.metricOptions.ServiceReplicasGauge.Collect(ch) e.metricOptions.ServiceAvailableReplicasGauge.Collect(ch) e.metricOptions.ServiceTargetLoad.Collect(ch) } // StartServiceWatcher starts a ticker and collects service replica counts to expose to prometheus func (e *Exporter) StartServiceWatcher( ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) quit := make(chan struct{}) go func() { for { select { case <-ticker.C: namespaces, err := e.runtime.NamespaceList(ctx) if err != nil { e.logger.Debug("unable to list namespaces: ", err) } services := []types.InferenceDeployment{} for _, namespace := range namespaces { nsServices, err := e.runtime.InferenceList(namespace) if err != nil { e.logger.Debug("unable to list services: ", err) continue } services = append(services, nsServices...) } e.services = services break case <-quit: return } } }() } ================================================ FILE: agent/pkg/metrics/metrics.go ================================================ // Copyright (c) Alex Ellis 2017. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. package metrics import ( "net/http" "sync" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) // MetricOptions to be used by web handlers type MetricOptions struct { GatewayInferenceInvocation *prometheus.CounterVec GatewayInferencesHistogram *prometheus.HistogramVec GatewayInferenceInvocationStarted *prometheus.CounterVec GatewayInferenceInvocationInflight *prometheus.GaugeVec ServiceReplicasGauge *prometheus.GaugeVec ServiceAvailableReplicasGauge *prometheus.GaugeVec ServiceTargetLoad *prometheus.GaugeVec PodStartHistogram *prometheus.HistogramVec } // ServiceMetricOptions provides RED metrics type ServiceMetricOptions struct { Histogram *prometheus.HistogramVec Counter *prometheus.CounterVec } // Synchronize to make sure MustRegister only called once var once = sync.Once{} // RegisterExporter registers with Prometheus for tracking func RegisterExporter(exporter *Exporter) { once.Do(func() { prometheus.MustRegister(exporter) }) } // PrometheusHandler Bootstraps prometheus for metrics collection func PrometheusHandler() http.Handler { return promhttp.Handler() } // BuildMetricsOptions builds metrics for tracking inferences in the API gateway func BuildMetricsOptions() MetricOptions { gatewayInferencesHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "gateway_inferences_seconds", Help: "Inference time taken", }, []string{"inference_name", "code"}) gatewayInferenceInvocation := prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "gateway", Subsystem: "inference", Name: "invocation_total", Help: "Inference metrics", }, []string{"inference_name", "code"}, ) serviceReplicas := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "gateway", Name: "service_count", Help: "Current count of replicas for inference", }, []string{"inference_name"}, ) serviceAvailableReplicas := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "gateway", Name: "service_available_count", Help: "Current count of available replicas for inference", }, []string{"inference_name"}, ) serviceTargetLoad := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "gateway", Name: "service_target_load", Help: "Target load for inference", }, []string{"inference_name", "scaling_type"}, ) gatewayInferenceInvocationStarted := prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "gateway", Subsystem: "inference", Name: "invocation_started", Help: "The total number of inference HTTP requests started.", }, []string{"inference_name"}, ) gatewayInferenceInvocationInflight := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "gateway", Subsystem: "inference", Name: "invocation_inflight", Help: "The number of inference HTTP inflight requests.", }, []string{"inference_name"}, ) podStartHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "pod_start_seconds", Help: "Pod start time taken", Buckets: prometheus.ExponentialBuckets(8, 1.5, 10), }, []string{"inference_name", "source_image"}) metricsOptions := MetricOptions{ GatewayInferencesHistogram: gatewayInferencesHistogram, GatewayInferenceInvocation: gatewayInferenceInvocation, ServiceReplicasGauge: serviceReplicas, ServiceAvailableReplicasGauge: serviceAvailableReplicas, ServiceTargetLoad: serviceTargetLoad, GatewayInferenceInvocationStarted: gatewayInferenceInvocationStarted, GatewayInferenceInvocationInflight: gatewayInferenceInvocationInflight, PodStartHistogram: podStartHistogram, } return metricsOptions } ================================================ FILE: agent/pkg/prom/prometheus_query.go ================================================ package prom import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strconv" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" ) // PrometheusQuery represents parameters for querying Prometheus type PrometheusQuery struct { Port int Host string Client *http.Client } type PrometheusQueryFetcher interface { Fetch(query string) (*VectorQueryResponse, error) } // NewPrometheusQuery create a NewPrometheusQuery func NewPrometheusQuery(host string, port int, client *http.Client) PrometheusQuery { return PrometheusQuery{ Client: client, Host: host, Port: port, } } func (p PrometheusQuery) AddMetrics(inferences []types.InferenceDeployment) { if len(inferences) > 0 { ns := inferences[0].Spec.Namespace q := fmt.Sprintf(`sum(gateway_inference_invocation_total{inference_name=~".*.%s"}) by (inference_name)`, ns) // Restrict query results to only inference names matching namespace suffix. results, err := p.Fetch(url.QueryEscape(q)) if err != nil { // log the error but continue, the mixIn will correctly handle the empty results. logrus.Debugf("Error querying Prometheus: %s\n", err.Error()) } mixIn(inferences, results) } } // Fetch queries aggregated stats func (q PrometheusQuery) Fetch(query string) (*VectorQueryResponse, error) { req, reqErr := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s:%d/api/v1/query?query=%s", q.Host, q.Port, query), nil) if reqErr != nil { return nil, reqErr } res, getErr := q.Client.Do(req) if getErr != nil { return nil, getErr } if res.Body != nil { defer res.Body.Close() } bytesOut, readErr := ioutil.ReadAll(res.Body) if readErr != nil { return nil, readErr } if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code from Prometheus want: %d, got: %d, body: %s", http.StatusOK, res.StatusCode, string(bytesOut)) } var values VectorQueryResponse unmarshalErr := json.Unmarshal(bytesOut, &values) if unmarshalErr != nil { return nil, fmt.Errorf("error unmarshalling result: %s, '%s'", unmarshalErr, string(bytesOut)) } return &values, nil } type VectorQueryResponse struct { Data struct { Result []struct { Metric struct { Code string `json:"code"` ScalingType string `json:"scaling_type"` InferenceName string `json:"inference_name"` } Value []interface{} `json:"value"` } } } func mixIn(inferences []types.InferenceDeployment, metrics *VectorQueryResponse) { if inferences == nil || metrics == nil { return } for i, inference := range inferences { for _, v := range metrics.Data.Result { if v.Metric.InferenceName == fmt.Sprintf("%s.%s", inference.Spec.Name, inference.Spec.Namespace) { metricValue := v.Value[1] switch value := metricValue.(type) { case string: f, err := strconv.ParseFloat(value, 64) if err != nil { logrus.Debugf("add_metrics: unable to convert value %q for metric: %s", value, err) continue } inferences[i].Status.InvocationCount += int32(f) } } } } } ================================================ FILE: agent/pkg/runtime/build.go ================================================ package runtime import ( "context" "fmt" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/agent/pkg/k8s" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) func (r generalRuntime) BuildList(ctx context.Context, namespace string) ( []types.Build, error) { res := []types.Build{} jobs, err := r.kubeClient.BatchV1().Jobs(namespace). List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=true", consts.AnnotationBuilding), }) if err != nil { if !k8serrors.IsNotFound(err) { return nil, errdefs.System(err) } } if jobs != nil { for _, job := range jobs.Items { build, err := k8s.AsBuild(job) if err != nil { return nil, errdefs.System(err) } res = append(res, build) } } return res, nil } func (r generalRuntime) BuildCreate(ctx context.Context, req types.Build, inference *v2alpha1.Inference, builderImage, buildkitdAddress, buildCtlBin, secret string) error { buildJob, err := k8s.MakeBuild(req, inference, builderImage, buildkitdAddress, buildCtlBin, secret) if err != nil { return errdefs.System(err) } if _, err := r.kubeClient.BatchV1().Jobs(req.Spec.Namespace). Create(ctx, buildJob, metav1.CreateOptions{}); err != nil { return errdefs.System(err) } return nil } func (r generalRuntime) BuildGet(ctx context.Context, namespace, buildName string) (types.Build, error) { job, err := r.kubeClient.BatchV1().Jobs(namespace).Get(ctx, buildName, metav1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return types.Build{}, errdefs.NotFound(err) } return types.Build{}, errdefs.System(err) } res, err := k8s.AsBuild(*job) if err != nil { return types.Build{}, errdefs.System(err) } return res, nil } ================================================ FILE: agent/pkg/runtime/cluster_info_get.go ================================================ package runtime import ( "strings" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/k8s" "github.com/tensorchord/openmodelz/agent/pkg/version" ) func (r generalRuntime) GetClusterInfo(cluster *types.ManagedCluster) error { info, err := k8s.GetKubernetesVersion(r.kubeClient) if err != nil { return err } cluster.KubernetesVersion = info.GitVersion cluster.Platform = info.Platform v := version.GetVersion() cluster.Version = v.Version resources, err := r.ListServerResource() if err != nil { return err } cluster.ServerResources = strings.Join(resources, ";") return nil } ================================================ FILE: agent/pkg/runtime/image_cache.go ================================================ package runtime import ( "context" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/k8s" modelzetes "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func (r generalRuntime) ImageCacheCreate(ctx context.Context, req types.ImageCache, inference *modelzetes.Inference) error { imageCache := k8s.MakeImageCache(req, inference) logrus.Infof("%v", imageCache) if _, err := r.kubefledgedClient.KubefledgedV1alpha3(). ImageCaches(req.Namespace). Create(ctx, imageCache, metav1.CreateOptions{}); err != nil { return err } return nil } ================================================ FILE: agent/pkg/runtime/inference_create.go ================================================ package runtime import ( "context" "fmt" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/agent/pkg/config" localconsts "github.com/tensorchord/openmodelz/agent/pkg/consts" ingressv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) func (r generalRuntime) InferenceCreate(ctx context.Context, req types.InferenceDeployment, cfg config.IngressConfig, event string, serverPort int) error { namespace := req.Spec.Namespace if r.eventEnabled { err := r.eventRecorder.CreateDeploymentEvent(namespace, req.Spec.Name, event, "") if err != nil { return err } } inf, err := makeInference(req) if err != nil { return err } // Create the ingress // TODO(gaocegege): Check if the domain is already used. if r.ingressEnabled { name := req.Spec.Labels[consts.LabelName] if r.ingressAnyIPToDomain { // Get the service with type=loadbalancer. svcs, err := r.kubeClient.CoreV1().Services("").List(ctx, metav1.ListOptions{}) if err != nil { return errdefs.System(fmt.Errorf("failed to list services: %v", err)) } if len(svcs.Items) == 0 { return errdefs.System(fmt.Errorf("no service with type=LoadBalancer")) } var externalIP string for _, s := range svcs.Items { if s.Spec.Type == v1.ServiceTypeLoadBalancer { if len(s.Status.LoadBalancer.Ingress) == 0 { continue } externalIP = s.Status.LoadBalancer.Ingress[0].IP break } } // Set the domain to ingressDomain := fmt.Sprintf("%s.%s", externalIP, localconsts.Domain) cfg.Domain = ingressDomain } domain, err := makeDomain(name, cfg.Domain) if err != nil { return errdefs.InvalidParameter(err) } // Set the domain. // Create the inference with the ingress domain. if inf.Spec.Annotations == nil { inf.Spec.Annotations = make(map[string]string) } if cfg.TLSEnabled { inf.Spec.Annotations[AnnotationDomain] = fmt.Sprintf("https://%s", domain) } else { inf.Spec.Annotations[AnnotationDomain] = fmt.Sprintf("http://%s", domain) } _, err = r.inferenceClient.TensorchordV2alpha1(). Inferences(namespace).Create( ctx, inf, metav1.CreateOptions{}) if err != nil { if k8serrors.IsAlreadyExists(err) { return errdefs.Conflict(err) } else { return errdefs.System(err) } } cfg.Domain = domain ingress, err := makeIngress(req, cfg) if err != nil { return err } _, err = r.ingressClient.TensorchordV1(). InferenceIngresses(cfg.Namespace). Create(ctx, ingress, metav1.CreateOptions{}) if err != nil { if k8serrors.IsAlreadyExists(err) { return errdefs.Conflict(err) } else { return errdefs.System(err) } } } else { // Set the gateway kubernetes service domain. domain := fmt.Sprintf("gateway.default:%d/api/v1/%s/%s/", serverPort, string(req.Spec.Framework), req.Spec.Name) if inf.Spec.Annotations == nil { inf.Spec.Annotations = make(map[string]string) } if cfg.TLSEnabled { inf.Spec.Annotations[AnnotationDomain] = fmt.Sprintf("https://%s", domain) } else { inf.Spec.Annotations[AnnotationDomain] = fmt.Sprintf("http://%s", domain) } _, err = r.inferenceClient.TensorchordV2alpha1(). Inferences(namespace).Create( ctx, inf, metav1.CreateOptions{}) if err != nil { if k8serrors.IsAlreadyExists(err) { return errdefs.Conflict(err) } else { return errdefs.System(err) } } } return nil } func makeInference(request types.InferenceDeployment) (*v2alpha1.Inference, error) { is := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: request.Spec.Name, Namespace: request.Spec.Namespace, Labels: map[string]string{ consts.LabelInferenceName: request.Spec.Name, }, }, Spec: v2alpha1.InferenceSpec{ Name: request.Spec.Name, Image: request.Spec.Image, Framework: v2alpha1.Framework(request.Spec.Framework), Port: request.Spec.Port, Command: request.Spec.Command, EnvVars: request.Spec.EnvVars, Secrets: request.Spec.Secrets, Constraints: request.Spec.Constraints, Labels: request.Spec.Labels, Annotations: request.Spec.Annotations, HTTPProbePath: request.Spec.HTTPProbePath, }, } if request.Spec.Scaling != nil { is.Spec.Scaling = &v2alpha1.ScalingConfig{ MinReplicas: request.Spec.Scaling.MinReplicas, MaxReplicas: request.Spec.Scaling.MaxReplicas, TargetLoad: request.Spec.Scaling.TargetLoad, ZeroDuration: request.Spec.Scaling.ZeroDuration, StartupDuration: request.Spec.Scaling.StartupDuration, } if request.Spec.Scaling.Type != nil { buf := v2alpha1.ScalingType(*request.Spec.Scaling.Type) is.Spec.Scaling.Type = &buf } } rr, err := createResources(request) if err != nil { return nil, errdefs.InvalidParameter(err) } is.Spec.Resources = &rr return is, nil } func makeIngress(request types.InferenceDeployment, cfg config.IngressConfig) (*ingressv1.InferenceIngress, error) { labels := map[string]string{ consts.LabelInferenceName: request.Spec.Name, consts.LabelInferenceNamespace: request.Spec.Namespace, } if request.Spec.Labels == nil { return nil, errdefs.InvalidParameter(fmt.Errorf("labels is required")) } ingress := &ingressv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Name: request.Spec.Name, Namespace: cfg.Namespace, Labels: labels, }, Spec: ingressv1.InferenceIngressSpec{ Domain: cfg.Domain, Framework: string(request.Spec.Framework), IngressType: "nginx", BypassGateway: false, Function: request.Spec.Name, TLS: &ingressv1.InferenceIngressTLS{ Enabled: cfg.TLSEnabled, }, }, } annotation := map[string]string{} if value, exist := request.Spec.Annotations[consts.AnnotationControlPlaneKey]; exist { annotation[consts.AnnotationControlPlaneKey] = value } ingress.Annotations = annotation return ingress, nil } ================================================ FILE: agent/pkg/runtime/inference_delete.go ================================================ package runtime import ( "context" ingressclientset "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" inferenceclientset "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/tensorchord/openmodelz/agent/errdefs" ) func (r generalRuntime) InferenceDelete(ctx context.Context, namespace, inferenceName, ingressNamespace, event string) error { if r.eventEnabled { err := r.eventRecorder.CreateDeploymentEvent(namespace, inferenceName, event, "") if err != nil { return err } } getOpts := metav1.GetOptions{} // This makes sure we don't delete non-labelled deployments _, err := r.inferenceClient.TensorchordV2alpha1(). Inferences(namespace). Get(context.TODO(), inferenceName, getOpts) if err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(err) } else { return errdefs.System(err) } } if err := deleteInference(ctx, namespace, r.inferenceClient, r.ingressClient, ingressNamespace, inferenceName, r.ingressEnabled); err != nil { return err } return nil } func deleteInference(ctx context.Context, namespace string, clientset inferenceclientset.Interface, ingressClient ingressclientset.Interface, baseNamespace string, inferenceName string, ingressEnabled bool) error { foregroundPolicy := metav1.DeletePropagationForeground opts := &metav1.DeleteOptions{PropagationPolicy: &foregroundPolicy} if deployErr := clientset.TensorchordV2alpha1().Inferences(namespace). Delete(ctx, inferenceName, *opts); deployErr != nil { if k8serrors.IsNotFound(deployErr) { return errdefs.NotFound(deployErr) } else { return errdefs.System(deployErr) } } if ingressEnabled && ingressClient != nil { if err := ingressClient.TensorchordV1().InferenceIngresses(baseNamespace).Delete(ctx, inferenceName, *opts); err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(err) } else { return errdefs.System(err) } } } return nil } ================================================ FILE: agent/pkg/runtime/inference_exec.go ================================================ package runtime import ( "errors" "fmt" "io" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" clientsetscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/remotecommand" "github.com/tensorchord/openmodelz/agent/errdefs" ) const ( // Time allowed to write a message to the peer. writeWait = 10 * time.Second // ctrl+d to close terminal. endOfTransmission = "\u0004" ) func (r generalRuntime) InferenceExec(ctx *gin.Context, namespace, instance string, commands []string, tty bool) error { pod, err := r.kubeClient.CoreV1().Pods(namespace).Get( ctx.Request.Context(), instance, metav1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(errors.New("inference instance not found")) } return errdefs.System(err) } if pod.Status.Phase != v1.PodRunning { return errdefs.Unavailable(errors.New("inference instance is not running")) } req := r.kubeClient.CoreV1().RESTClient().Post(). Resource("pods"). Name(instance). Namespace(namespace). SubResource("exec") req.VersionedParams(&v1.PodExecOptions{ Command: commands, Stdin: tty, Stdout: true, Stderr: true, TTY: tty, }, clientsetscheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(r.clientConfig, http.MethodPost, req.URL()) if err != nil { return errdefs.System(err) } if tty { t, err := newTerminalSession(fmt.Sprintf("exec/%s/%s/%s", namespace, instance, rand.String(5)), ctx.Request, ctx.Writer) if err != nil { return err } defer t.Close() logrus.WithField("exec", exec).Debugf("executing command") if err = exec.StreamWithContext(ctx.Request.Context(), remotecommand.StreamOptions{ Stdin: t, Stdout: t, Stderr: t, TerminalSizeQueue: t, Tty: true, }); err != nil { // The response is already hijacked, so we can't return an error. logrus.Warnf("exec stream failed: %v", err) return nil } } else { logrus.Debugf("running without tty") if err := exec.StreamWithContext(ctx.Request.Context(), remotecommand.StreamOptions{ Stdout: ctx.Writer, Stderr: ctx.Writer, Tty: tty, }); err != nil { return errdefs.System(err) } } return nil } type PtyHandler interface { io.Reader io.Writer remotecommand.TerminalSizeQueue } // TerminalMessage is the messaging protocol between ShellController and TerminalSession. // // OP DIRECTION FIELD(S) USED DESCRIPTION // --------------------------------------------------------------------- // bind fe->be SessionID Id sent back from TerminalResponse // stdin fe->be Data Keystrokes/paste buffer // resize fe->be Rows, Cols New terminal size // stdout be->fe Data Output from the process // toast be->fe Data OOB message to be shown to the user type TerminalMessage struct { ID string `json:"id,omitempty"` Op string `json:"op,omitempty"` Data string `json:"data,omitempty"` Rows uint16 `json:"rows,omitempty"` Cols uint16 `json:"cols,omitempty"` } // TerminalSession type TerminalSession struct { ID string wsConn *websocket.Conn sizeChan chan remotecommand.TerminalSize doneChan chan struct{} } // TerminalSize handles pty->process resize events // Called in a loop from remotecommand as long as the process is running func (t *TerminalSession) Next() *remotecommand.TerminalSize { select { case size := <-t.sizeChan: return &size case <-t.doneChan: return nil } } // Read handles pty->process messages (stdin, resize) // Called in a loop from remotecommand as long as the process is running func (t *TerminalSession) Read(p []byte) (int, error) { var msg TerminalMessage if err := t.wsConn.ReadJSON(&msg); err != nil { logrus.Debugf("%s: read json failed: %v", t.ID, err) return copy(p, endOfTransmission), err } logrus.Debugf("%s: read json: %v", t.ID, msg) switch msg.Op { case "stdin": logrus.WithField("remote", t.wsConn.RemoteAddr()).Debugf("%s: read %d bytes: %s", t.ID, len(msg.Data), msg.Data) size := copy(p, msg.Data) logrus.WithField("remote", t.wsConn.RemoteAddr()).Debugf("%s: copied %d bytes: %s", t.ID, size, p) return size, nil case "resize": t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows} return 0, nil default: logrus.WithField("remote", t.wsConn.RemoteAddr()).Debugf("%s: unknown message type '%s'", t.ID, msg.Op) return copy(p, endOfTransmission), fmt.Errorf("unknown message type '%s'", msg.Op) } } // Write handles process->pty stdout // Called from remotecommand whenever there is any output func (t *TerminalSession) Write(p []byte) (int, error) { msg := TerminalMessage{ Op: "stdout", Data: string(p), } logrus.WithField("remote", t.wsConn.RemoteAddr()).Debugf("%s: write %d bytes: %s", t.ID, len(p), string(p)) if err := t.wsConn.WriteJSON(msg); err != nil { logrus.WithField("remote", t.wsConn.RemoteAddr()).Debugf("write message failed: %v", err) return 0, err } return len(p), nil } func (t *TerminalSession) Close() error { close(t.doneChan) return t.wsConn.Close() } func newTerminalSession(id string, r *http.Request, w http.ResponseWriter) (*TerminalSession, error) { upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return nil, err } return &TerminalSession{ ID: id, wsConn: conn, sizeChan: make(chan remotecommand.TerminalSize), doneChan: make(chan struct{}), }, nil } ================================================ FILE: agent/pkg/runtime/inference_get.go ================================================ package runtime import ( "fmt" k8serrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/client-go/listers/apps/v1" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/agent/pkg/k8s" apis "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/client/listers/modelzetes/v2alpha1" ) func (r generalRuntime) InferenceGet(namespace, inferenceName string) ( *types.InferenceDeployment, error) { return inferenceGet(namespace, inferenceName, r.inferenceInformer.Lister(), r.deploymentInformer.Lister()) } func (r generalRuntime) InferenceGetCRD(namespace, name string) (*apis.Inference, error) { inference, err := r.inferenceInformer.Lister().Inferences(namespace).Get(name) if err != nil { if k8serrors.IsNotFound(err) { return nil, errdefs.NotFound(err) } return nil, err } return inference, nil } // inferenceGet returns a inference or nil if not found func inferenceGet(namespace string, inferenceName string, infLister v2alpha1.InferenceLister, lister v1.DeploymentLister) (*types.InferenceDeployment, error) { inference, err := infLister.Inferences(namespace).Get(inferenceName) if err != nil { if k8serrors.IsNotFound(err) { return nil, errdefs.NotFound(err) } return nil, err } item, err := lister.Deployments(namespace). Get(inferenceName) if err != nil { if !k8serrors.IsNotFound(err) { return nil, err } } inf := k8s.AsInferenceDeployment(inference, item) if inf != nil { return inf, nil } return nil, fmt.Errorf("inference: %s not found", inferenceName) } ================================================ FILE: agent/pkg/runtime/inference_instance.go ================================================ package runtime import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" v1 "k8s.io/client-go/listers/core/v1" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/agent/pkg/k8s" ) func (r generalRuntime) InferenceInstanceList(namespace, inferenceName string) ( []types.InferenceDeploymentInstance, error) { return getInstances(namespace, inferenceName, r.podInformer.Lister()) } func getInstances(functionNamespace string, functionName string, lister v1.PodLister) ([]types.InferenceDeploymentInstance, error) { instances := make([]types.InferenceDeploymentInstance, 0) items, err := lister.List( labels.SelectorFromSet(k8s.MakeLabelSelector(functionName))) if err != nil { if k8serrors.IsNotFound(err) { return nil, nil } return nil, errdefs.System(err) } for _, item := range items { if item != nil { instance := k8s.InstanceFromPod(*item) if instance != nil { instances = append(instances, *instance) } } } return instances, nil } ================================================ FILE: agent/pkg/runtime/inference_list.go ================================================ package runtime import ( "sort" mv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" modelzetesv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/listers/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" appsv1 "k8s.io/api/apps/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" v1 "k8s.io/client-go/listers/apps/v1" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/agent/pkg/k8s" ) func (r generalRuntime) InferenceList(namespace string) ([]types.InferenceDeployment, error) { infLister := r.inferenceInformer.Lister() deploymentLister := r.deploymentInformer.Lister() functions, err := inferenceList(namespace, infLister, deploymentLister) if err != nil { return nil, err } return functions, nil } func inferenceList(functionNamespace string, infLister modelzetesv2alpha1.InferenceLister, deploymentLister v1.DeploymentLister) ([]types.InferenceDeployment, error) { functions := []types.InferenceDeployment{} sel := labels.NewSelector() req, err := labels.NewRequirement(consts.LabelInferenceName, selection.Exists, []string{}) if err != nil { return functions, errdefs.NotFound(err) } onlyFunctions := sel.Add(*req) inferences, err := infLister.Inferences(functionNamespace). List(labels.Everything()) if err != nil { if k8serrors.IsNotFound(err) { return functions, nil } else { return functions, errdefs.System(err) } } deploys, err := deploymentLister.Deployments(functionNamespace).List(onlyFunctions) if err != nil { if k8serrors.IsNotFound(err) { return getInferences(inferences, deploys) } else { return functions, errdefs.System(err) } } return getInferences(inferences, deploys) } func getInferences(inferences []*mv2alpha1.Inference, deploys []*appsv1.Deployment) ([]types.InferenceDeployment, error) { sort.Slice(inferences, func(i, j int) bool { return (*inferences[i]).Name < (*inferences[j]).Name }) sort.Slice(deploys, func(i, j int) bool { return (*deploys[i]).Name < (*deploys[j]).Name }) res := []types.InferenceDeployment{} j := 0 for i := range inferences { if j >= len(deploys) { res = append(res, *k8s.AsInferenceDeployment(inferences[i], nil)) } else if inferences[i].Name != deploys[j].Name { res = append(res, *k8s.AsInferenceDeployment(inferences[i], nil)) } else { res = append(res, *k8s.AsInferenceDeployment(inferences[i], deploys[j])) j++ } } return res, nil } ================================================ FILE: agent/pkg/runtime/inference_replicas.go ================================================ package runtime import ( "context" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" ) func (r generalRuntime) InferenceScale(ctx context.Context, namespace string, req types.ScaleServiceRequest, inf *types.InferenceDeployment) (err error) { options := metav1.GetOptions{ TypeMeta: metav1.TypeMeta{ Kind: "Deployment", APIVersion: "apps/v1", }, } deployment, err := r.kubeClient.AppsV1().Deployments(namespace). Get(ctx, req.ServiceName, options) if err != nil { return errdefs.InvalidParameter(err) } oldReplicas := *deployment.Spec.Replicas replicas := int32(req.Replicas) if inf.Spec.Scaling != nil { minReplicas := *inf.Spec.Scaling.MinReplicas if replicas < minReplicas { replicas = minReplicas } maxReplicas := *inf.Spec.Scaling.MaxReplicas if replicas > maxReplicas { replicas = maxReplicas } } if replicas >= consts.MaxReplicas { replicas = consts.MaxReplicas } if oldReplicas == replicas { return nil } event := types.DeploymentScaleDownEvent if oldReplicas < replicas { event = types.DeploymentScaleUpEvent } var building bool if r.buildEnabled { _, building = deployment.Annotations[consts.AnnotationBuilding] } if building { event = types.DeploymentScaleBlockEvent req.EventMessage = "Deployment is building image, scale is blocked" replicas = 0 } if r.eventEnabled { // Only create event when the first time scale up/down if req.Attempt == 0 { err = r.eventRecorder.CreateDeploymentEvent(namespace, deployment.Name, event, req.EventMessage) if err != nil { return err } } } deployment.Spec.Replicas = &replicas r.logger.WithField("deployment", deployment.Name). WithField("namespace", namespace). WithField("replicas", replicas).Debug("scaling deployment") if _, err = r.kubeClient.AppsV1().Deployments(namespace). Update(ctx, deployment, metav1.UpdateOptions{}); err != nil { return errdefs.System(err) } return nil } ================================================ FILE: agent/pkg/runtime/inference_update.go ================================================ package runtime import ( "context" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" inferenceclientset "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" ) func (r generalRuntime) InferenceUpdate(ctx context.Context, namespace string, req types.InferenceDeployment, event string) (err error) { if r.eventEnabled { err := r.eventRecorder.CreateDeploymentEvent(namespace, req.Spec.Name, event, req.Status.EventMessage) if err != nil { return err } } if err = updateInference(ctx, namespace, r.inferenceClient, req); err != nil { return err } return nil } func updateInference( ctx context.Context, functionNamespace string, inferenceClient inferenceclientset.Interface, request types.InferenceDeployment) (err error) { actual, err := inferenceClient.TensorchordV2alpha1(). Inferences(functionNamespace).Get( ctx, request.Spec.Name, metav1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(err) } else { return errdefs.System(err) } } expected := actual.DeepCopy() if request.Spec.Image != "" { expected.Spec.Image = request.Spec.Image } if request.Spec.Scaling != nil { expected.Spec.Scaling = &v2alpha1.ScalingConfig{ MinReplicas: request.Spec.Scaling.MinReplicas, MaxReplicas: request.Spec.Scaling.MaxReplicas, TargetLoad: request.Spec.Scaling.TargetLoad, ZeroDuration: request.Spec.Scaling.ZeroDuration, StartupDuration: request.Spec.Scaling.StartupDuration, } if request.Spec.Scaling.Type != nil { expected.Spec.Scaling.Type = new(v2alpha1.ScalingType) *expected.Spec.Scaling.Type = v2alpha1.ScalingType(*request.Spec.Scaling.Type) } } if request.Spec.EnvVars != nil { expected.Spec.EnvVars = request.Spec.EnvVars } if request.Spec.Secrets != nil { expected.Spec.Secrets = request.Spec.Secrets } if request.Spec.Constraints != nil { expected.Spec.Constraints = request.Spec.Constraints } if request.Spec.Labels != nil { expected.Spec.Labels = request.Spec.Labels } if request.Spec.Annotations != nil { expected.Spec.Annotations = request.Spec.Annotations } if request.Spec.Resources != nil { rr, err := createResources(request) if err != nil { return errdefs.InvalidParameter(err) } expected.Spec.Resources = &rr } if _, err := inferenceClient.TensorchordV2alpha1(). Inferences(functionNamespace).Update( ctx, expected, metav1.UpdateOptions{}); err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(err) } else { return errdefs.System(err) } } return nil } ================================================ FILE: agent/pkg/runtime/mock/mock.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: pkg/runtime/runtime.go // Package mock is a generated GoMock package. package mock import ( context "context" reflect "reflect" gin "github.com/gin-gonic/gin" gomock "github.com/golang/mock/gomock" types "github.com/tensorchord/openmodelz/agent/api/types" config "github.com/tensorchord/openmodelz/agent/pkg/config" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" ) // MockRuntime is a mock of Runtime interface. type MockRuntime struct { ctrl *gomock.Controller recorder *MockRuntimeMockRecorder } // MockRuntimeMockRecorder is the mock recorder for MockRuntime. type MockRuntimeMockRecorder struct { mock *MockRuntime } // NewMockRuntime creates a new mock instance. func NewMockRuntime(ctrl *gomock.Controller) *MockRuntime { mock := &MockRuntime{ctrl: ctrl} mock.recorder = &MockRuntimeMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockRuntime) EXPECT() *MockRuntimeMockRecorder { return m.recorder } // BuildCreate mocks base method. func (m *MockRuntime) BuildCreate(ctx context.Context, req types.Build, inference *v2alpha1.Inference, builderImage, buildkitdAddress, buildCtlBin, secret string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildCreate", ctx, req, inference, builderImage, buildkitdAddress, buildCtlBin, secret) ret0, _ := ret[0].(error) return ret0 } // BuildCreate indicates an expected call of BuildCreate. func (mr *MockRuntimeMockRecorder) BuildCreate(ctx, req, inference, builderImage, buildkitdAddress, buildCtlBin, secret interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildCreate", reflect.TypeOf((*MockRuntime)(nil).BuildCreate), ctx, req, inference, builderImage, buildkitdAddress, buildCtlBin, secret) } // BuildGet mocks base method. func (m *MockRuntime) BuildGet(ctx context.Context, namespace, buildName string) (types.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildGet", ctx, namespace, buildName) ret0, _ := ret[0].(types.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // BuildGet indicates an expected call of BuildGet. func (mr *MockRuntimeMockRecorder) BuildGet(ctx, namespace, buildName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildGet", reflect.TypeOf((*MockRuntime)(nil).BuildGet), ctx, namespace, buildName) } // BuildList mocks base method. func (m *MockRuntime) BuildList(ctx context.Context, namespace string) ([]types.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildList", ctx, namespace) ret0, _ := ret[0].([]types.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // BuildList indicates an expected call of BuildList. func (mr *MockRuntimeMockRecorder) BuildList(ctx, namespace interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildList", reflect.TypeOf((*MockRuntime)(nil).BuildList), ctx, namespace) } // GetClusterInfo mocks base method. func (m *MockRuntime) GetClusterInfo(cluster *types.ManagedCluster) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClusterInfo", cluster) ret0, _ := ret[0].(error) return ret0 } // GetClusterInfo indicates an expected call of GetClusterInfo. func (mr *MockRuntimeMockRecorder) GetClusterInfo(cluster interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterInfo", reflect.TypeOf((*MockRuntime)(nil).GetClusterInfo), cluster) } // ImageCacheCreate mocks base method. func (m *MockRuntime) ImageCacheCreate(ctx context.Context, req types.ImageCache, inference *v2alpha1.Inference) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageCacheCreate", ctx, req, inference) ret0, _ := ret[0].(error) return ret0 } // ImageCacheCreate indicates an expected call of ImageCacheCreate. func (mr *MockRuntimeMockRecorder) ImageCacheCreate(ctx, req, inference interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageCacheCreate", reflect.TypeOf((*MockRuntime)(nil).ImageCacheCreate), ctx, req, inference) } // InferenceCreate mocks base method. func (m *MockRuntime) InferenceCreate(ctx context.Context, req types.InferenceDeployment, cfg config.IngressConfig, event string, serverPort int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceCreate", ctx, req, cfg, event, serverPort) ret0, _ := ret[0].(error) return ret0 } // InferenceCreate indicates an expected call of InferenceCreate. func (mr *MockRuntimeMockRecorder) InferenceCreate(ctx, req, cfg, event, serverPort interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceCreate", reflect.TypeOf((*MockRuntime)(nil).InferenceCreate), ctx, req, cfg, event, serverPort) } // InferenceDelete mocks base method. func (m *MockRuntime) InferenceDelete(ctx context.Context, namespace, inferenceName, ingressNamespace, event string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceDelete", ctx, namespace, inferenceName, ingressNamespace, event) ret0, _ := ret[0].(error) return ret0 } // InferenceDelete indicates an expected call of InferenceDelete. func (mr *MockRuntimeMockRecorder) InferenceDelete(ctx, namespace, inferenceName, ingressNamespace, event interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceDelete", reflect.TypeOf((*MockRuntime)(nil).InferenceDelete), ctx, namespace, inferenceName, ingressNamespace, event) } // InferenceExec mocks base method. func (m *MockRuntime) InferenceExec(ctx *gin.Context, namespace, instance string, commands []string, tty bool) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceExec", ctx, namespace, instance, commands, tty) ret0, _ := ret[0].(error) return ret0 } // InferenceExec indicates an expected call of InferenceExec. func (mr *MockRuntimeMockRecorder) InferenceExec(ctx, namespace, instance, commands, tty interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceExec", reflect.TypeOf((*MockRuntime)(nil).InferenceExec), ctx, namespace, instance, commands, tty) } // InferenceGet mocks base method. func (m *MockRuntime) InferenceGet(namespace, inferenceName string) (*types.InferenceDeployment, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceGet", namespace, inferenceName) ret0, _ := ret[0].(*types.InferenceDeployment) ret1, _ := ret[1].(error) return ret0, ret1 } // InferenceGet indicates an expected call of InferenceGet. func (mr *MockRuntimeMockRecorder) InferenceGet(namespace, inferenceName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceGet", reflect.TypeOf((*MockRuntime)(nil).InferenceGet), namespace, inferenceName) } // InferenceGetCRD mocks base method. func (m *MockRuntime) InferenceGetCRD(namespace, name string) (*v2alpha1.Inference, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceGetCRD", namespace, name) ret0, _ := ret[0].(*v2alpha1.Inference) ret1, _ := ret[1].(error) return ret0, ret1 } // InferenceGetCRD indicates an expected call of InferenceGetCRD. func (mr *MockRuntimeMockRecorder) InferenceGetCRD(namespace, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceGetCRD", reflect.TypeOf((*MockRuntime)(nil).InferenceGetCRD), namespace, name) } // InferenceInstanceList mocks base method. func (m *MockRuntime) InferenceInstanceList(namespace, inferenceName string) ([]types.InferenceDeploymentInstance, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceInstanceList", namespace, inferenceName) ret0, _ := ret[0].([]types.InferenceDeploymentInstance) ret1, _ := ret[1].(error) return ret0, ret1 } // InferenceInstanceList indicates an expected call of InferenceInstanceList. func (mr *MockRuntimeMockRecorder) InferenceInstanceList(namespace, inferenceName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceInstanceList", reflect.TypeOf((*MockRuntime)(nil).InferenceInstanceList), namespace, inferenceName) } // InferenceList mocks base method. func (m *MockRuntime) InferenceList(namespace string) ([]types.InferenceDeployment, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceList", namespace) ret0, _ := ret[0].([]types.InferenceDeployment) ret1, _ := ret[1].(error) return ret0, ret1 } // InferenceList indicates an expected call of InferenceList. func (mr *MockRuntimeMockRecorder) InferenceList(namespace interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceList", reflect.TypeOf((*MockRuntime)(nil).InferenceList), namespace) } // InferenceScale mocks base method. func (m *MockRuntime) InferenceScale(ctx context.Context, namespace string, req types.ScaleServiceRequest, inf *types.InferenceDeployment) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceScale", ctx, namespace, req, inf) ret0, _ := ret[0].(error) return ret0 } // InferenceScale indicates an expected call of InferenceScale. func (mr *MockRuntimeMockRecorder) InferenceScale(ctx, namespace, req, inf interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceScale", reflect.TypeOf((*MockRuntime)(nil).InferenceScale), ctx, namespace, req, inf) } // InferenceUpdate mocks base method. func (m *MockRuntime) InferenceUpdate(ctx context.Context, namespace string, req types.InferenceDeployment, event string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "InferenceUpdate", ctx, namespace, req, event) ret0, _ := ret[0].(error) return ret0 } // InferenceUpdate indicates an expected call of InferenceUpdate. func (mr *MockRuntimeMockRecorder) InferenceUpdate(ctx, namespace, req, event interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InferenceUpdate", reflect.TypeOf((*MockRuntime)(nil).InferenceUpdate), ctx, namespace, req, event) } // NamespaceCreate mocks base method. func (m *MockRuntime) NamespaceCreate(ctx context.Context, name string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NamespaceCreate", ctx, name) ret0, _ := ret[0].(error) return ret0 } // NamespaceCreate indicates an expected call of NamespaceCreate. func (mr *MockRuntimeMockRecorder) NamespaceCreate(ctx, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NamespaceCreate", reflect.TypeOf((*MockRuntime)(nil).NamespaceCreate), ctx, name) } // NamespaceDelete mocks base method. func (m *MockRuntime) NamespaceDelete(ctx context.Context, name string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NamespaceDelete", ctx, name) ret0, _ := ret[0].(error) return ret0 } // NamespaceDelete indicates an expected call of NamespaceDelete. func (mr *MockRuntimeMockRecorder) NamespaceDelete(ctx, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NamespaceDelete", reflect.TypeOf((*MockRuntime)(nil).NamespaceDelete), ctx, name) } // NamespaceGet mocks base method. func (m *MockRuntime) NamespaceGet(ctx context.Context, name string) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NamespaceGet", ctx, name) ret0, _ := ret[0].(bool) return ret0 } // NamespaceGet indicates an expected call of NamespaceGet. func (mr *MockRuntimeMockRecorder) NamespaceGet(ctx, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NamespaceGet", reflect.TypeOf((*MockRuntime)(nil).NamespaceGet), ctx, name) } // NamespaceList mocks base method. func (m *MockRuntime) NamespaceList(ctx context.Context) ([]string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NamespaceList", ctx) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } // NamespaceList indicates an expected call of NamespaceList. func (mr *MockRuntimeMockRecorder) NamespaceList(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NamespaceList", reflect.TypeOf((*MockRuntime)(nil).NamespaceList), ctx) } // ServerDeleteNode mocks base method. func (m *MockRuntime) ServerDeleteNode(ctx context.Context, name string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServerDeleteNode", ctx, name) ret0, _ := ret[0].(error) return ret0 } // ServerDeleteNode indicates an expected call of ServerDeleteNode. func (mr *MockRuntimeMockRecorder) ServerDeleteNode(ctx, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerDeleteNode", reflect.TypeOf((*MockRuntime)(nil).ServerDeleteNode), ctx, name) } // ServerLabelCreate mocks base method. func (m *MockRuntime) ServerLabelCreate(ctx context.Context, name string, spec types.ServerSpec) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServerLabelCreate", ctx, name, spec) ret0, _ := ret[0].(error) return ret0 } // ServerLabelCreate indicates an expected call of ServerLabelCreate. func (mr *MockRuntimeMockRecorder) ServerLabelCreate(ctx, name, spec interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerLabelCreate", reflect.TypeOf((*MockRuntime)(nil).ServerLabelCreate), ctx, name, spec) } // ServerList mocks base method. func (m *MockRuntime) ServerList(ctx context.Context) ([]types.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServerList", ctx) ret0, _ := ret[0].([]types.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // ServerList indicates an expected call of ServerList. func (mr *MockRuntimeMockRecorder) ServerList(ctx interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerList", reflect.TypeOf((*MockRuntime)(nil).ServerList), ctx) } ================================================ FILE: agent/pkg/runtime/namespace.go ================================================ package runtime import ( "context" "fmt" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) func (r generalRuntime) NamespaceList(ctx context.Context) ([]string, error) { ns, err := r.kubeClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=true", consts.LabelNamespace), }) if err != nil { if k8serrors.IsNotFound(err) { return nil, nil } else { return nil, errdefs.System(err) } } res := make([]string, len(ns.Items)) for i, n := range ns.Items { res[i] = n.Name } return res, nil } func (r generalRuntime) NamespaceCreate(ctx context.Context, name string) error { ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: name, Labels: map[string]string{ consts.LabelNamespace: "true", }, }, } _, err := r.kubeClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}) if err != nil { if k8serrors.IsAlreadyExists(err) { return errdefs.Conflict(err) } else { return errdefs.System(err) } } return nil } func (r generalRuntime) NamespaceGet(ctx context.Context, name string) bool { _, err := r.kubeClient.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) if err != nil { return false } return true } func (r generalRuntime) NamespaceDelete(ctx context.Context, name string) error { err := r.kubeClient.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{}) return err } ================================================ FILE: agent/pkg/runtime/node.go ================================================ package runtime import ( "context" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func (r generalRuntime) ListServerResource() ([]string, error) { resources := []string{} listOptions := metav1.ListOptions{ LabelSelector: consts.LabelServerResource, } nodes, err := r.kubeClient.CoreV1().Nodes().List(context.Background(), listOptions) if err != nil { logrus.Errorf("failed to list nodes: %v", err) return resources, err } for _, node := range nodes.Items { resources = append(resources, node.Labels[consts.LabelServerResource]) } return resources, nil } ================================================ FILE: agent/pkg/runtime/runtime.go ================================================ package runtime import ( "context" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" apicorev1 "k8s.io/api/core/v1" appsv1 "k8s.io/client-go/informers/apps/v1" corev1 "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" clientsetscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" kubefledged "github.com/senthilrch/kube-fledged/pkg/client/clientset/versioned" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/config" "github.com/tensorchord/openmodelz/agent/pkg/event" ingressclient "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" apis "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" modelzetes "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" clientset "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" modelzv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions/modelzetes/v2alpha1" ) type Runtime interface { // build BuildList(ctx context.Context, namespace string) ([]types.Build, error) BuildCreate(ctx context.Context, req types.Build, inference *v2alpha1.Inference, builderImage, buildkitdAddress, buildCtlBin, secret string) error BuildGet(ctx context.Context, namespace, buildName string) (types.Build, error) // cache ImageCacheCreate(ctx context.Context, req types.ImageCache, inference *modelzetes.Inference) error // inference InferenceCreate(ctx context.Context, req types.InferenceDeployment, cfg config.IngressConfig, event string, serverPort int) error InferenceDelete(ctx context.Context, namespace, inferenceName, ingressNamespace, event string) error InferenceExec(ctx *gin.Context, namespace, instance string, commands []string, tty bool) error InferenceGet(namespace, inferenceName string) (*types.InferenceDeployment, error) InferenceGetCRD(namespace, name string) (*apis.Inference, error) InferenceInstanceList(namespace, inferenceName string) ([]types.InferenceDeploymentInstance, error) InferenceList(namespace string) ([]types.InferenceDeployment, error) InferenceScale(ctx context.Context, namespace string, req types.ScaleServiceRequest, inf *types.InferenceDeployment) error InferenceUpdate(ctx context.Context, namespace string, req types.InferenceDeployment, event string) (err error) // namespace NamespaceList(ctx context.Context) ([]string, error) NamespaceCreate(ctx context.Context, name string) error NamespaceGet(ctx context.Context, name string) bool NamespaceDelete(ctx context.Context, name string) error // server ServerDeleteNode(ctx context.Context, name string) error ServerLabelCreate(ctx context.Context, name string, spec types.ServerSpec) error ServerList(ctx context.Context) ([]types.Server, error) // managed cluster GetClusterInfo(cluster *types.ManagedCluster) error } type generalRuntime struct { endpointsInformer corev1.EndpointsInformer deploymentInformer appsv1.DeploymentInformer inferenceInformer modelzv2alpha1.InferenceInformer podInformer corev1.PodInformer kubeClient kubernetes.Interface clientConfig *rest.Config restClient *rest.RESTClient ingressClient ingressclient.Interface inferenceClient clientset.Interface kubefledgedClient kubefledged.Interface logger *logrus.Entry eventRecorder event.Interface ingressEnabled bool ingressAnyIPToDomain bool eventEnabled bool buildEnabled bool } func New(clientConfig *rest.Config, endpointsInformer corev1.EndpointsInformer, deploymentInformer appsv1.DeploymentInformer, inferenceInformer modelzv2alpha1.InferenceInformer, podInformer corev1.PodInformer, kubeClient kubernetes.Interface, ingressClient ingressclient.Interface, kubefledgedClient kubefledged.Interface, inferenceClient clientset.Interface, eventRecorder event.Interface, ingressEnabled bool, eventEnabled bool, buildEnabled bool, ingressAnyIPToDomain bool, ) (Runtime, error) { r := generalRuntime{ endpointsInformer: endpointsInformer, deploymentInformer: deploymentInformer, inferenceInformer: inferenceInformer, podInformer: podInformer, kubeClient: kubeClient, kubefledgedClient: kubefledgedClient, clientConfig: clientConfig, ingressClient: ingressClient, inferenceClient: inferenceClient, logger: logrus.WithField("component", "runtime"), eventRecorder: eventRecorder, ingressEnabled: ingressEnabled, ingressAnyIPToDomain: ingressAnyIPToDomain, eventEnabled: eventEnabled, buildEnabled: buildEnabled, } // Ref https://github.com/operator-framework/operator-sdk/issues/1570 clientConfig.APIPath = "api" clientConfig.GroupVersion = &apicorev1.SchemeGroupVersion clientConfig.NegotiatedSerializer = clientsetscheme.Codecs r.clientConfig = clientConfig restClient, err := rest.RESTClientFor(clientConfig) if err != nil { return r, err } r.restClient = restClient return r, nil } ================================================ FILE: agent/pkg/runtime/server_delete.go ================================================ package runtime import ( "context" "github.com/tensorchord/openmodelz/agent/errdefs" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func (r generalRuntime) ServerDeleteNode(ctx context.Context, name string) error { err := r.kubeClient.CoreV1().Nodes().Delete(ctx, name, metav1.DeleteOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(err) } return errdefs.System(err) } return nil } ================================================ FILE: agent/pkg/runtime/server_label_create.go ================================================ package runtime import ( "context" "path/filepath" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func (r generalRuntime) ServerLabelCreate(ctx context.Context, name string, spec types.ServerSpec) error { node, err := r.kubeClient.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(err) } else { return errdefs.System(err) } } if len(node.Labels) == 0 { node.Labels = map[string]string{} } for k, v := range spec.Labels { node.Labels[filepath.Join("tensorchord.ai", k)] = v } _, err = r.kubeClient.CoreV1().Nodes().Update(ctx, node, metav1.UpdateOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return errdefs.NotFound(err) } else { return errdefs.System(err) } } return nil } ================================================ FILE: agent/pkg/runtime/server_list.go ================================================ package runtime import ( "context" "strings" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/agent/pkg/k8s" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func (r generalRuntime) ServerList(ctx context.Context) ([]types.Server, error) { nodes, err := r.kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return nil, errdefs.NotFound(err) } else { return nil, errdefs.System(err) } } if len(nodes.Items) == 0 { return nil, nil } return getServers(nodes.Items), nil } func getServers(nodes []v1.Node) []types.Server { res := []types.Server{} for _, n := range nodes { res = append(res, getServer(n)) } return res } func getServer(n v1.Node) types.Server { node := types.Server{ Spec: types.ServerSpec{ Name: n.Name, Labels: make(map[string]string), }, Status: types.ServerStatus{ Allocatable: k8s.AsResourceList(n.Status.Allocatable), Capacity: k8s.AsResourceList(n.Status.Capacity), System: types.NodeSystemInfo{ MachineID: n.Status.NodeInfo.MachineID, KernelVersion: n.Status.NodeInfo.KernelVersion, OSImage: n.Status.NodeInfo.OSImage, OperatingSystem: n.Status.NodeInfo.OperatingSystem, Architecture: n.Status.NodeInfo.Architecture, }, }, } for k, v := range n.Labels { if strings.HasPrefix(k, "ai.tensorchord.") { node.Spec.Labels[strings.TrimPrefix(k, "ai.tensorchord.")] = v } if k == consts.LabelServerResource { node.Status.System.ResourceType = v } } phase := "Ready" for _, c := range n.Status.Conditions { if c.Type == v1.NodeReady && c.Status != v1.ConditionTrue { phase = "NotReady" } else if c.Type == v1.NodeDiskPressure && c.Status != v1.ConditionFalse { phase = "DiskPressure" } else if c.Type == v1.NodeMemoryPressure && c.Status != v1.ConditionFalse { phase = "MemoryPressure" } else if c.Type == v1.NodePIDPressure && c.Status != v1.ConditionFalse { phase = "PIDPressure" } else if c.Type == v1.NodeNetworkUnavailable && c.Status != v1.ConditionFalse { phase = "NetworkUnavailable" } } node.Status.Phase = phase return node } ================================================ FILE: agent/pkg/runtime/util_domain.go ================================================ package runtime import ( "fmt" "github.com/dchest/uniuri" ) const ( AnnotationDomain = "ai.tensorchord.domain" ) const ( // stdLen is a standard length of uniuri string to achieve ~95 bits of entropy. stdLen = 16 ) // StdChars is a set of standard characters allowed in uniuri string. var stdChars = []byte("abcdefghijklmnopqrstuvwxyz0123456789") func makeDomain(name, baseDomain string) (string, error) { if baseDomain == "" { return "", fmt.Errorf("base domain is required") } if name == "" { return "", fmt.Errorf("domain name is required") } hash := uniuri.NewLenChars(stdLen, stdChars) return fmt.Sprintf("%s-%s.%s", name, hash, baseDomain), nil } ================================================ FILE: agent/pkg/runtime/util_resource.go ================================================ package runtime import ( "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "github.com/tensorchord/openmodelz/agent/api/types" ) func createResources(request types.InferenceDeployment) (corev1.ResourceRequirements, error) { resources := corev1.ResourceRequirements{ Limits: corev1.ResourceList{}, Requests: corev1.ResourceList{}, } if request.Spec.Resources == nil { return resources, nil } // Set Memory limits if request.Spec.Resources.Limits[types.ResourceMemory] != "" { qty, err := resource.ParseQuantity( string(request.Spec.Resources.Limits[types.ResourceMemory])) if err != nil { return resources, err } resources.Limits[corev1.ResourceMemory] = qty } if request.Spec.Resources.Requests[types.ResourceMemory] != "" { qty, err := resource.ParseQuantity( string(request.Spec.Resources.Requests[types.ResourceMemory])) if err != nil { return resources, err } resources.Requests[corev1.ResourceMemory] = qty } // Set CPU limits if request.Spec.Resources.Limits[types.ResourceCPU] != "" { qty, err := resource.ParseQuantity( string(request.Spec.Resources.Limits[types.ResourceCPU])) if err != nil { return resources, err } resources.Limits[corev1.ResourceCPU] = qty } if request.Spec.Resources.Requests[types.ResourceCPU] != "" { qty, err := resource.ParseQuantity( string(request.Spec.Resources.Requests[types.ResourceCPU])) if err != nil { return resources, err } resources.Requests[corev1.ResourceCPU] = qty } // Set GPU limits if request.Spec.Resources.Limits[types.ResourceGPU] != "" { qty, err := resource.ParseQuantity( string(request.Spec.Resources.Limits[types.ResourceGPU])) if err != nil { return resources, err } resources.Limits[consts.ResourceNvidiaGPU] = qty } if request.Spec.Resources.Requests[types.ResourceGPU] != "" { qty, err := resource.ParseQuantity( string(request.Spec.Resources.Requests[types.ResourceGPU])) if err != nil { return resources, err } resources.Requests[consts.ResourceNvidiaGPU] = qty } return resources, nil } ================================================ FILE: agent/pkg/scaling/function_scaler.go ================================================ package scaling import ( "context" "fmt" "sync" "time" "github.com/dgraph-io/ristretto" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/runtime" ) const ( maxPollCount = 1000 retries = 20 pollInterval = time.Millisecond * 100 ) // InferenceScaler create a new scaler with the specified // ScalingConfig func NewInferenceScaler(r runtime.Runtime, defaultTTL time.Duration) (*InferenceScaler, error) { cache, err := ristretto.NewCache(&ristretto.Config{ NumCounters: 1e7, MaxCost: 1 << 28, BufferItems: 64, }) if err != nil { return nil, err } return &InferenceScaler{ cache: *cache, runtime: r, defaultTTL: defaultTTL, }, nil } // InferenceScaler scales from zero type InferenceScaler struct { cache ristretto.Cache mu sync.RWMutex runtime runtime.Runtime defaultTTL time.Duration } // FunctionScaleResult holds the result of scaling from zero type FunctionScaleResult struct { Available bool Error error Found bool Duration time.Duration } func (s *InferenceScaler) get( namespace, inferenceName string) (ServiceQueryResponse, error) { key := inferenceName + "." + namespace s.mu.RLock() raw, exit := s.cache.Get(key) s.mu.RUnlock() if exit { return raw.(ServiceQueryResponse), nil } s.mu.Lock() defer s.mu.Unlock() raw, exit = s.cache.Get(key) if exit { return raw.(ServiceQueryResponse), nil } // The wasn't a hit, or there were no available replicas found // so query the live endpoint inf, err := s.runtime.InferenceGet(namespace, inferenceName) if err != nil { return ServiceQueryResponse{}, err } sqr, err := AsServerQueryResponse(inf) if err != nil { return ServiceQueryResponse{}, err } if sqr == nil { return ServiceQueryResponse{}, fmt.Errorf("unable to get service query response") } s.cache.SetWithTTL(key, *sqr, 1, s.defaultTTL) return *sqr, nil } // Scale scales a function from zero replicas to 1 or the value set in // the minimum replicas metadata func (s *InferenceScaler) Scale(ctx context.Context, namespace, inferenceName string) FunctionScaleResult { start := time.Now() resp, err := s.get(namespace, inferenceName) if err != nil { return FunctionScaleResult{ Error: err, Available: false, Found: false, Duration: time.Since(start), } } // Check if there are available replicas in the live data if resp.AvailableReplicas > 0 { return FunctionScaleResult{ Error: nil, Available: true, Found: true, Duration: time.Since(start), } } // If the desired replica count is 0, then a scale up event // is required. if resp.Replicas == 0 { // If the max replicas is 0, then the function is not // scalable if resp.MaxReplicas == 0 { return FunctionScaleResult{ Error: fmt.Errorf("unable to scale up %s, max replicas is 0", inferenceName), Available: false, Found: true, Duration: time.Since(start), } } minReplicas := uint64(1) if resp.MinReplicas > 0 { minReplicas = resp.MinReplicas } // In a retry-loop, first query desired replicas, then // set them if the value is still at 0. scaleResult := Retry(func(attempt int) error { inf, err := s.runtime.InferenceGet(namespace, inferenceName) if err != nil { return err } // The scale up is complete because the desired replica count // has been set to 1 or more. if inf.Status.Replicas > 0 { return nil } // Request a scale up to the minimum amount of replicas if err := s.runtime.InferenceScale(ctx, namespace, types.ScaleServiceRequest{ ServiceName: inferenceName, Replicas: minReplicas, EventMessage: fmt.Sprintf("scale up to replicas %d", minReplicas), Attempt: attempt, }, inf); err != nil { return err } logrus.WithField("inference", inferenceName). WithField("replicas", minReplicas). Debug("scaling up inference") return nil }, "Scale", retries, pollInterval) if scaleResult != nil { return FunctionScaleResult{ Error: scaleResult, Available: false, Found: true, Duration: time.Since(start), } } } switch resp.Framework { // Return early for prototype frameworks. case "gradio", "streamlit": return FunctionScaleResult{ Error: nil, Available: false, Found: true, Duration: time.Since(start), } } // Holding pattern for at least one function replica to be available for i := 0; i < maxPollCount; i++ { inf, err := s.runtime.InferenceGet(namespace, inferenceName) if err != nil { return FunctionScaleResult{ Error: err, Available: false, Found: true, Duration: time.Since(start), } } totalTime := time.Since(start) if inf.Status.AvailableReplicas > 0 { logrus.Debugf("[Ready] function=%s waited for - %.4fs", inferenceName, totalTime.Seconds()) return FunctionScaleResult{ Error: nil, Available: true, Found: true, Duration: totalTime, } } time.Sleep(pollInterval) } return FunctionScaleResult{ Error: nil, Available: true, Found: true, Duration: time.Since(start), } } ================================================ FILE: agent/pkg/scaling/ranges.go ================================================ package scaling import "time" type ScaleType string const ( // DefaultMinReplicas is the minimal amount of replicas for a service. DefaultMinReplicas = 1 // DefaultMaxReplicas is the amount of replicas a service will auto-scale up to. DefaultMaxReplicas = 5 DefaultZeroDuration = 3 * time.Minute // DefaultScalingFactor is the defining proportion for the scaling increments. DefaultScalingFactor = 10 ScaleTypeRPS ScaleType = "rps" ScaleTypeCapacity ScaleType = "capacity" // MinScaleLabel label indicating min scale for a Inference MinScaleLabel = "ai.tensorchord.scale.min" // MaxScaleLabel label indicating max scale for a Inference MaxScaleLabel = "ai.tensorchord.scale.max" // ScalingFactorLabel label indicates the scaling factor for a Inference ScalingFactorLabel = "ai.tensorchord.scale.factor" // TargetLoadLabel label indicates the target load for a Inference TargetLoadLabel = "ai.tensorchord.scale.target" // ZeroDurationLabel label indicates the zero duration for a Inference ZeroDurationLabel = "ai.tensorchord.scale.zero-duration" // ScaleTypeLabel label indicates the scale type for a Inference ScaleTypeLabel = "ai.tensorchord.scale.type" FrameworkLabel = "ai.tensorchord.framework" ) ================================================ FILE: agent/pkg/scaling/retry.go ================================================ package scaling import ( "log" "time" ) type routine func(attempt int) error func Retry(r routine, label string, attempts int, interval time.Duration) error { var err error for i := 0; i < attempts; i++ { res := r(i) if res != nil { err = res log.Printf("[%s]: %d/%d, error: %s\n", label, i, attempts, res) } else { err = nil break } time.Sleep(interval) } return err } ================================================ FILE: agent/pkg/scaling/service_query.go ================================================ // Copyright (c) OpenFaaS Author(s). All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. package scaling import "time" // ServiceQuery provides interface for replica querying/setting type ServiceQuery interface { GetReplicas(service, namespace string) (response ServiceQueryResponse, err error) SetReplicas(service, namespace string, count uint64) error } // ServiceQueryResponse response from querying a function status type ServiceQueryResponse struct { Framework string TargetLoad uint64 ZeroDuration time.Duration Replicas uint64 MaxReplicas uint64 MinReplicas uint64 ScalingFactor uint64 AvailableReplicas uint64 Annotations map[string]string } ================================================ FILE: agent/pkg/scaling/util.go ================================================ package scaling import ( "time" "github.com/tensorchord/openmodelz/agent/api/types" ) func AsServerQueryResponse(inf *types.InferenceDeployment) (*ServiceQueryResponse, error) { if inf == nil { return nil, nil } res := ServiceQueryResponse{} res.Replicas = uint64(inf.Status.Replicas) res.Annotations = inf.Spec.Annotations res.AvailableReplicas = uint64(inf.Status.AvailableReplicas) res.Framework = string(inf.Spec.Framework) res.MinReplicas = uint64(*inf.Spec.Scaling.MinReplicas) res.MaxReplicas = uint64(*inf.Spec.Scaling.MaxReplicas) res.TargetLoad = uint64(*inf.Spec.Scaling.TargetLoad) res.ZeroDuration = time.Duration(*inf.Spec.Scaling.ZeroDuration) * time.Second return &res, nil } ================================================ FILE: agent/pkg/server/error.go ================================================ package server import ( "bytes" "fmt" "net/http" "github.com/tensorchord/openmodelz/agent/errdefs" ) // Error defines a standard application error. type Error struct { // Machine-readable error code. HTTPStatusCode int `json:"http_status_code,omitempty"` // Human-readable message. Message string `json:"message,omitempty"` Request string `json:"request,omitempty"` // Logical operation and nested error. Op string `json:"op,omitempty"` Err error `json:"error,omitempty"` } // Error returns the string representation of the error message. func (e *Error) Error() string { var buf bytes.Buffer // Print the current operation in our stack, if any. if e.Op != "" { fmt.Fprintf(&buf, "%s: ", e.Op) } // If wrapping an error, print its Error() message. // Otherwise print the error code & message. if e.Err != nil { buf.WriteString(e.Err.Error()) } else { if e.HTTPStatusCode != 0 { fmt.Fprintf(&buf, "<%s> ", http.StatusText(e.HTTPStatusCode)) } buf.WriteString(e.Message) } return buf.String() } func NewError(code int, err error, op string) error { return &Error{ HTTPStatusCode: code, Err: err, Message: err.Error(), Op: op, } } func errFromErrDefs(err error, op string) error { if errdefs.IsCancelled(err) { return NewError(http.StatusRequestTimeout, err, op) } else if errdefs.IsConflict(err) { return NewError(http.StatusConflict, err, op) } else if errdefs.IsDataLoss(err) { return NewError(http.StatusInternalServerError, err, op) } else if errdefs.IsDeadline(err) { return NewError(http.StatusRequestTimeout, err, op) } else if errdefs.IsForbidden(err) { return NewError(http.StatusForbidden, err, op) } else if errdefs.IsInvalidParameter(err) { return NewError(http.StatusBadRequest, err, op) } else if errdefs.IsNotFound(err) { return NewError(http.StatusNotFound, err, op) } else if errdefs.IsNotImplemented(err) { return NewError(http.StatusNotImplemented, err, op) } else if errdefs.IsNotModified(err) { return NewError(http.StatusNotModified, err, op) } else if errdefs.IsSystem(err) { return NewError(http.StatusInternalServerError, err, op) } else if errdefs.IsUnauthorized(err) { return NewError(http.StatusUnauthorized, err, op) } else if errdefs.IsUnavailable(err) { return NewError(http.StatusServiceUnavailable, err, op) } else if errdefs.IsUnknown(err) { return NewError(http.StatusInternalServerError, err, op) } return NewError(http.StatusInternalServerError, err, op) } ================================================ FILE: agent/pkg/server/handler_build_create.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Create the build. // @Description Create the build. // @Tags build // @Accept json // @Produce json // @Param body body types.Build true "build" // @Success 200 {object} types.Build // @Router /system/build [post] func (s *Server) handleBuildCreate(c *gin.Context) error { var req types.Build if err := c.ShouldBindJSON(&req); err != nil { return NewError( http.StatusBadRequest, err, "build-create") } if err := s.validator.ValidateBuildRequest(&req); err != nil { return NewError( http.StatusBadRequest, err, "build-create") } s.validator.DefaultBuildRequest(&req) inference, err := s.runtime.InferenceGetCRD(req.Spec.Namespace, req.Spec.Name) if err != nil { return errFromErrDefs(err, "inference-instance-list") } if err := s.runtime.BuildCreate(c.Request.Context(), req, inference, s.config.Build.BuilderImage, s.config.Build.BuildkitdAddress, s.config.Build.BuildCtlBin, s.config.Build.BuildImagePullSecret); err != nil { logrus.Errorf("failed to create build: %v", err) return errFromErrDefs(err, "build-create") } c.JSON(http.StatusOK, req) return nil } ================================================ FILE: agent/pkg/server/handler_build_get.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Get the build by name. // @Description Get the build by name. // @Tags build // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Param name path string true "inference id" // @Success 200 {object} types.Build // @Router /system/build/{name} [get] func (s *Server) handleBuildGet(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), "inference-list") } name := c.Param("name") if name == "" { return NewError( http.StatusBadRequest, errors.New("name is required"), "build-get") } build, err := s.runtime.BuildGet(c.Request.Context(), namespace, name) if err != nil { return errFromErrDefs(err, "build-get") } c.JSON(http.StatusOK, build) return nil } ================================================ FILE: agent/pkg/server/handler_build_list.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary List the builds. // @Description List the builds. // @Tags build // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Success 200 {object} []types.Build // @Router /system/build [get] func (s *Server) handleBuildList(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), "inference-list") } builds, err := s.runtime.BuildList(c.Request.Context(), namespace) if err != nil { return errFromErrDefs(err, "build-list") } c.JSON(http.StatusOK, builds) return nil } ================================================ FILE: agent/pkg/server/handler_build_logs.go ================================================ package server import ( "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Get the build logs. // @Description Get the build logs. // @Tags log // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Param name query string true "Build Name" // @Param instance query string false "Instance" // @Param tail query int false "Tail" // @Param follow query bool false "Follow" // @Param since query string false "Since" // @Success 200 {object} []types.Message // @Router /system/logs/build [get] func (s *Server) handleBuildLogs(c *gin.Context) error { return s.getLogsFromRequester(c, s.buildLogRequester) } ================================================ FILE: agent/pkg/server/handler_gradio_proxy.go ================================================ package server import ( "fmt" "io" "net" "net/http" "net/http/httputil" "net/url" "path" "strconv" "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/pkg/consts" "github.com/tensorchord/openmodelz/agent/pkg/server/static" ) // @Summary Reverse proxy to the backend gradio. // @Description Reverse proxy to the backend gradio. // @Tags inference // @Accept */* // @Produce json // @Param id path string true "Deployment ID" // @Router /gradio/{id} [get] // @Router /gradio/{id} [post] // @Success 201 func (s *Server) proxyGradio(c *gin.Context) error { remote, err := url.Parse(fmt.Sprintf("http://0.0.0.0:%d", s.config.Server.ServerPort)) if err != nil { return err } proxy := httputil.NewSingleHostReverseProxy(remote) proxy.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: s.config.ModelZCloud.UpstreamTimeout, KeepAlive: s.config.ModelZCloud.UpstreamTimeout, DualStack: true, }).DialContext, MaxIdleConns: s.config.ModelZCloud.MaxIdleConnections, MaxIdleConnsPerHost: s.config.ModelZCloud.MaxIdleConnectionsPerHost, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } uid, deployment, err := s.proxyNoAuth(c) if err != nil { return err } ns := consts.DefaultPrefix + uid proxy.Director = func(req *http.Request) { req.Header = c.Request.Header req.Host = remote.Host req.URL.Scheme = remote.Scheme req.URL.Host = remote.Host req.URL.Path = path.Join( "/", "inference", fmt.Sprintf("%s.%s", deployment, ns), c.Param("proxyPath")) logrus.WithFields(logrus.Fields{ "deployment": deployment, "uid": uid, "ns": ns, "path": req.URL.Path, "remote": remote.String(), }).Debug("proxying to gradio") } proxy.ModifyResponse = func(resp *http.Response) error { // http.StatusSeeOther indicates that the server is still loading. if resp.StatusCode == http.StatusSeeOther { resp.StatusCode = http.StatusOK instances, err := s.runtime.InferenceInstanceList(ns, deployment) if err != nil { return NewError(http.StatusInternalServerError, err, "instance-list") } buf, err := static.RenderDeploymentLoadingPage("gradio", resp.Header.Get("X-Call-Id"), "We are currently processing your request.", deployment, instances) if err != nil { return NewError(http.StatusInternalServerError, err, "render-loading-page") } resp.Body = io.NopCloser(buf) resp.ContentLength = int64(buf.Len()) resp.Header.Set("Content-Length", strconv.Itoa(buf.Len())) resp.Header.Set("Content-Type", "text/html") resp.StatusCode = http.StatusServiceUnavailable } return nil } proxy.ServeHTTP(c.Writer, c.Request) return nil } ================================================ FILE: agent/pkg/server/handler_healthz.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" ) // @Summary Healthz // @Description Healthz // @Tags system // @Accept json // @Produce json // @Success 200 // @Router /healthz [get] func (s *Server) handleHealthz(c *gin.Context) error { c.Status(http.StatusOK) return nil } ================================================ FILE: agent/pkg/server/handler_healthz_test.go ================================================ package server import ( "github.com/gin-gonic/gin" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("healthz", func() { BeforeEach(func() { server = &Server{ router: gin.New(), metricsRouter: gin.New(), runtime: mockRuntime, } }) It("healthz", func() { c := mkContext("GET", "/", nil, nil) err := server.handleHealthz(c) Expect(err).NotTo(HaveOccurred()) }) }) ================================================ FILE: agent/pkg/server/handler_image_cache.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Create the image cache. // @Description Create the image cache. // @Tags image-cache // @Accept json // @Produce json // @Param body body types.ImageCache true "image-cache" // @Success 201 {object} types.ImageCache // @Router /system/image-cache [post] func (s *Server) handleImageCacheCreate(c *gin.Context) error { var req types.ImageCache if err := c.ShouldBindJSON(&req); err != nil { return NewError( http.StatusBadRequest, err, "image-cache-create") } if err := s.validator.ValidateImageCacheRequest(&req); err != nil { return NewError( http.StatusBadRequest, err, "image-cache-create") } inference, err := s.runtime.InferenceGetCRD(req.Namespace, req.Name) if err != nil { return errFromErrDefs(err, "inference-instance-list") } if err := s.runtime.ImageCacheCreate(c.Request.Context(), req, inference); err != nil { return errFromErrDefs(err, "image-cache-create") } c.JSON(http.StatusOK, req) return nil } ================================================ FILE: agent/pkg/server/handler_inference_create.go ================================================ package server import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/client" ) // @Summary Create the inferences. // @Description Create the inferences. // @Tags inference // @Accept json // @Produce json // @Param request body types.InferenceDeployment true "query params" // @Success 201 {object} types.InferenceDeployment // @Router /system/inferences [post] func (s *Server) handleInferenceCreate(c *gin.Context) error { event := types.DeploymentCreateEvent var req types.InferenceDeployment if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } if s.config.ModelZCloud.Enabled { ns := req.Spec.Namespace user, err := client.GetUserIDFromNamespace(ns) if err != nil { return err } else if user == "" { return fmt.Errorf("user id is empty") } s.cache.SetWithTTL(req.Spec.Name, user, 1, 0) exist := s.runtime.NamespaceGet(c.Request.Context(), ns) if !exist { if err := s.runtime.NamespaceCreate(c.Request.Context(), ns); err != nil { return err } } } // Set the default values. s.validator.DefaultDeployRequest(&req) // Validate the request. if err := s.validator.ValidateDeployRequest(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } // Create the inference. if err := s.runtime.InferenceCreate(c.Request.Context(), req, s.config.Ingress, event, s.config.Server.ServerPort); err != nil { return errFromErrDefs(err, event) } c.JSON(http.StatusCreated, req) return nil } ================================================ FILE: agent/pkg/server/handler_inference_create_test.go ================================================ package server import ( "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/server/validator" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" ) var _ = Describe("inference create", func() { BeforeEach(func() { server = &Server{ router: gin.New(), metricsRouter: gin.New(), runtime: mockRuntime, validator: validator.New(), } }) It("invalid request - nil", func() { c := mkContext("GET", "/", nil, nil) err := server.handleInferenceCreate(c) Expect(err).To(HaveOccurred()) }) It("invalid request - empty", func() { c := mkJsonBodyContext("GET", "/", nil, types.InferenceDeployment{}) err := server.handleInferenceCreate(c) Expect(err).To(HaveOccurred()) }) It("good request", func() { mockRuntime.EXPECT().InferenceCreate(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) c := mkJsonBodyContext("GET", "/", nil, types.InferenceDeployment{ Spec: types.InferenceDeploymentSpec{ Name: "abc", Image: "mock-image", Port: Ptr(int32(123)), }, }) err := server.handleInferenceCreate(c) Expect(err).NotTo(HaveOccurred()) }) }) ================================================ FILE: agent/pkg/server/handler_inference_delete.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Delete the inferences. // @Description Delete the inferences. // @Tags inference // @Accept json // @Produce json // @Param request body types.DeleteFunctionRequest true "query params" // @Param namespace query string true "Namespace" // @Success 202 {object} types.DeleteFunctionRequest // @Router /system/inferences [delete] func (s *Server) handleInferenceDelete(c *gin.Context) error { event := types.DeploymentDeleteEvent var req types.DeleteFunctionRequest if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), event) } if req.FunctionName == "" { return NewError( http.StatusBadRequest, errors.New("function name is required"), event) } if err := s.runtime.InferenceDelete(c.Request.Context(), namespace, req.FunctionName, s.config.Ingress.Namespace, event); err != nil { return errFromErrDefs(err, event) } c.JSON(http.StatusAccepted, req) return nil } ================================================ FILE: agent/pkg/server/handler_inference_delete_test.go ================================================ package server import ( "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/server/validator" ) var _ = Describe("inference delete", func() { BeforeEach(func() { server = &Server{ router: gin.New(), metricsRouter: gin.New(), runtime: mockRuntime, validator: validator.New(), } }) It("invalid request - nil", func() { c := mkContext("GET", "/", nil, nil) err := server.handleInferenceDelete(c) Expect(err).To(HaveOccurred()) }) It("invalid request - empty", func() { c := mkJsonBodyContext("GET", "/", nil, types.DeleteFunctionRequest{}) err := server.handleInferenceDelete(c) Expect(err).To(HaveOccurred()) }) It("good request", func() { mockRuntime.EXPECT().InferenceDelete(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil) c := mkJsonBodyContext("GET", "/", nil, types.DeleteFunctionRequest{ FunctionName: "mock-inference", }) setQuery(c, map[string]string{"namespace": "mock-namespace"}) err := server.handleInferenceDelete(c) Expect(err).NotTo(HaveOccurred()) }) }) ================================================ FILE: agent/pkg/server/handler_inference_get.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Get the inference by name. // @Description Get the inference by name. // @Tags inference // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Param name path string true "inference id" // @Success 200 {object} types.InferenceDeployment // @Router /system/inference/{name} [get] func (s *Server) handleInferenceGet(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), "inference-get") } name := c.Param("name") if name == "" { return NewError( http.StatusBadRequest, errors.New("name is required"), "inference-get") } function, err := s.runtime.InferenceGet(namespace, name) if err != nil { return errFromErrDefs(err, "inference-get") } c.JSON(http.StatusOK, function) return nil } ================================================ FILE: agent/pkg/server/handler_inference_get_test.go ================================================ package server import ( "errors" "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/server/validator" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" ) var _ = Describe("inference get", func() { BeforeEach(func() { server = &Server{ router: gin.New(), metricsRouter: gin.New(), runtime: mockRuntime, validator: validator.New(), } }) It("invalid request - no namespace", func() { c := mkContext("GET", "/", nil, nil) err := server.handleInferenceGet(c) Expect(err).To(HaveOccurred()) }) It("invalid request - no name", func() { c := mkJsonBodyContext("GET", "/", nil, nil) setQuery(c, map[string]string{"namespace": "mock-namespace"}) err := server.handleInferenceGet(c) Expect(err).To(HaveOccurred()) }) It("invalid request - mock error", func() { mockRuntime.EXPECT().InferenceGet(gomock.Any(), gomock.Any()).Times(1).Return(nil, errors.New("mock-error")) c := mkJsonBodyContext("GET", "/", nil, nil) setQuery(c, map[string]string{"namespace": "mock-namespace"}) setParam(c, map[string]string{"name": "mock-name"}) err := server.handleInferenceGet(c) Expect(err).To(HaveOccurred()) }) It("good request", func() { mockRuntime.EXPECT().InferenceGet(gomock.Any(), gomock.Any()).Times(1).Return(Ptr(types.InferenceDeployment{}), nil) c := mkJsonBodyContext("GET", "/", nil, nil) setQuery(c, map[string]string{"namespace": "mock-namespace"}) setParam(c, map[string]string{"name": "mock-name"}) err := server.handleInferenceGet(c) Expect(err).NotTo(HaveOccurred()) }) }) ================================================ FILE: agent/pkg/server/handler_inference_instance.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary List the inference instances. // @Description List the inference instances. // @Tags inference // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Param name path string true "Name" // @Success 200 {object} []types.InferenceDeployment // @Router /system/inference/{name}/instances [get] func (s *Server) handleInferenceInstance(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError(http.StatusBadRequest, errors.New("namespace is required"), "inference-instance-list") } name := c.Param("name") if name == "" { return NewError(http.StatusBadRequest, errors.New("name is required"), "inference-instance-list") } instances, err := s.runtime.InferenceInstanceList(namespace, name) if err != nil { return errFromErrDefs(err, "inference-instance-list") } c.JSON(http.StatusOK, instances) return nil } ================================================ FILE: agent/pkg/server/handler_inference_instance_exec.go ================================================ package server import ( "errors" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Attach to the inference instance. // @Description Attach to the inference instance. // @Tags inference // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Param name path string true "Name" // @Param instance path string true "Instance name" // @Success 200 {object} []types.InferenceDeployment // @Router /system/inference/{name}/instance/{instance} [post] func (s *Server) handleInferenceInstanceExec(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError(http.StatusBadRequest, errors.New("namespace is required"), "inference-instance-list") } name := c.Param("name") if name == "" { return NewError(http.StatusBadRequest, errors.New("name is required"), "inference-instance-list") } instance := c.Param("instance") if name == "" { return NewError(http.StatusBadRequest, errors.New("instance is required"), "inference-instance-list") } tty := c.Query("tty") if tty == "" { tty = "false" } ttyBoolean, err := strconv.ParseBool(tty) if err != nil { return NewError(http.StatusBadRequest, err, "inference-instance-exec") } command := c.Query("command") commandSlice := strings.Split(command, ",") if err := s.runtime.InferenceExec( c, namespace, instance, commandSlice, ttyBoolean); err != nil { return errFromErrDefs(err, "inference-instance-exec") } return nil } ================================================ FILE: agent/pkg/server/handler_inference_list.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary List the inferences. // @Description List the inferences. // @Tags inference // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Success 200 {object} []types.InferenceDeployment // @Router /system/inferences [get] func (s *Server) handleInferenceList(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), "inference-list") } inferenes, err := s.runtime.InferenceList(namespace) if err != nil { return errFromErrDefs(err, "inference-list") } // Add invocation count metrics into the body. // TODO: https://github.com/tensorchord/openmodelz/issues/203 s.prometheusClient.AddMetrics(inferenes) c.JSON(http.StatusOK, inferenes) return nil } ================================================ FILE: agent/pkg/server/handler_inference_logs.go ================================================ package server import ( "context" "encoding/json" "net/http" "time" "github.com/cockroachdb/errors" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/log" ) // @Summary Get the inference logs. // @Description Get the inference logs. // @Tags log // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Param name query string true "Name" // @Param instance query string false "Instance" // @Param tail query int false "Tail" // @Param follow query bool false "Follow" // @Param since query string false "Since" // @Param end query string false "End" // @Success 200 {object} []types.Message // @Router /system/logs/inference [get] func (s *Server) handleInferenceLogs(c *gin.Context) error { return s.getLogsFromRequester(c, s.deploymentLogRequester) } func (s Server) getLogsFromRequester(c *gin.Context, requester log.Requester) error { cn, ok := c.Writer.(http.CloseNotifier) if !ok { return NewError(http.StatusNotFound, errors.New("LogHandler: response is not a CloseNotifier, required for streaming response"), "log-get") } flusher, ok := c.Writer.(http.Flusher) if !ok { return NewError(http.StatusNotFound, errors.New("LogHandler: response is not a Flusher, required for streaming response"), "log-get") } var req types.LogRequest if err := c.ShouldBindQuery(&req); err != nil { return NewError(http.StatusBadRequest, err, "log-get") } _ = cn timeout := s.config.Inference.LogTimeout if req.Follow { // use a much larger timeout for streaming log timeout = time.Hour } ctx, cancelQuery := context.WithTimeout(c.Request.Context(), timeout) defer cancelQuery() messages, err := requester.Query(ctx, req) if err != nil { return errFromErrDefs(err, "log-get") } // Send the initial headers saying we're gonna stream the response. c.Header("Content-Type", "application/x-ndjson") c.Header("Transfer-Encoding", "chunked") c.Header("Connection", "Keep-Alive") flusher.Flush() defer flusher.Flush() defer c.Writer.Write([]byte{}) defer flusher.Flush() jsonEncoder := json.NewEncoder(c.Writer) for messages != nil { select { case <-cn.CloseNotify(): s.logger.WithField("req", req). Debug("client closed connection") return nil case msg, ok := <-messages: if !ok { s.logger.WithField("req", req). Debug("log stream closed") messages = nil return nil } // serialize and write the msg to the http ResponseWriter err := jsonEncoder.Encode(msg) if err != nil { // can't actually write the status header here so we should json serialize an error // and return that because we have already sent the content type and status code s.logger.WithError(err).Error("LogHandler: failed to serialize log message") // write json error message here ? jsonEncoder.Encode(types.Message{Text: "failed to serialize log message"}) flusher.Flush() return nil } flusher.Flush() } } return nil } ================================================ FILE: agent/pkg/server/handler_inference_proxy.go ================================================ package server import ( "errors" "fmt" "net" "net/http" "net/http/httputil" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus" "github.com/tensorchord/openmodelz/agent/errdefs" ) // @Summary Inference. // @Description Inference proxy. // @Tags inference-proxy // @Accept json // @Produce json // @Param name path string true "inference id" // @Router /inference/{name} [post] // @Router /inference/{name} [get] // @Router /inference/{name} [put] // @Router /inference/{name} [delete] // @Success 200 // @Failure 303 // @Failure 400 // @Failure 404 // @Failure 500 func (s *Server) handleInferenceProxy(c *gin.Context) error { namespacedName := c.Param("name") if namespacedName == "" { return NewError( http.StatusBadRequest, errors.New("name is required"), "inference-proxy") } namespace, name, err := getNamespaceAndName(namespacedName) if err != nil { return NewError( http.StatusBadRequest, err, "inference-proxy") } // Update metrics. s.metricsOptions.GatewayInferenceInvocationStarted. WithLabelValues(namespacedName).Inc() s.metricsOptions.GatewayInferenceInvocationInflight. WithLabelValues(namespacedName).Inc() start := time.Now() label := prometheus.Labels{"inference_name": namespacedName, "code": strconv.Itoa(http.StatusProcessing)} defer func() { s.metricsOptions.GatewayInferenceInvocationInflight. WithLabelValues(namespacedName).Dec() s.metricsOptions.GatewayInferencesHistogram.With(label). Observe(time.Since(start).Seconds()) s.metricsOptions.GatewayInferenceInvocation.With(label).Inc() }() res := s.scaler.Scale(c.Request.Context(), namespace, name) if !res.Found { label["code"] = strconv.Itoa(http.StatusNotFound) return NewError( http.StatusNotFound, errors.New("inference not found"), "inference-proxy") } else if res.Error != nil { label["code"] = strconv.Itoa(http.StatusInternalServerError) return NewError( http.StatusInternalServerError, res.Error, "inference-proxy") } if res.Available { statusCode, err := s.forward(c, namespace, name) if err != nil { label["code"] = strconv.Itoa(statusCode) return NewError(statusCode, err, "inference-proxy") } label["code"] = strconv.Itoa(statusCode) return nil } else { // The inference is still being created. label["code"] = strconv.Itoa(http.StatusSeeOther) return NewError(http.StatusSeeOther, fmt.Errorf("inference %s is not available", name), "inference-proxy") } } func (s *Server) forward(c *gin.Context, namespace, name string) (int, error) { backendURL, err := s.endpointResolver.Resolve(namespace, name) if err != nil { return 0, errdefs.InvalidParameter(err) } defer s.endpointResolver.Close(backendURL) proxyServer := httputil.ReverseProxy{} proxyServer.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: s.config.Server.ReadTimeout, KeepAlive: s.config.Server.ReadTimeout, DualStack: true, }).DialContext, } proxyServer.Director = func(req *http.Request) { targetQuery := backendURL.RawQuery req.URL.Scheme = backendURL.Scheme req.URL.Host = backendURL.Host if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery } else { req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery } req.URL.Path = c.Param("proxyPath") if req.URL.Path == "" { req.URL.Path = "/" } s.logger.WithField("url", backendURL.String()). WithField("path", req.URL.Path). WithField("header", req.Header). WithField("raw-query", req.URL.RawQuery).Debug("reverse proxy") } var statusCode int proxyServer.ModifyResponse = func(resp *http.Response) error { statusCode = resp.StatusCode return nil } proxyServer.ServeHTTP(c.Writer, c.Request) return statusCode, nil } func getNamespaceAndName(name string) (string, string, error) { if !strings.Contains(name, ".") { return "", "", fmt.Errorf("name is not namespaced") } namespace := name[strings.LastIndexAny(name, ".")+1:] infName := strings.TrimSuffix(name, "."+namespace) if namespace == "" { return "", "", fmt.Errorf("namespace is empty") } if infName == "" { return "", "", fmt.Errorf("inference name is empty") } return namespace, infName, nil } ================================================ FILE: agent/pkg/server/handler_inference_scale.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Scale the inferences. // @Description Scale the inferences. // @Tags inference // @Accept json // @Produce json // @Param namespace query string true "Namespace" // @Param request body types.ScaleServiceRequest true "query params" // @Success 202 {object} []types.ScaleServiceRequest // @Failure 400 // @Router /system/scale-inference [post] func (s *Server) handleInferenceScale(c *gin.Context) error { var req types.ScaleServiceRequest if err := c.ShouldBindJSON(&req); err != nil { return NewError( http.StatusBadRequest, err, "inference-scale") } namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), "inference-scale") } inf, err := s.runtime.InferenceGet(namespace, req.ServiceName) if err != nil { return errFromErrDefs(err, "inference-scale") } if err := s.runtime.InferenceScale(c.Request.Context(), namespace, req, inf); err != nil { return errFromErrDefs(err, "inference-scale") } c.JSON(http.StatusAccepted, req) return nil } ================================================ FILE: agent/pkg/server/handler_inference_update.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Update the inferences. // @Description Update the inferences. // @Tags inference // @Accept json // @Produce json // @Param request body types.InferenceDeployment true "query params" // @Param namespace query string true "Namespace" // @Success 202 {object} types.InferenceDeployment // @Router /system/inferences [put] func (s *Server) handleInferenceUpdate(c *gin.Context) error { event := types.DeploymentUpdateEvent var req types.InferenceDeployment if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), event) } if err := s.validator.ValidateDeployRequest(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } if err := s.runtime.InferenceUpdate(c.Request.Context(), namespace, req, event); err != nil { return errFromErrDefs(err, event) } c.JSON(http.StatusAccepted, req) return nil } ================================================ FILE: agent/pkg/server/handler_info.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/version" ) // @Summary Get system info. // @Description Get system info. // @Tags system // @Accept json // @Produce json // @Success 200 {object} types.ProviderInfo // @Router /system/info [get] func (s *Server) handleInfo(c *gin.Context) error { v := version.GetVersion() c.JSON(http.StatusOK, types.ProviderInfo{ Name: "agent", Orchestration: "kubernetes", Version: &types.VersionInfo{ Version: v.Version, BuildDate: v.BuildDate, GitCommit: v.GitCommit, GitTag: v.GitTag, GitTreeState: v.GitTreeState, GoVersion: v.GoVersion, Compiler: v.Compiler, Platform: v.Platform, }, }) return nil } ================================================ FILE: agent/pkg/server/handler_mosec_proxy.go ================================================ package server import ( "fmt" "path" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/pkg/consts" ) // @Summary Proxy to the backend mosec. // @Description Proxy to the backend mosec. // @Tags inference // @Accept */* // @Produce json // @Param id path string true "Deployment ID" // @Router /mosec/{id} [get] // @Router /mosec/{id}/metrics [get] // @Router /mosec/{id}/inference [post] // @Success 201 func (s *Server) proxyMosec(c *gin.Context) error { uid, deployment, err := s.proxyAuth(c) if err != nil { return err } c.Request.URL.Path = path.Join( "/", "inference", fmt.Sprintf("%s.%s", deployment, consts.DefaultPrefix+uid), c.Param("proxyPath")) return s.handleInferenceProxy(c) } ================================================ FILE: agent/pkg/server/handler_namespace_create.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Create the namespace. // @Description Create the namespace. // @Tags namespace // @Accept json // @Produce json // @Param body body types.NamespaceRequest true "Namespace name" // @Success 200 {object} types.NamespaceRequest // @Router /system/namespaces [post] func (s *Server) handleNamespaceCreate(c *gin.Context) error { var req types.NamespaceRequest if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, "namespace-create") } if err := s.runtime.NamespaceCreate(c.Request.Context(), req.Name); err != nil { return errFromErrDefs(err, "namespace-create") } c.JSON(http.StatusOK, req) return nil } ================================================ FILE: agent/pkg/server/handler_namespace_delete.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Delete the namespace. // @Description Delete the namespace. // @Tags namespace // @Accept json // @Produce json // @Param body body types.NamespaceRequest true "Namespace name" // @Success 200 {object} types.NamespaceRequest // @Router /system/namespaces [delete] func (s *Server) handleNamespaceDelete(c *gin.Context) error { var req types.NamespaceRequest if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, "namespace-delete") } if err := s.runtime.NamespaceDelete(c.Request.Context(), req.Name); err != nil { return errFromErrDefs(err, "namespace-delete") } c.JSON(http.StatusOK, req) return nil } ================================================ FILE: agent/pkg/server/handler_namespace_delete_test.go ================================================ package server import ( "errors" "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/server/validator" ) var _ = Describe("namespace delete", func() { BeforeEach(func() { server = &Server{ router: gin.New(), metricsRouter: gin.New(), runtime: mockRuntime, validator: validator.New(), } }) It("invalid request - nil", func() { c := mkContext("GET", "/", nil, nil) err := server.handleNamespaceDelete(c) Expect(err).To(HaveOccurred()) }) It("invalid request - mock error", func() { mockRuntime.EXPECT().NamespaceDelete(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("mock-error")) c := mkJsonBodyContext("GET", "/", nil, types.NamespaceRequest{ Name: "mock-ns", }) err := server.handleNamespaceDelete(c) Expect(err).To(HaveOccurred()) }) It("good request", func() { mockRuntime.EXPECT().NamespaceDelete(gomock.Any(), gomock.Any()).Times(1).Return(nil) c := mkJsonBodyContext("GET", "/", nil, types.NamespaceRequest{ Name: "mock-ns", }) err := server.handleNamespaceDelete(c) Expect(err).NotTo(HaveOccurred()) }) }) ================================================ FILE: agent/pkg/server/handler_namespace_list.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" ) // @Summary List the namespaces. // @Description List the namespaces. // @Tags namespace // @Accept json // @Produce json // @Success 200 {object} []string // @Router /system/namespaces [get] func (s *Server) handleNamespaceList(c *gin.Context) error { ns, err := s.runtime.NamespaceList(c.Request.Context()) if err != nil { return errFromErrDefs(err, "namespace-list") } c.JSON(http.StatusOK, ns) return nil } ================================================ FILE: agent/pkg/server/handler_other_proxy.go ================================================ package server import ( "fmt" "net" "net/http" "net/http/httputil" "net/url" "path" "time" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/pkg/consts" ) // @Summary Reverse proxy to the backend other. // @Description Reverse proxy to the backend other. // @Tags inference // @Accept */* // @Produce json // @Param id path string true "Deployment ID" // @Router /other/{id} [get] // @Router /other/{id} [post] // @Success 201 func (s *Server) proxyOther(c *gin.Context) error { remote, err := url.Parse(fmt.Sprintf("http://0.0.0.0:%d", s.config.Server.ServerPort)) if err != nil { return err } proxy := httputil.NewSingleHostReverseProxy(remote) proxy.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: s.config.ModelZCloud.UpstreamTimeout, KeepAlive: s.config.ModelZCloud.UpstreamTimeout, DualStack: true, }).DialContext, MaxIdleConns: s.config.ModelZCloud.MaxIdleConnections, MaxIdleConnsPerHost: s.config.ModelZCloud.MaxIdleConnectionsPerHost, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } uid, deployment, err := s.proxyAuth(c) if err != nil { return err } proxy.Director = func(req *http.Request) { req.Header = c.Request.Header req.Host = remote.Host req.URL.Scheme = remote.Scheme req.URL.Host = remote.Host req.URL.Path = path.Join( "/", "inference", fmt.Sprintf("%s.%s", deployment, consts.DefaultPrefix+uid), c.Param("proxyPath")) } proxy.ServeHTTP(c.Writer, c.Request) return nil } ================================================ FILE: agent/pkg/server/handler_root.go ================================================ package server import ( "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/pkg/server/static" ) func (s *Server) handleRoot(c *gin.Context) error { lp, err := static.RenderLoadingPage() if err != nil { return err } c.Data(200, "text/html; charset=utf-8", lp.Bytes()) return nil } ================================================ FILE: agent/pkg/server/handler_server_delete.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" ) // @Summary Delete a node from the cluster. // @Description Delete a node. // @Tags namespace // @Param name path string true "Server Name" // @Accept json // @Produce json // @Success 200 // @Router /system/server/{name}/delete [delete] func (s *Server) handleServerDelete(c *gin.Context) error { name := c.Param("name") if name == "" { return NewError(http.StatusBadRequest, errors.New("name is required"), "server-delete-node") } err := s.runtime.ServerDeleteNode(c.Request.Context(), name) if err != nil { return errFromErrDefs(err, "server-delete-node") } return nil } ================================================ FILE: agent/pkg/server/handler_server_label_create.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary List the servers. // @Description List the servers. // @Tags namespace // @Param name path string true "Server Name" // @Param request body types.ServerSpec true "query params" // @Accept json // @Produce json // @Success 200 {object} []string // @Router /system/server/{name}/labels [post] func (s *Server) handleServerLabelCreate(c *gin.Context) error { name := c.Param("name") if name == "" { return NewError(http.StatusBadRequest, errors.New("name is required"), "server-label-create") } var req types.ServerSpec if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, "server-label-create") } err := s.runtime.ServerLabelCreate(c.Request.Context(), name, req) if err != nil { return errFromErrDefs(err, "namespace-list") } c.JSON(http.StatusOK, req) return nil } ================================================ FILE: agent/pkg/server/handler_server_list.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary List the servers. // @Description List the servers. // @Tags namespace // @Accept json // @Produce json // @Success 200 {object} []types.Server // @Router /system/servers [get] func (s *Server) handleServerList(c *gin.Context) error { ns := []types.Server{} ns, err := s.runtime.ServerList(c.Request.Context()) if err != nil { return errFromErrDefs(err, "namespace-list") } c.JSON(http.StatusOK, ns) return nil } ================================================ FILE: agent/pkg/server/handler_streamlit_proxy.go ================================================ package server import ( "fmt" "io" "net" "net/http" "net/http/httputil" "net/url" "path" "strconv" "time" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/pkg/consts" "github.com/tensorchord/openmodelz/agent/pkg/server/static" ) // @Summary Reverse proxy to streamlit. // @Description Reverse proxy to streamlit. // @Tags inference // @Accept */* // @Produce json // @Param id path string true "Deployment ID" // @Router /streamlit/{id} [get] // @Router /streamlit/{id} [post] // @Success 201 func (s *Server) proxyStreamlit(c *gin.Context) error { remote, err := url.Parse(fmt.Sprintf("http://0.0.0.0:%d", s.config.Server.ServerPort)) if err != nil { return err } proxy := httputil.NewSingleHostReverseProxy(remote) proxy.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: s.config.ModelZCloud.UpstreamTimeout, KeepAlive: s.config.ModelZCloud.UpstreamTimeout, DualStack: true, }).DialContext, MaxIdleConns: s.config.ModelZCloud.MaxIdleConnections, MaxIdleConnsPerHost: s.config.ModelZCloud.MaxIdleConnectionsPerHost, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } uid, deployment, err := s.proxyNoAuth(c) if err != nil { return err } ns := consts.DefaultPrefix + uid proxy.Director = func(req *http.Request) { req.Header = c.Request.Header req.Host = remote.Host req.URL.Scheme = remote.Scheme req.URL.Host = remote.Host req.URL.Path = path.Join( "/", "inference", fmt.Sprintf("%s.%s", deployment, ns), c.Param("proxyPath")) logrus.WithFields(logrus.Fields{ "deployment": deployment, "uid": uid, "ns": ns, "path": req.URL.Path, "remote": remote.String(), }).Debug("proxying to streamlit") } proxy.ModifyResponse = func(resp *http.Response) error { if resp.StatusCode == http.StatusSeeOther { resp.StatusCode = http.StatusOK instances, err := s.runtime.InferenceInstanceList(ns, deployment) if err != nil { return NewError(http.StatusInternalServerError, err, "instance-list") } buf, err := static.RenderDeploymentLoadingPage("streamlit", resp.Header.Get("X-Call-Id"), "We are currently processing your request.", deployment, instances) if err != nil { return NewError(http.StatusInternalServerError, err, "render-loading-page") } resp.Body = io.NopCloser(buf) resp.ContentLength = int64(buf.Len()) resp.Header.Set("Content-Length", strconv.Itoa(buf.Len())) resp.Header.Set("Content-Type", "text/html") resp.StatusCode = http.StatusServiceUnavailable } return nil } proxy.ServeHTTP(c.Writer, c.Request) return nil } ================================================ FILE: agent/pkg/server/middleware_callid.go ================================================ package server import ( "fmt" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" ) func (s Server) middlewareCallID(c *gin.Context) error { start := time.Now() if len(c.Request.Header.Get("X-Call-Id")) == 0 { callID := uuid.New().String() c.Request.Header.Add("X-Call-Id", callID) c.Writer.Header().Add("X-Call-Id", callID) } c.Request.Header.Add("X-Start-Time", fmt.Sprintf("%d", start.UTC().UnixNano())) c.Writer.Header().Add("X-Start-Time", fmt.Sprintf("%d", start.UTC().UnixNano())) c.Next() return nil } ================================================ FILE: agent/pkg/server/proxy_auth.go ================================================ package server import ( "context" "fmt" "strings" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/errdefs" "github.com/tensorchord/openmodelz/agent/pkg/consts" ) func (s *Server) proxyAuth(c *gin.Context) (string, string, error) { var uid string var valid bool deployment := c.Param("id") if len(deployment) == 0 { return "", "", errFromErrDefs( fmt.Errorf("cannot find the deployment name in %s", c.Request.RequestURI), "get-deployment") } key := c.GetHeader("X-API-Key") // Be compatible with the OpenAI API. rawKeyStr := c.GetHeader("Authorization") logrus.Debug("proxyOther: key: ", key, ", rawKeyStr: ", rawKeyStr) if s.validateUnifiedKey(key) { // uid 0 means to use unified api key uid = "00000000-0000-0000-0000-000000000000" } else if len(key) > 0 { uid, valid = s.validateAPIKey(key) if !valid { return "", "", errdefs.Unauthorized(fmt.Errorf("invalid API key")) } } else if len(rawKeyStr) > 0 { strs := strings.Split(rawKeyStr, " ") if len(strs) != 2 { return "", "", errdefs.Unauthorized(fmt.Errorf("invalid Authorization API key")) } if strs[0] != "Bearer" { return "", "", errdefs.Unauthorized(fmt.Errorf("invalid Authorization API key")) } uid, valid = s.validateAPIKey(strs[1]) if !valid { return "", "", errdefs.Unauthorized(fmt.Errorf("invalid Authorization API key")) } } if len(uid) == 0 { return "", "", errdefs.Unauthorized(fmt.Errorf("invalid API key")) } return uid, deployment, nil } func (s *Server) proxyNoAuth(c *gin.Context) (string, string, error) { deployment := c.Param("id") if len(deployment) == 0 { return "", "", errdefs.InvalidParameter( fmt.Errorf("cannot find the deployment name in %s", c.Request.RequestURI)) } uid, found := s.getUIDFromDeploymentID(c.Request.Context(), deployment) if !found { return "", "", errdefs.InvalidParameter( fmt.Errorf("cannot find the user id from the deployment id")) } return uid, deployment, nil } func (s *Server) validateAPIKey(key string) (string, bool) { if !strings.HasPrefix(key, consts.APIKEY_PREFIX) { return "", false } apikeys := s.config.ModelZCloud.APIKeys uid, exit := apikeys[key] if exit { return uid, true } apiServerReady := make(chan struct{}) go func() { if err := s.modelzCloudClient.WaitForAPIServerReady(); err != nil { logrus.Fatalf("failed to wait for apiserver ready: %v", err) } close(apiServerReady) }() // Get from apiserver apikeys, err := s.modelzCloudClient.GetAPIKeys(context.Background(), apiServerReady, s.config.ModelZCloud.AgentToken, s.config.ModelZCloud.ID) if err != nil { logrus.Errorf("failed to get apikeys: %v", err) return "", false } uid, exit = apikeys[key] if exit { return uid, true } return "", false } func (s *Server) validateUnifiedKey(key string) bool { if !strings.HasPrefix(key, consts.APIKEY_PREFIX) { return false } if len(s.config.ModelZCloud.UnifiedAPIKey) != 0 && s.config.ModelZCloud.UnifiedAPIKey == key { return true } return false } ================================================ FILE: agent/pkg/server/server_factory.go ================================================ package server import ( "net/http" "github.com/dgraph-io/ristretto" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/client" ginlogrus "github.com/toorop/gin-logrus" "github.com/tensorchord/openmodelz/agent/pkg/config" "github.com/tensorchord/openmodelz/agent/pkg/event" "github.com/tensorchord/openmodelz/agent/pkg/k8s" "github.com/tensorchord/openmodelz/agent/pkg/log" "github.com/tensorchord/openmodelz/agent/pkg/metrics" "github.com/tensorchord/openmodelz/agent/pkg/prom" "github.com/tensorchord/openmodelz/agent/pkg/runtime" "github.com/tensorchord/openmodelz/agent/pkg/scaling" "github.com/tensorchord/openmodelz/agent/pkg/server/validator" ) type Server struct { router *gin.Engine metricsRouter *gin.Engine logger *logrus.Entry validator *validator.Validator runtime runtime.Runtime // endpointResolver resolves the requests from the client to the // corresponding inference kubernetes service. endpointResolver k8s.Resolver buildLogRequester log.Requester deploymentLogRequester log.Requester // prometheusClient is the client to query the prometheus server. // It is used in inference list. prometheusClient prom.PrometheusQuery metricsOptions metrics.MetricOptions // scaler scales the inference from 0 to 1. scaler *scaling.InferenceScaler config config.Config eventRecorder event.Interface modelzCloudClient *client.Client cache ristretto.Cache } func New(c config.Config) (Server, error) { router := gin.New() router.Use(ginlogrus.Logger(logrus.StandardLogger(), "/healthz")) router.Use(gin.Recovery()) // metrics server metricsRouter := gin.New() metricsRouter.Use(gin.Recovery()) if gin.Mode() == gin.DebugMode { logrus.SetLevel(logrus.DebugLevel) logrus.Debug("Allow CORS") router.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, AllowHeaders: []string{"*"}, })) } promCli := prom.NewPrometheusQuery(c.Metrics.PrometheusHost, c.Metrics.PrometheusPort, http.DefaultClient) logger := logrus.WithField("component", "server") s := Server{ router: router, metricsRouter: metricsRouter, config: c, logger: logger, validator: validator.New(), prometheusClient: promCli, } cache, err := ristretto.NewCache(&ristretto.Config{ NumCounters: 1e7, MaxCost: 1 << 28, BufferItems: 64, }) if err != nil { return s, err } s.cache = *cache if s.config.ModelZCloud.EventEnabled { logrus.Info("Event recording is enabled") cli, err := client.NewClientWithOpts( client.WithHost(s.config.ModelZCloud.URL)) if err != nil { return s, errors.Wrap(err, "failed to create modelz cloud client") } s.eventRecorder = event.NewEventRecorder(cli, s.config.ModelZCloud.AgentToken) } else { s.eventRecorder = event.NewFake() } s.registerRoutes() s.registerMetricsRoutes() if err := s.initKubernetesResources(); err != nil { return s, err } if c.ModelZCloud.Enabled { err := s.initModelZCloud(c.ModelZCloud.URL, c.ModelZCloud.AgentToken, c.ModelZCloud.Region) if err != nil { return s, err } } if err := s.initMetrics(); err != nil { return s, err } s.initLogs() return s, nil } ================================================ FILE: agent/pkg/server/server_handlerfunc.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) type HandlerFunc func(c *gin.Context) error func WrapHandler(handler HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { err := handler(c) if err != nil { var serverErr *Error if !errors.As(err, &serverErr) { serverErr = &Error{ HTTPStatusCode: http.StatusInternalServerError, Err: err, Message: err.Error(), } } serverErr.Request = c.Request.Method + " " + c.Request.URL.String() if gin.Mode() == "debug" { logrus.Debugf("error: %+v", err) } else { // Remove detailed info when in the release mode serverErr.Op = "" serverErr.Err = nil } c.JSON(serverErr.HTTPStatusCode, serverErr) c.Abort() return } } } ================================================ FILE: agent/pkg/server/server_init_kubernetes.go ================================================ package server import ( "context" "fmt" "time" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kubeinformers "k8s.io/client-go/informers" kubeinformersv1 "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" kubefledged "github.com/senthilrch/kube-fledged/pkg/client/clientset/versioned" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/pkg/event" "github.com/tensorchord/openmodelz/agent/pkg/k8s" "github.com/tensorchord/openmodelz/agent/pkg/log" "github.com/tensorchord/openmodelz/agent/pkg/runtime" "github.com/tensorchord/openmodelz/agent/pkg/scaling" ingressclient "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" clientset "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" informers "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" "github.com/tensorchord/openmodelz/modelzetes/pkg/signals" ) func (s *Server) initKubernetesResources() error { clientCmdConfig, err := clientcmd.BuildConfigFromFlags( s.config.KubeConfig.MasterURL, s.config.KubeConfig.Kubeconfig) if err != nil { return err } clientCmdConfig.QPS = float32(s.config.KubeConfig.QPS) clientCmdConfig.Burst = s.config.KubeConfig.Burst kubeClient, err := kubernetes.NewForConfig(clientCmdConfig) if err != nil { return err } inferenceClient, err := clientset.NewForConfig(clientCmdConfig) if err != nil { return err } var ingressClient ingressclient.Interface if s.config.Ingress.IngressEnabled { ingressClient, err = ingressclient.NewForConfig(clientCmdConfig) if err != nil { return err } } kubefledgedClient, err := kubefledged.NewForConfig(clientCmdConfig) if err != nil { return err } kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions( kubeClient, s.config.KubeConfig.ResyncPeriod) inferenceInformerFactory := informers.NewSharedInformerFactoryWithOptions( inferenceClient, s.config.KubeConfig.ResyncPeriod) // set up signals so we handle the first shutdown signal gracefully stopCh := signals.SetupSignalHandler() inferences := inferenceInformerFactory.Tensorchord().V2alpha1().Inferences() go inferences.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:inferences", consts.ProviderName), stopCh, inferences.Informer().HasSynced); !ok { s.logger.Errorf("failed to wait for cache to sync") } deployments := kubeInformerFactory.Apps().V1().Deployments() go deployments.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:deployments", consts.ProviderName), stopCh, deployments.Informer().HasSynced); !ok { s.logger.Errorf("failed to wait for cache to sync") } pods := kubeInformerFactory.Core().V1().Pods() s.podStartWatch(pods, kubeClient) go pods.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:pods", consts.ProviderName), stopCh, pods.Informer().HasSynced); !ok { s.logger.Errorf("failed to wait for cache to sync") } endpoints := kubeInformerFactory.Core().V1().Endpoints() go endpoints.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:endpoints", consts.ProviderName), stopCh, endpoints.Informer().HasSynced); !ok { s.logger.Errorf("failed to wait for cache to sync") } runtime, err := runtime.New(clientCmdConfig, endpoints, deployments, inferences, pods, kubeClient, ingressClient, kubefledgedClient, inferenceClient, s.eventRecorder, s.config.Ingress.IngressEnabled, s.config.ModelZCloud.EventEnabled, s.config.Build.BuildEnabled, s.config.Ingress.AnyIPToDomain, ) if err != nil { return err } s.runtime = runtime if s.config.Server.Dev { logrus.Warn("running in dev mode, using port forwarding to access pods, please do not use dev mode in production") s.endpointResolver = k8s.NewPortForwardingResolver(clientCmdConfig, kubeClient) } else { s.endpointResolver = k8s.NewEndpointResolver(endpoints.Lister()) } s.deploymentLogRequester = log.NewK8sAPIRequestor(kubeClient) s.scaler, err = scaling.NewInferenceScaler(runtime, s.config.Inference.CacheTTL) if err != nil { return err } if s.scaler == nil { return fmt.Errorf("scaler is nil") } return nil } // podStartWatch log event when pod start began and finished func (s *Server) podStartWatch(pods kubeinformersv1.PodInformer, client *kubernetes.Clientset) { pods.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { new := obj.(*v1.Pod) controlPlane, exist := new.Annotations[consts.AnnotationControlPlaneKey] // for inference created by modelz apiserver if !exist || controlPlane != consts.ModelzAnnotationValue { return } podWatchEventLog(s.eventRecorder, new, types.PodCreateEvent) start := time.Now() // Ticker will keep watching until pod start or timeout ticker := time.NewTicker(time.Second * 2) timeout := time.After(5 * time.Minute) go func() { for { select { case <-timeout: podWatchEventLog(s.eventRecorder, new, types.PodTimeoutEvent) return case <-ticker.C: pod, err := client.CoreV1().Pods(new.Namespace).Get(context.TODO(), new.Name, metav1.GetOptions{}) if err != nil { logrus.WithFields(logrus.Fields{ "namespace": pod.Namespace, "deployment": pod.Labels["app"], "name": pod.Name, }).Errorf("failed to get pod: %s", err) return } for _, c := range pod.Status.Conditions { if c.Type == v1.PodReady && c.Status == v1.ConditionTrue { podWatchEventLog(s.eventRecorder, pod, types.PodReadyEvent) label := prometheus.Labels{ "inference_name": fmt.Sprintf("%s.%s", pod.Labels["app"], pod.Namespace), "source_image": pod.Annotations[consts.AnnotationDockerImage]} s.metricsOptions.PodStartHistogram.With(label). Observe(time.Since(start).Seconds()) return } } } } }() }, }) } // log status for pod watch status transfer func podWatchEventLog(recorder event.Interface, obj *v1.Pod, event string) { deployment := obj.Labels["app"] err := recorder.CreateDeploymentEvent(obj.Namespace, deployment, event, obj.Name) if err != nil { logrus.WithFields(logrus.Fields{ "namespace": obj.Namespace, "deployment": deployment, "name": obj.Name, "event": event, }).Errorf("failed to create deployment event: %s", err) } } ================================================ FILE: agent/pkg/server/server_init_logs.go ================================================ package server import ( "github.com/tensorchord/openmodelz/agent/pkg/log" ) func (s *Server) initLogs() { if len(s.config.Logs.LokiURL) > 0 { s.logger.Info("enable Loki logs requester") s.buildLogRequester = log.NewLokiAPIRequestor( s.config.Logs.LokiURL, s.config.Logs.LokiUser, s.config.Logs.LokiToken) } } ================================================ FILE: agent/pkg/server/server_init_metrics.go ================================================ package server import ( "context" "github.com/tensorchord/openmodelz/agent/pkg/metrics" ) func (s *Server) initMetrics() error { metricsOptions := metrics.BuildMetricsOptions() s.metricsOptions = metricsOptions exporter := metrics.NewExporter(metricsOptions, s.runtime) metrics.RegisterExporter(exporter) exporter.StartServiceWatcher(context.TODO(), s.config.Metrics.PollingInterval) return nil } ================================================ FILE: agent/pkg/server/server_init_modelz_cloud.go ================================================ package server import ( "context" "fmt" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/agent/client" ) func (s *Server) initModelZCloud(url, token, region string) error { cluster := types.ManagedCluster{ Region: region, PrometheusURL: fmt.Sprintf("%s:%d", s.config.Metrics.PrometheusHost, s.config.Metrics.PrometheusPort), } cli, err := client.NewClientWithOpts( client.WithHost(url)) if err != nil { return errors.Wrap(err, "failed to create modelz cloud client") } s.modelzCloudClient = cli err = s.runtime.GetClusterInfo(&cluster) if err != nil { return errors.Wrap(err, "failed to get managed cluster info") } apiServerReady := make(chan struct{}) go func() { if err := s.modelzCloudClient.WaitForAPIServerReady(); err != nil { logrus.Fatalf("failed to wait for apiserver ready: %v", err) } close(apiServerReady) }() cluster.Status = types.ClusterStatusInit // after init modelz cloud client, register agent err = cli.RegisterAgent(context.Background(), token, &cluster) if err != nil { return errors.Wrap(err, "failed to register agent to modelz cloud") } s.config.ModelZCloud.ID = cluster.ID s.config.ModelZCloud.TokenID = cluster.TokenID s.config.ModelZCloud.Name = cluster.Name apikeys, err := s.modelzCloudClient.GetAPIKeys(context.Background(), apiServerReady, s.config.ModelZCloud.AgentToken, s.config.ModelZCloud.ID) if err != nil { logrus.Errorf("failed to get apikeys: %v", err) } s.config.ModelZCloud.APIKeys = apikeys namespaces, err := s.modelzCloudClient.GetNamespaces(context.Background(), apiServerReady, s.config.ModelZCloud.AgentToken, s.config.ModelZCloud.ID) if err != nil { logrus.Errorf("failed to get namespaces: %v", err) } nss := []string{} for _, ns := range namespaces.Items { nss = append(nss, ns) err = s.runtime.NamespaceCreate(context.Background(), ns) if err != nil { logrus.Errorf("failed to create namespace %s: %v", ns, err) continue } } s.config.ModelZCloud.UserNamespaces = nss return nil } ================================================ FILE: agent/pkg/server/server_init_route.go ================================================ package server import ( "github.com/gin-gonic/gin" swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" _ "github.com/tensorchord/openmodelz/agent/pkg/docs" "github.com/tensorchord/openmodelz/agent/pkg/metrics" ) const ( endpointInferencePlural = "/inferences" endpointInference = "/inference" endpointServerPlural = "/servers" endpointServer = "/server" endpointScaleInference = "/scale-inference" endpointInfo = "/info" endpointLogPlural = "/logs" endpointNamespacePlural = "/namespaces" endpointHealthz = "/healthz" endpointBuild = "/build" endpointImageCache = "/image-cache" ) func (s *Server) registerRoutes() { root := s.router.Group("/") v1 := s.router.Group("/api/v1") // swagger root.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) // dataplane root.Any("/inference/:name", WrapHandler(s.middlewareCallID), WrapHandler(s.handleInferenceProxy)) root.Any("/inference/:name/*proxyPath", WrapHandler(s.middlewareCallID), WrapHandler(s.handleInferenceProxy)) v1.Any("/mosec/:id/*proxyPath", WrapHandler(s.proxyMosec)) v1.Any("/gradio/:id/*proxyPath", WrapHandler(s.proxyGradio)) v1.Any("/streamlit/:id/*proxyPath", WrapHandler(s.proxyStreamlit)) v1.Any("/other/:id/*proxyPath", WrapHandler(s.proxyOther)) // healthz root.GET(endpointHealthz, WrapHandler(s.handleHealthz)) // landing page root.GET("/", WrapHandler(s.handleRoot)) // control plane controlPlane := root.Group("/system") // inferences controlPlane.GET(endpointInferencePlural, WrapHandler(s.handleInferenceList)) controlPlane.POST(endpointInferencePlural, WrapHandler(s.handleInferenceCreate)) controlPlane.PUT(endpointInferencePlural, WrapHandler(s.handleInferenceUpdate)) controlPlane.DELETE(endpointInferencePlural, WrapHandler(s.handleInferenceDelete)) controlPlane.POST(endpointScaleInference, WrapHandler(s.handleInferenceScale)) controlPlane.GET(endpointInference+"/:name", WrapHandler(s.handleInferenceGet)) // instances controlPlane.GET(endpointInference+"/:name/instances", WrapHandler(s.handleInferenceInstance)) controlPlane.GET(endpointInference+"/:name/instance/:instance/exec", WrapHandler(s.handleInferenceInstanceExec)) // info controlPlane.GET(endpointInfo, WrapHandler(s.handleInfo)) // servers controlPlane.GET(endpointServerPlural, WrapHandler(s.handleServerList)) controlPlane.POST(endpointServer+"/:name/labels", WrapHandler(s.handleServerLabelCreate)) controlPlane.DELETE(endpointServer+"/:name/delete", WrapHandler(s.handleServerDelete)) // logs controlPlane.GET(endpointLogPlural+endpointInference, WrapHandler(s.handleInferenceLogs)) controlPlane.GET(endpointLogPlural+endpointBuild, WrapHandler(s.handleBuildLogs)) // namespaces controlPlane.GET(endpointNamespacePlural, WrapHandler(s.handleNamespaceList)) controlPlane.POST(endpointNamespacePlural, WrapHandler(s.handleNamespaceCreate)) controlPlane.DELETE(endpointNamespacePlural, WrapHandler(s.handleNamespaceDelete)) // TODO(gaocegege): Support secrets // controlPlane.GET("/secrets") // builds if s.config.Build.BuildEnabled { controlPlane.GET(endpointBuild, WrapHandler(s.handleBuildList)) controlPlane.GET(endpointBuild+"/:name", WrapHandler(s.handleBuildGet)) controlPlane.POST(endpointBuild, WrapHandler(s.handleBuildCreate)) } // TODO(gaocegege): Support metrics // metrics // image cache controlPlane.POST(endpointImageCache, WrapHandler(s.handleImageCacheCreate)) } // registerMetricsRoutes registers the metrics routes. func (s *Server) registerMetricsRoutes() { s.metricsRouter.GET("/metrics", gin.WrapH(metrics.PrometheusHandler())) s.metricsRouter.GET(endpointHealthz, WrapHandler(s.handleHealthz)) } ================================================ FILE: agent/pkg/server/server_run.go ================================================ package server import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" "k8s.io/apimachinery/pkg/util/wait" ) func (s *Server) Run() error { srv := &http.Server{ Addr: fmt.Sprintf(":%d", s.config.Server.ServerPort), Handler: s.router, WriteTimeout: s.config.Server.WriteTimeout, ReadTimeout: s.config.Server.ReadTimeout, } go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logrus.Errorf("listen on port %d error: %v", s.config.Server.ServerPort, err) } }() metricsSrv := &http.Server{ Addr: fmt.Sprintf(":%d", s.config.Metrics.ServerPort), Handler: s.metricsRouter, ReadTimeout: s.config.Metrics.PollingInterval, WriteTimeout: s.config.Metrics.PollingInterval, } go func() { if err := metricsSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logrus.Errorf("listen on port %d error: %v", s.config.Metrics.ServerPort, err) } }() logrus.WithField("port", s.config.Server.ServerPort). Info("server is running...") logrus.WithField("metrics-port", s.config.Metrics.ServerPort). Info("metrics server is running...") if s.config.ModelZCloud.Enabled { // check apiserver is ready apiServerReady := make(chan struct{}) go func() { if err := s.modelzCloudClient.WaitForAPIServerReady(); err != nil { logrus.Fatalf("failed to wait for apiserver ready: %v", err) } close(apiServerReady) }() // websocket // build websocket go s.connect(apiServerReady) // heartbeat with apiserver go wait.UntilWithContext(context.Background(), func(ctx context.Context) { cluster := types.ManagedCluster{ Name: s.config.ModelZCloud.Name, ID: s.config.ModelZCloud.ID, Status: types.ClusterStatusActive, UpdatedAt: time.Now().UTC(), TokenID: s.config.ModelZCloud.TokenID, Region: s.config.ModelZCloud.Region, PrometheusURL: fmt.Sprintf("%s:%d", s.config.Metrics.PrometheusHost, s.config.Metrics.PrometheusPort), } err := s.runtime.GetClusterInfo(&cluster) if err != nil { logrus.Errorf("failed to get managed cluster info: %v", err) } err = s.modelzCloudClient.UpdateAgentStatus(ctx, apiServerReady, s.config.ModelZCloud.AgentToken, cluster) if err != nil { logrus.Errorf("failed to update agent status: %v", err) } logrus.Debugf("update agent status: %v", cluster) }, s.config.ModelZCloud.HeartbeatInterval) go wait.UntilWithContext(context.Background(), func(ctx context.Context) { apikeys, err := s.modelzCloudClient.GetAPIKeys(ctx, apiServerReady, s.config.ModelZCloud.AgentToken, s.config.ModelZCloud.ID) if err != nil { logrus.Errorf("failed to get apikeys: %v", err) } s.config.ModelZCloud.APIKeys = apikeys logrus.Debugf("update apikeys") }, s.config.ModelZCloud.HeartbeatInterval) // default 1min update, TODO(xieydd) make it configurable go wait.UntilWithContext(context.Background(), func(ctx context.Context) { namespaces, err := s.modelzCloudClient.GetNamespaces(ctx, apiServerReady, s.config.ModelZCloud.AgentToken, s.config.ModelZCloud.ID) if err != nil { logrus.Errorf("failed to get namespaces: %v", err) } for _, ns := range namespaces.Items { if ContainString(ns, s.config.ModelZCloud.UserNamespaces) { continue } err = s.runtime.NamespaceCreate(ctx, ns) if err != nil { logrus.Errorf("failed to create namespace %s: %v", ns, err) continue } s.config.ModelZCloud.UserNamespaces = append(s.config.ModelZCloud.UserNamespaces, ns) logrus.Debugf("update namespaces") } }, s.config.ModelZCloud.HeartbeatInterval) // default 1h update, make it configurable } quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logrus.Info("shutdown server") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return srv.Shutdown(ctx) } func ContainString(target string, strs []string) bool { for _, str := range strs { if str == target { return true } } return false } ================================================ FILE: agent/pkg/server/server_websocket.go ================================================ package server import ( "context" "net/http" "net/url" "time" "github.com/rancher/remotedialer" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/api/types" ) func (s *Server) connect(apiServerReady <-chan struct{}) { <-apiServerReady var clusterDialEndpoint string headers := http.Header{ "X-Cluster-ID": {s.config.ModelZCloud.ID}, "Agent-Token": {s.config.ModelZCloud.AgentToken}, } u, err := url.Parse(s.config.ModelZCloud.URL) if err != nil { logrus.Errorf("failed to parse url: %v", err) } switch u.Scheme { case "http": clusterDialEndpoint = "ws://" + u.Host + types.DailEndPointSuffix case "https": clusterDialEndpoint = "wss://" + u.Host + types.DailEndPointSuffix } ctx := context.Background() go func() { for { remotedialer.ClientConnect(ctx, clusterDialEndpoint, headers, nil, func(proto, address string) bool { return true }, nil) select { case <-ctx.Done(): return case <-time.After(s.config.ModelZCloud.HeartbeatInterval): // retry connect after interval } } }() // retry( // s.config.ModelZCloud.HeartbeatInterval, // func() error { // logrus.Debugf("run websocket server") // ctx := context.Background() // err := remotedialer.ClientConnect(ctx, clusterDialEndpoint, headers, nil, // func(proto, address string) bool { return true }, nil) // if err != nil { // logrus.Errorf("failed to connect to apiserver: %v", err) // return err // } // return nil // }, // ) } func retry(sleep time.Duration, f func() error) { i := 1 for { err := f() if err == nil { return } else { logrus.Errorf("retry %d times, still failed", i) time.Sleep(sleep) i++ } } } ================================================ FILE: agent/pkg/server/static/index.html ================================================ OpenModelZ Serving | Running

OpenModelZ server is running Version: {{.Version}}

Please check out the documentation for the next steps. Please contact us on discord if there is any issue.

================================================ FILE: agent/pkg/server/static/landing.go ================================================ package static import ( "bytes" _ "embed" "html/template" "github.com/tensorchord/openmodelz/agent/pkg/version" ) //go:embed index.html var htmlTemplate string type htmlStruct struct { Version string } func RenderLoadingPage() (*bytes.Buffer, error) { tmpl, err := template.New("root").Parse(htmlTemplate) if err != nil { return nil, err } data := htmlStruct{ Version: version.GetAgentVersion(), } var buffer bytes.Buffer if err := tmpl.Execute(&buffer, data); err != nil { return nil, err } return &buffer, nil } ================================================ FILE: agent/pkg/server/static/page_loading.go ================================================ package static import ( "bytes" "html/template" "github.com/tensorchord/openmodelz/agent/api/types" ) const htmlDeploymentTemplate = `Loading - {{.Framework}}

{{.StatusString}}Framework: {{.Framework}}Deployment: {{.Deployment}}Scheduling, ContainerCreating, Initializing, RunningStatus: {{.InstanceStatus}}The page will auto refresh once the request is completed. Kindly wait for the page to reload automatically. If the issue persists, please contact modelz support team on discord for assistance.

` type htmlDeploymentStruct struct { Deployment string Framework string ID string StatusString string InstanceStatus string } func RenderDeploymentLoadingPage(framework, id, statusString, deployment string, instances []types.InferenceDeploymentInstance) (*bytes.Buffer, error) { tmpl, err := template.New("root").Parse(htmlDeploymentTemplate) if err != nil { return nil, err } data := htmlDeploymentStruct{ Deployment: deployment, Framework: framework, ID: id, StatusString: statusString, InstanceStatus: "Scaling", } if len(instances) > 0 { data.InstanceStatus = string(instances[0].Status.Phase) } var buffer bytes.Buffer if err := tmpl.Execute(&buffer, data); err != nil { return nil, err } return &buffer, nil } ================================================ FILE: agent/pkg/server/suite_test.go ================================================ package server import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" runtimemock "github.com/tensorchord/openmodelz/agent/pkg/runtime/mock" ) var ( ctrl *gomock.Controller mockRuntime *runtimemock.MockRuntime server *Server ) func mkContext(method string, path string, header map[string][]string, body io.Reader) *gin.Context { c, _ := gin.CreateTestContext(httptest.NewRecorder()) if c == nil { panic(c) } req, _ := http.NewRequest(method, path, body) for k, vs := range header { for _, v := range vs { req.Header.Add(k, v) } } c.Request = req return c } func mkJsonBodyContext(method string, path string, header map[string][]string, body any) *gin.Context { jsonValue, err := json.Marshal(body) if err != nil { panic(err) } return mkContext(method, path, header, bytes.NewBuffer(jsonValue)) } func setQuery(c *gin.Context, query map[string]string) { params, _ := url.ParseQuery(c.Request.URL.RawQuery) for k, v := range query { params.Set(k, v) } c.Request.URL.RawQuery = params.Encode() } func setParam(c *gin.Context, param map[string]string) { for k, v := range param { c.Params = []gin.Param{{Key: k, Value: v}} } } func TestBuilder(t *testing.T) { gin.SetMode(gin.ReleaseMode) RegisterFailHandler(Fail) RunSpecs(t, "server") } var _ = BeforeSuite(func() { ctrl = gomock.NewController(GinkgoT()) mockRuntime = runtimemock.NewMockRuntime(ctrl) }) ================================================ FILE: agent/pkg/server/user.go ================================================ package server import ( "context" "github.com/sirupsen/logrus" ) func (s *Server) getUIDFromDeploymentID(ctx context.Context, id string) (string, bool) { uid, exit := s.cache.Get(id) if exit { return uid.(string), true } uid, err := s.modelzCloudClient.GetUIDFromDeploymentID(ctx, s.config.ModelZCloud.AgentToken, s.config.ModelZCloud.ID, id) if err != nil { logrus.Errorf("failed to get uid from deployment id: %v", err) return "", false } // no expiration s.cache.SetWithTTL(id, uid, 1, 0) return uid.(string), true } ================================================ FILE: agent/pkg/server/validator/validator.go ================================================ package validator import ( "fmt" "regexp" "k8s.io/apimachinery/pkg/util/rand" "github.com/tensorchord/openmodelz/agent/api/types" ) const ( defaultMinReplicas = 0 defaultMaxReplicas = 1 maxReplicas = 5 defaultTargetLoad = 100 defaultZeroDuration = 300 defaultStartupDuration = 600 defaultBuildDuration = "40m" defaultHTTPProbePath = "/" ) var ( dnsValidRegex = `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` ) type Validator struct { validDNS *regexp.Regexp } func New() *Validator { return &Validator{ validDNS: regexp.MustCompile(dnsValidRegex), } } // Validates that the service name is valid for Kubernetes func (v Validator) ValidateService(service string) error { matched := v.validDNS.MatchString(service) if matched { return nil } return fmt.Errorf("service: (%s) is invalid, must be a valid DNS entry", service) } // DefaultDeployRequest sets default values for the deploy request. func (v Validator) DefaultDeployRequest(request *types.InferenceDeployment) { if request.Spec.Scaling == nil { request.Spec.Scaling = &types.ScalingConfig{} } if request.Spec.Scaling.MinReplicas == nil { request.Spec.Scaling.MinReplicas = new(int32) *request.Spec.Scaling.MinReplicas = defaultMinReplicas } if request.Spec.Scaling.MaxReplicas == nil { request.Spec.Scaling.MaxReplicas = new(int32) *request.Spec.Scaling.MaxReplicas = defaultMinReplicas } if request.Spec.Scaling.TargetLoad == nil { request.Spec.Scaling.TargetLoad = new(int32) *request.Spec.Scaling.TargetLoad = defaultTargetLoad } if request.Spec.Scaling.Type == nil { request.Spec.Scaling.Type = new(types.ScalingType) *request.Spec.Scaling.Type = types.ScalingTypeCapacity } if request.Spec.Scaling.ZeroDuration == nil { request.Spec.Scaling.ZeroDuration = new(int32) *request.Spec.Scaling.ZeroDuration = defaultZeroDuration } if request.Spec.Scaling.StartupDuration == nil { request.Spec.Scaling.StartupDuration = new(int32) *request.Spec.Scaling.StartupDuration = defaultStartupDuration } if request.Spec.Framework == "" { request.Spec.Framework = types.FrameworkOther } } // ValidateDeployRequest validates that the service name is valid for Kubernetes func (v Validator) ValidateDeployRequest(request *types.InferenceDeployment) error { if request.Spec.Name == "" { return fmt.Errorf("service: is required") } err := v.ValidateService(request.Spec.Name) if err != nil { return err } if request.Spec.Image == "" { return fmt.Errorf("image: is required") } if request.Spec.Scaling == nil { return fmt.Errorf("scaling: is required") } if request.Spec.Framework == types.FrameworkOther { if request.Spec.Port == nil { return fmt.Errorf("port: is required for other framework") } } return nil } func (v Validator) ValidateBuildRequest(request *types.Build) error { if request.Spec.Name == "" { return fmt.Errorf("name: is required") } if request.Spec.BuildTarget.ArtifactImage == "" { return fmt.Errorf("artifact image: is required") } return nil } func (v Validator) ValidateImageCacheRequest(request *types.ImageCache) error { if request.Name == "" { return fmt.Errorf("name: is required") } if request.Namespace == "" { return fmt.Errorf("namespace: is required") } if request.Image == "" { return fmt.Errorf("image: is required") } if request.NodeSelector == "" { return fmt.Errorf("node selector: is required") } return nil } func (v Validator) DefaultBuildRequest(request *types.Build) { if request.Spec.BuildTarget.Builder == "" { request.Spec.BuildTarget.Builder = types.BuilderTypeImage } if request.Spec.BuildTarget.Builder != types.BuilderTypeImage { if request.Spec.Branch == "" && request.Spec.Revision == "" { request.Spec.Branch = "main" } if request.Spec.BuildTarget.Duration == "" { request.Spec.BuildTarget.Duration = defaultBuildDuration } } if request.Spec.BuildTarget.ArtifactImageTag == "" { request.Spec.BuildTarget.ArtifactImageTag = rand.String(8) } } ================================================ FILE: agent/pkg/version/version.go ================================================ /* Copyright The TensorChord Inc. Copyright The BuildKit Authors. Copyright The containerd 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 version import ( "fmt" "regexp" "runtime" "strings" "sync" ) var ( // Package is filled at linking time Package = "github.com/tensorchord/openmodelz/agent" // Revision is filled with the VCS (e.g. git) revision being used to build // the program at linking time. Revision = "" version = "0.0.0+unknown" buildDate = "1970-01-01T00:00:00Z" // output from `date -u +'%Y-%m-%dT%H:%M:%SZ'` gitCommit = "" // output from `git rev-parse HEAD` gitTag = "" // output from `git describe --exact-match --tags HEAD` (if clean tree state) gitTreeState = "" // determined from `git status --porcelain`. either 'clean' or 'dirty' developmentFlag = "false" ) // Version contains version information type Version struct { Version string BuildDate string GitCommit string GitTag string GitTreeState string GoVersion string Compiler string Platform string } func (v Version) String() string { return v.Version } // SetGitTagForE2ETest sets the gitTag for test purpose. func SetGitTagForE2ETest(tag string) { gitTag = tag } // GetAgentVersion gets version information func GetAgentVersion() string { var versionStr string if gitCommit != "" && gitTag != "" && gitTreeState == "clean" && developmentFlag == "false" { // if we have a clean tree state and the current commit is tagged, // this is an official release. versionStr = gitTag } else { // otherwise formulate a version string based on as much metadata // information we have available. if strings.HasPrefix(version, "v") { versionStr = version } else { versionStr = "v" + version } if len(gitCommit) >= 7 { versionStr += "+" + gitCommit[0:7] if gitTreeState != "clean" { versionStr += ".dirty" } } else { versionStr += "+unknown" } } return versionStr } // GetVersion returns the version information func GetVersion() Version { return Version{ Version: GetAgentVersion(), BuildDate: buildDate, GitCommit: gitCommit, GitTag: gitTag, GitTreeState: gitTreeState, GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } } var ( reRelease *regexp.Regexp reDev *regexp.Regexp reOnce sync.Once ) func UserAgent() string { version := GetVersion().String() reOnce.Do(func() { reRelease = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+$`) reDev = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+`) }) if matches := reRelease.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] } else if matches := reDev.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] + "-dev" } return "openmodelz/agent/" + version } ================================================ FILE: agent/sqlc.yaml ================================================ version: "2" sql: - engine: "postgresql" queries: "sql/query/" schema: "./sql/schema.sql" gen: go: package: "query" sql_package: "pgx/v4" out: "pkg/query" emit_prepared_queries: true emit_interface: true emit_exact_table_names: false emit_json_tags: true ================================================ FILE: autoscaler/.gitignore ================================================ # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out *.report # Dependency directories (remove the comment below to include it) vendor/ # Go workspace file go.work .vscode/* .idea # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix __debug_bin bin/ debug-bin/ /build.envd .ipynb_checkpoints/ cover.html cmd/test/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ wheelhouse/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST .demo/ pkg/docs/swagger.* ================================================ FILE: autoscaler/Dockerfile ================================================ FROM ubuntu:22.04 LABEL maintainer="modelz-support@tensorchord.ai" COPY autoscaler /usr/bin/autoscaler ENTRYPOINT ["/usr/bin/autoscaler"] ================================================ FILE: autoscaler/Makefile ================================================ # Copyright 2022 TensorChord Inc. # # The old school Makefile, following are required targets. The Makefile is written # to allow building multiple binaries. You are free to add more targets or change # existing implementations, as long as the semantics are preserved. # # make - default to 'build' target # make lint - code analysis # make test - run unit test (or plus integration test) # make build - alias to build-local target # make build-local - build local binary targets # make build-linux - build linux binary targets # make container - build containers # $ docker login registry -u username -p xxxxx # make push - push containers # make clean - clean up targets # # Not included but recommended targets: # make e2e-test # # The makefile is also responsible to populate project version information. # # # Tweak the variables based on your project. # # This repo's root import path (under GOPATH). ROOT := github.com/tensorchord/openmodelz/autoscaler # Target binaries. You can build multiple binaries for a single project. TARGETS := autoscaler # Container image prefix and suffix added to targets. # The final built images are: # $[REGISTRY]/$[IMAGE_PREFIX]$[TARGET]$[IMAGE_SUFFIX]:$[VERSION] # $[REGISTRY] is an item from $[REGISTRIES], $[TARGET] is an item from $[TARGETS]. IMAGE_PREFIX ?= $(strip ) IMAGE_SUFFIX ?= $(strip ) # Container registries. REGISTRY ?= ghcr.io/tensorchord # Container registry for base images. BASE_REGISTRY ?= docker.io BASE_REGISTRY_USER ?= modelzai # Disable CGO by default. CGO_ENABLED ?= 0 GOOS ?= $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH) # # These variables should not need tweaking. # # It's necessary to set this because some environments don't link sh -> bash. export SHELL := bash # It's necessary to set the errexit flags for the bash shell. export SHELLOPTS := errexit PACKAGE_NAME := github.com/tensorchord/openmodelz GOLANG_CROSS_VERSION ?= v1.17.6 # Project main package location (can be multiple ones). CMD_DIR := ./cmd # Project output directory. OUTPUT_DIR := ./bin DEBUG_DIR := ./debug-bin # Build directory. BUILD_DIR := ./build # Current version of the project. VERSION ?= $(shell git describe --match 'v[0-9]*' --always --tags --abbrev=0) BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') GIT_COMMIT=$(shell git rev-parse HEAD) GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi) GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi) GITSHA ?= $(shell git rev-parse --short HEAD) # Track code version with Docker Label. DOCKER_LABELS ?= git-describe="$(shell date -u +v%Y%m%d)-$(shell git describe --tags --always --dirty)" # Golang standard bin directory. GOPATH ?= $(shell go env GOPATH) GOROOT ?= $(shell go env GOROOT) BIN_DIR := $(GOPATH)/bin GOLANGCI_LINT := $(BIN_DIR)/golangci-lint # check if we need embed the dashboard DASHBOARD_BUILD ?= debug # Default golang flags used in build and test # -mod=vendor: force go to use the vendor files instead of using the `$GOPATH/pkg/mod` # -p: the number of programs that can be run in parallel # -count: run each test and benchmark 1 times. Set this flag to disable test cache export GOFLAGS ?= -count=1 # # Define all targets. At least the following commands are required: # # All targets. .PHONY: help lint test build container push addlicense debug debug-local build-local generate clean test-local addlicense-install release build-image .DEFAULT_GOAL:=build build: build-local ## Build the release version of envd help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) debug: debug-local ## Build the debug version of envd # more info about `GOGC` env: https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint lint: $(GOLANGCI_LINT) ## Lint GO code @$(GOLANGCI_LINT) run $(GOLANGCI_LINT): curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin mockgen-install: go install github.com/golang/mock/mockgen@v1.6.0 addlicense-install: go install github.com/google/addlicense@latest build-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags $(DASHBOARD_BUILD) -trimpath -v -o $(OUTPUT_DIR)/$${target} \ -ldflags "-s -w -X $(ROOT)/pkg/version.version=$(VERSION) -X $(ROOT)/pkg/version.buildDate=$(BUILD_DATE) -X $(ROOT)/pkg/version.gitCommit=$(GIT_COMMIT) -X $(ROOT)/pkg/version.gitTreeState=$(GIT_TREE_STATE)" \ $(CMD_DIR)/$${target}; \ done # It is used by vscode to attach into the process. debug-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) go build -tags $(DASHBOARD_BUILD) -trimpath \ -v -o $(DEBUG_DIR)/$${target} \ -gcflags='all=-N -l' \ $(CMD_DIR)/$${target}; \ done addlicense: addlicense-install ## Add license to GO code files addlicense -l mpl -c "TensorChord Inc." $$(find . -type f -name '*.go' | grep -v pkg/docs/docs.go) test-local: @go test -tags=$(DASHBOARD_BUILD) -v -race -coverprofile=coverage.out ./... test: ## Run the tests @go test -tags=$(DASHBOARD_BUILD) -race -coverpkg=./pkg/... -coverprofile=coverage.out ./... @go tool cover -func coverage.out | tail -n 1 | awk '{ print "Total coverage: " $$3 }' clean: ## Clean the outputs and artifacts @-rm -vrf ${OUTPUT_DIR} @-rm -vrf ${DEBUG_DIR} @-rm -vrf build dist .eggs *.egg-info fmt: ## Run go fmt against code. go fmt ./... vet: ## Run go vet against code. go vet ./... build-image: build-local docker build -t ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/modelz-autoscaler:dev -f Dockerfile ./bin docker push ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/modelz-autoscaler:dev release: @if [ ! -f ".release-env" ]; then \ echo "\033[91m.release-env is required for release\033[0m";\ exit 1;\ fi docker run \ --rm \ --privileged \ -e CGO_ENABLED=1 \ --env-file .release-env \ -v /var/run/docker.sock:/var/run/docker.sock \ -v `pwd`:/go/src/$(PACKAGE_NAME) \ -v `pwd`/sysroot:/sysroot \ -w /go/src/$(PACKAGE_NAME) \ goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ release --rm-dist tsschema: swag @cd dashboard; pnpm tsschema generate: mockgen-install sqlc-install swag tsschema @mockgen -source pkg/query/querier.go -destination pkg/query/mock/mock.go -package mock @sqlc generate dashboard-build: @cd dashboard; pnpm build ================================================ FILE: autoscaler/cmd/autoscaler/main.go ================================================ package main import ( "fmt" "os" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" "github.com/tensorchord/openmodelz/autoscaler/pkg/autoscalerapp" "github.com/tensorchord/openmodelz/autoscaler/pkg/version" ) func run(args []string) error { cli.VersionPrinter = func(c *cli.Context) { fmt.Println(c.App.Name, version.Package, c.App.Version, version.Revision) } app := autoscalerapp.New() return app.Run(args) } func handleErr(err error) { if err == nil { return } logrus.Error(err) } func main() { err := run(os.Args) handleErr(err) } ================================================ FILE: autoscaler/pkg/autoscaler/factory.go ================================================ package autoscaler import ( "net/http" "net/url" "time" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/client" "github.com/tensorchord/openmodelz/autoscaler/pkg/prom" ) type Opt struct { GatewayHost string PrometheusHost string BasicAuthEnabled bool SecretPath string PrometheusPort int Interval time.Duration } func New(opt Opt) (*Scaler, error) { logrus.Info("Creating autoscaler with options: ", opt) gatewayURL, err := url.Parse(opt.GatewayHost) if err != nil { return nil, errors.Wrap(err, "failed to parse gateway host") } client, err := client.NewClientWithOpts( client.WithHost(gatewayURL.String()), ) if err != nil { return nil, errors.Wrap(err, "failed to create client") } prometheusQuery := prom.NewPrometheusQuery(opt.PrometheusHost, opt.PrometheusPort, &http.Client{}) as := newScaler(client, &prometheusQuery, newLoadCache(), newInferenceCache()) return as, nil } ================================================ FILE: autoscaler/pkg/autoscaler/inferencecache.go ================================================ package autoscaler import ( "time" "github.com/tensorchord/openmodelz/agent/api/types" ) type Inference struct { Deployment types.InferenceDeployment Timestamp time.Time } type InferenceCache struct { inference map[string]Inference } func newInferenceCache() *InferenceCache { return &InferenceCache{ inference: make(map[string]Inference), } } func (i *InferenceCache) Set(key string, inference Inference) { i.inference[key] = inference } func (i *InferenceCache) Get(key string, expireTime time.Duration) (types.InferenceDeployment, bool) { inference, ok := i.inference[key] // expired if !ok || time.Since(inference.Timestamp) > expireTime { return types.InferenceDeployment{}, false } return inference.Deployment, ok } ================================================ FILE: autoscaler/pkg/autoscaler/loadcache.go ================================================ package autoscaler import "time" // LoadCache is a cache for load metrics. type LoadCache struct { load map[string]Load } type Load struct { ScalingType string CurrentStartedRequests float64 CurrentLoad float64 Timestamp time.Time } func newLoadCache() *LoadCache { return &LoadCache{ load: make(map[string]Load), } } func (l *LoadCache) Get(key string) (Load, bool) { load, ok := l.load[key] return load, ok } func (l *LoadCache) Set(key string, load Load) { l.load[key] = load } ================================================ FILE: autoscaler/pkg/autoscaler/scaler.go ================================================ package autoscaler import ( "context" "fmt" "math" "net/url" "strconv" "strings" "time" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/client" "github.com/tensorchord/openmodelz/agent/pkg/scaling" "github.com/tensorchord/openmodelz/autoscaler/pkg/prom" ) type Scaler struct { PromQuery *prom.PrometheusQuery client *client.Client LoadCache *LoadCache ZeroCache map[string]time.Time InferenceCache *InferenceCache } func newScaler(c *client.Client, promQuery *prom.PrometheusQuery, loadCache *LoadCache, inferanceCache *InferenceCache) *Scaler { return &Scaler{ client: c, PromQuery: promQuery, LoadCache: loadCache, ZeroCache: make(map[string]time.Time), InferenceCache: inferanceCache, } } func (s *Scaler) AutoScale(interval time.Duration) { ticker := time.NewTicker(interval) quit := make(chan struct{}) TTL := 1 * time.Minute for { select { case <-ticker.C: // Detect if the instance pod always restart, // if pod restart count in 10 minutes before last update time is more than 2, will scale it down. results, err := s.GetRestartMetrics() if err != nil { logrus.Info("Get Restart Metrics of inference Failed") continue } inferenceCount := make(map[string]int) for _, ts := range results { labels := ts.Labels podName, inferenceName, namespace := "", "", "" for _, label := range labels { switch label.Name { case "pod": podName = label.Value case "inference_name": inferenceName = label.Value case "namespace": namespace = label.Value } } if len(ts.Samples) < 1 { logrus.Infof("Sample not found for inference %s.", inferenceName) continue } strs := strings.Split(inferenceName, ".") if len(strs) != 2 { logrus.Infof("Invalid inference name: %s", inferenceName) continue } name := strs[0] resp, err := s.client.InstanceList(context.TODO(), namespace, name) if err != nil { logrus.WithFields(logrus.Fields{ "service": inferenceName, "error": err, }).Error("failed to get instance list") continue } for _, instance := range resp { if instance.Spec.Name == podName { if instance.Status.Phase == "CrashLoopBackOff" { inferenceCount[inferenceName] += 1 } } } } if len(inferenceCount) != 0 { for inferenceName, count := range inferenceCount { strs := strings.Split(inferenceName, ".") if len(strs) != 2 { logrus.Infof("Invalid inference name: %s", inferenceName) continue } name := strs[0] namespace := strs[1] resp, ok := s.InferenceCache.Get(inferenceName, TTL) if !ok { resp, err = s.client.InferenceGet(context.TODO(), namespace, name) if err != nil { logrus.WithFields(logrus.Fields{ "service": inferenceName, "error": err, }).Error("failed to get inference") continue } // update inference cache inference := Inference{ Timestamp: time.Now(), Deployment: resp, } s.InferenceCache.Set(inferenceName, inference) } // check if the instance already exists var expectedReplicas int totalReplicas := resp.Status.Replicas if count > int(totalReplicas) { expectedReplicas = 0 } else { expectedReplicas = int(totalReplicas) - count } if expectedReplicas != int(totalReplicas) { logrus.Infof("Scaling inference %s to %d replicas", inferenceName, expectedReplicas) // Add event to record the scale down operation eventMessage := fmt.Sprintf("Deployment %d replicas always CrashLoopBackOff, system scale down the deployment replicas to %d", count, expectedReplicas) if err := s.client.InferenceScale(context.TODO(), namespace, name, expectedReplicas, eventMessage); err != nil { logrus.WithFields(logrus.Fields{ "service": inferenceName, "expected": expectedReplicas, "error": err, }).Error("failed to scale inference") continue } // update the inference, set minReplicas to expectedReplicas if resp.Spec.Scaling.MinReplicas != nil && *resp.Spec.Scaling.MinReplicas > int32(expectedReplicas) { resp.Status.EventMessage = fmt.Sprintf("Deployment %d replicas always CrashLoopBackOff, system scales down the replicas to %d, original min replicas is %d, reset it to %d", count, expectedReplicas, resp.Spec.Scaling.MinReplicas, expectedReplicas) *resp.Spec.Scaling.MinReplicas = int32(expectedReplicas) if _, err := s.client.DeploymentUpdate(context.TODO(), namespace, resp); err != nil { logrus.WithFields(logrus.Fields{ "service": inferenceName, "expected": expectedReplicas, "error": err, }).Error("failed to update inference") continue } } } } } s.LoadCache = newLoadCache() s.GetLoadMetrics() for service, lc := range s.LoadCache.load { // if instances of inference are restarting, do not scale it. if value, ok := inferenceCount[service]; ok && value > 0 { continue } strs := strings.Split(service, ".") if len(strs) != 2 { logrus.Infof("Invalid inference name: %s", service) continue } name := strs[0] namespace := strs[1] resp, ok := s.InferenceCache.Get(service, TTL) if !ok { resp, err = s.client.InferenceGet(context.TODO(), namespace, name) if err != nil { logrus.WithFields(logrus.Fields{ "service": service, "error": err, }).Error("failed to get inference") continue } // update inference cache inference := Inference{ Timestamp: time.Now(), Deployment: resp, } s.InferenceCache.Set(service, inference) } if resp.Spec.Labels == nil { logrus.WithFields(logrus.Fields{ "service": service, "error": err, }).Error("failed to get inference labels") continue } var expectedReplicas int var targetLoad int // If the inference has a target load label, use that instead. if resp.Spec.Scaling != nil && resp.Spec.Scaling.TargetLoad != nil { targetLoad = int(*resp.Spec.Scaling.TargetLoad) expectedReplicas = int(math.Ceil( lc.CurrentLoad / float64(*resp.Spec.Scaling.TargetLoad))) } if expectedReplicas == 0 { // Check the current start requests to see if the inference is being used. if lc.CurrentStartedRequests > 0 { logrus.WithFields(logrus.Fields{ "service": service, "current_started_requests": lc.CurrentStartedRequests, "target_load": lc.CurrentLoad, }).Debug("inference is being used") expectedReplicas = 1 } } var maxReplicas, minReplicas int var zeroDuration time.Duration if resp.Spec.Scaling != nil { if resp.Spec.Scaling.MinReplicas != nil { minReplicas = int(*resp.Spec.Scaling.MinReplicas) } else { minReplicas = scaling.DefaultMinReplicas } if resp.Spec.Scaling.MaxReplicas != nil { maxReplicas = int(*resp.Spec.Scaling.MaxReplicas) } else { maxReplicas = scaling.DefaultMaxReplicas } if resp.Spec.Scaling.ZeroDuration != nil { zeroDuration = time.Duration(*resp.Spec.Scaling.ZeroDuration) * time.Second } else { zeroDuration = scaling.DefaultZeroDuration } } if expectedReplicas > maxReplicas { logrus.Infof("Expected replicas (%d) exceeds max replicas (%d) for inference %s", expectedReplicas, maxReplicas, service) expectedReplicas = maxReplicas } if expectedReplicas < minReplicas { logrus.Infof("Expected replicas (%d) is less than min replicas (%d) for inference %s", expectedReplicas, minReplicas, service) expectedReplicas = minReplicas } availableReplicas := resp.Status.AvailableReplicas totalReplicas := resp.Status.Replicas if expectedReplicas == int(totalReplicas) { // If the expected replicas is the same as the current replicas, remove the entry from the zero cache. delete(s.ZeroCache, service) logrus.WithFields(logrus.Fields{ "service": service, "replicas": totalReplicas, "expectedReplicas": expectedReplicas, }).Debug("delete zero cache") } if expectedReplicas == 0 && totalReplicas != 0 { if availableReplicas == 0 { // If the expected replicas is 0 and there are no available replicas, // set the expected replicas to 1 to prevent the inference from being scaled to zero. expectedReplicas = 1 } else { // If the expected replicas is 0 and there is no entry in the zero cache, add one. if _, ok := s.ZeroCache[service]; !ok { s.ZeroCache[service] = time.Now() } // If the inference has been idle for longer than the zero duration, scale to zero. if time.Since(s.ZeroCache[service]) > zeroDuration { logrus.Infof("Inference %s has been idle for %s, scaling to zero", service, zeroDuration) } else { // If the inference has not been idle for longer than the zero duration, scale to 1. expectedReplicas = 1 } } } if expectedReplicas == 1 && totalReplicas == 0 { // If the expected replicas is 1 and the current replicas is 0, do nothing since the scaling handler in gateway will take care of this situation. expectedReplicas = 0 } logrus.WithFields(logrus.Fields{ "service": service, "replicas": totalReplicas, "expectedReplicas": expectedReplicas, "availableReplicas": availableReplicas, "currentLoad": lc.CurrentLoad, "targetLoad": targetLoad, "zeroDuration": zeroDuration, "zeroCache": s.ZeroCache[service], }).Debug("start scaling (replicas)") if expectedReplicas != int(totalReplicas) { delete(s.ZeroCache, service) logrus.Infof("Scaling inference %s to %d replicas", service, expectedReplicas) eventMessage := fmt.Sprintf("Scaling inference based load, current %f, target %d", lc.CurrentLoad, targetLoad) if err := s.client.InferenceScale(context.TODO(), namespace, name, expectedReplicas, eventMessage); err != nil { logrus.WithFields(logrus.Fields{ "service": service, "expected": expectedReplicas, "error": err, }).Error("failed to scale inference") continue } } } case <-quit: return } } } func (s *Scaler) GetLoadMetrics() { results, err := s.PromQuery.Fetch(url.QueryEscape("job:inference_current_load:sum")) if err != nil { // log the error but continue, the mixIn will correctly handle the empty results. logrus.Infof("Error querying Prometheus: %s\n", err.Error()) } currentSumResults, err := s.PromQuery.Fetch( url.QueryEscape("job:inference_current_started:max_sum")) if err != nil { // log the error but continue, the mixIn will correctly handle the empty results. logrus.Infof("Error querying Prometheus: %s\n", err.Error()) } for _, result := range results.Data.Result { currentLoad := 0.0 switch val := result.Value[1].(type) { case string: f, err := strconv.ParseFloat(val, 64) if err != nil { logrus.Infof("add_metrics: unable to convert value %q for metric: %s", val, err) continue } currentLoad = f } timestamp := time.Now() switch val := result.Value[0].(type) { case float64: timestamp = time.Unix(int64(val), 0) } if l, ok := s.LoadCache.Get(result.Metric.InferenceName); ok { l.CurrentLoad = currentLoad l.Timestamp = timestamp s.LoadCache.Set(result.Metric.InferenceName, l) } else { s.LoadCache.Set(result.Metric.InferenceName, Load{ CurrentLoad: currentLoad, Timestamp: timestamp, }) } } for _, result := range currentSumResults.Data.Result { currentSum := 0.0 switch val := result.Value[1].(type) { case string: f, err := strconv.ParseFloat(val, 64) if err != nil { logrus.Infof("add_metrics: unable to convert value %q for metric: %s", val, err) continue } currentSum = f } timestamp := time.Now() switch val := result.Value[0].(type) { case float64: timestamp = time.Unix(int64(val), 0) } if l, ok := s.LoadCache.Get(result.Metric.InferenceName); ok { l.CurrentStartedRequests = currentSum l.Timestamp = timestamp s.LoadCache.Set(result.Metric.InferenceName, l) } else { s.LoadCache.Set(result.Metric.InferenceName, Load{ CurrentStartedRequests: currentSum, Timestamp: timestamp, }) } } } func (s *Scaler) GetRestartMetrics() ([]*prom.TimeSeries, error) { // record this rule in prometheus // (sum by (pod,namespace) (increase(kube_pod_container_status_restarts_total{namespace=~"modelz-(.*)"}[10m])) > 2) * on (pod) group_left(inference_name) (label_join(label_replace(kube_pod_info{created_by_kind="ReplicaSet",namespace=~"modelz-(.*)"}, "inference", "$1", "created_by_name", "(.+)-.+"), "inference_name",".","inference","namespace")) query := "pod_restart_count_over_2_10m" tsList, err := s.PromQuery.Query(query, time.Now()) if err != nil { logrus.Infof("Error querying Prometheus: %s\n", err.Error()) return nil, err } return tsList, nil } ================================================ FILE: autoscaler/pkg/autoscalerapp/root.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package autoscalerapp import ( "time" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" cli "github.com/urfave/cli/v2" "github.com/tensorchord/openmodelz/autoscaler/pkg/autoscaler" "github.com/tensorchord/openmodelz/autoscaler/pkg/server" "github.com/tensorchord/openmodelz/autoscaler/pkg/version" ) type EnvdServerApp struct { *cli.App } func New() EnvdServerApp { internalApp := cli.NewApp() internalApp.EnableBashCompletion = true internalApp.Name = "modelz-autoscaler" internalApp.Usage = "Autoscaler for modelz serverless inference platform" internalApp.HideHelpCommand = true internalApp.HideVersion = false internalApp.Version = version.GetVersion().String() internalApp.Flags = []cli.Flag{ &cli.BoolFlag{ Name: "debug", Usage: "enable debug output in logs", }, &cli.StringFlag{ Name: "gateway-host", Usage: "host for gateway", EnvVars: []string{"MODELZ_GATEWAY_HOST"}, Aliases: []string{"gh"}, }, &cli.StringFlag{ Name: "prometheus-host", Usage: "host for prometheus", Value: "prometheus", EnvVars: []string{"MODELZ_PROMETHEUS_HOST"}, Aliases: []string{"ph"}, }, &cli.IntFlag{ Name: "prometheus-port", Usage: "port for prometheus", Value: 9090, EnvVars: []string{"MODELZ_PROMETHEUS_PORT"}, Aliases: []string{"pp"}, }, &cli.BoolFlag{ Name: "basic-auth", Usage: "enable basic auth", EnvVars: []string{"MODELZ_BASIC_AUTH"}, Aliases: []string{"ba"}, Value: true, }, &cli.PathFlag{ Name: "secret-path", Usage: "path to secrets", Value: "/var/modelz/secrets", EnvVars: []string{"MODELZ_SECRET_PATH"}, Aliases: []string{"sp"}, }, &cli.DurationFlag{ Name: "interval", Usage: "interval for autoscaling", Value: time.Second, EnvVars: []string{"MODELZ_INTERVAL"}, Aliases: []string{"i"}, }, } internalApp.Action = runServer // Deal with debug flag. var debugEnabled bool internalApp.Before = func(context *cli.Context) error { debugEnabled = context.Bool("debug") if debugEnabled { logrus.SetReportCaller(true) logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) logrus.SetLevel(logrus.DebugLevel) } else { logrus.SetFormatter(&logrus.JSONFormatter{}) } return nil } return EnvdServerApp{ App: internalApp, } } func runServer(clicontext *cli.Context) error { opt := autoscaler.Opt{ GatewayHost: clicontext.String("gateway-host"), PrometheusHost: clicontext.String("prometheus-host"), BasicAuthEnabled: clicontext.Bool("basic-auth"), SecretPath: clicontext.Path("secret-path"), PrometheusPort: clicontext.Int("prometheus-port"), Interval: clicontext.Duration("interval"), } as, err := autoscaler.New(opt) if err != nil { return errors.Wrap(err, "failed to create autoscaler") } logrus.Info("starting system info server") go server.RunInfoServe() logrus.Info("starting autoscaler") as.AutoScale(opt.Interval) return nil } ================================================ FILE: autoscaler/pkg/prom/prom.go ================================================ package prom import ( "context" "encoding/json" "fmt" "io/ioutil" "net/http" "time" "github.com/prometheus/client_golang/api" promapiv1 "github.com/prometheus/client_golang/api/prometheus/v1" prommodel "github.com/prometheus/common/model" "github.com/sirupsen/logrus" ) // PrometheusQuery represents parameters for querying Prometheus type PrometheusQuery struct { Port int Host string Client *http.Client } type PrometheusQueryFetcher interface { Fetch(query string) (*VectorQueryResponse, error) } // NewPrometheusQuery create a NewPrometheusQuery func NewPrometheusQuery(host string, port int, client *http.Client) PrometheusQuery { return PrometheusQuery{ Client: client, Host: host, Port: port, } } // Fetch queries aggregated stats func (q PrometheusQuery) Fetch(query string) (*VectorQueryResponse, error) { req, reqErr := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s:%d/api/v1/query?query=%s", q.Host, q.Port, query), nil) if reqErr != nil { return nil, reqErr } res, getErr := q.Client.Do(req) if getErr != nil { return nil, getErr } if res.Body != nil { defer res.Body.Close() } bytesOut, readErr := ioutil.ReadAll(res.Body) if readErr != nil { return nil, readErr } if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code from Prometheus want: %d, got: %d, body: %s", http.StatusOK, res.StatusCode, string(bytesOut)) } var values VectorQueryResponse unmarshalErr := json.Unmarshal(bytesOut, &values) if unmarshalErr != nil { return nil, fmt.Errorf("error unmarshalling result: %s, '%s'", unmarshalErr, string(bytesOut)) } return &values, nil } // TODO(xieydd) Refactor PrometheusQuery // Query queries Prometheus with given query string and time func (q PrometheusQuery) Query(query string, time time.Time) ([]*TimeSeries, error) { var ts []*TimeSeries client, err := api.NewClient(api.Config{ Address: fmt.Sprintf("http://%s:%d", q.Host, q.Port), }) if err != nil { return ts, err } api := promapiv1.NewAPI(client) results, warnings, err := api.Query(context.TODO(), query, time) if len(warnings) != 0 { logrus.Info("Prom query warnings", "warnings", warnings) } if err != nil { return ts, err } logrus.Info("Prom query result", "result", results.String(), "resultsType", results.Type()) return convertPromResultsToTimeSeries(results) } func convertPromResultsToTimeSeries(value prommodel.Value) ([]*TimeSeries, error) { var results []*TimeSeries typeValue := value.Type() switch typeValue { case prommodel.ValMatrix: if matrix, ok := value.(prommodel.Matrix); ok { for _, sampleStream := range matrix { if sampleStream == nil { continue } ts := NewTimeSeries() for key, val := range sampleStream.Metric { ts.AppendLabel(string(key), string(val)) } for _, pair := range sampleStream.Values { ts.AppendSample(int64(pair.Timestamp/1000), float64(pair.Value)) } results = append(results, ts) } return results, nil } else { return results, fmt.Errorf("prometheus value type is %v, but assert failed", typeValue) } case prommodel.ValVector: if vector, ok := value.(prommodel.Vector); ok { for _, sample := range vector { if sample == nil { continue } ts := NewTimeSeries() for key, val := range sample.Metric { ts.AppendLabel(string(key), string(val)) } // for vector, all the sample has the same timestamp. just one point for each metric ts.AppendSample(int64(sample.Timestamp/1000), float64(sample.Value)) results = append(results, ts) } return results, nil } else { return results, fmt.Errorf("prometheus value type is %v, but assert failed", typeValue) } case prommodel.ValScalar: return results, fmt.Errorf("not support for scalar when use timeseries") case prommodel.ValString: return results, fmt.Errorf("not support for string when use timeseries") case prommodel.ValNone: return results, fmt.Errorf("prometheus return value type is none") } return results, fmt.Errorf("prometheus return unknown model value type %v", typeValue) } ================================================ FILE: autoscaler/pkg/prom/types.go ================================================ package prom import ( "fmt" "sort" ) type VectorQueryResponse struct { Data struct { Result []struct { Metric struct { InferenceName string `json:"inference_name"` } Value []interface{} `json:"value"` } } } // Ref: https://github.com/gocrane/crane/blob/9aaeb2aa9cf9f43a31842b4663e48bc47ac05f17/pkg/common/types.go // TimeSeries is a stream of samples that belong to a metric with a set of labels type TimeSeries struct { // A collection of Labels that are attached by monitoring system as metadata // for the metrics, which are known as dimensions. Labels []Label // A collection of Samples in chronological order. Samples []Sample } // Sample pairs a Value with a Timestamp. type Sample struct { Value float64 Timestamp int64 } // A Label is a Name and Value pair that provides additional information about the metric. // It is metadata for the metric. For example, Kubernetes pod metrics always have // 'namespace' label that represents which namespace the pod belongs to. type Label struct { Name string Value string } func (s *Sample) String() string { return fmt.Sprintf("%d %f", s.Timestamp, s.Value) } func (l *Label) String() string { return l.Name + "=" + l.Value } func (ts *TimeSeries) SetLabels(labels []Label) { ts.Labels = labels } func (ts *TimeSeries) SetSamples(samples []Sample) { ts.Samples = samples } func (ts *TimeSeries) AppendLabel(key, val string) { ts.Labels = append(ts.Labels, Label{key, val}) } func (ts *TimeSeries) AppendSample(timestamp int64, val float64) { ts.Samples = append(ts.Samples, Sample{Timestamp: timestamp, Value: val}) } func (ts *TimeSeries) SortSampleAsc() { sort.Slice(ts.Samples, func(i, j int) bool { if ts.Samples[i].Timestamp < ts.Samples[j].Timestamp { return true } else { return false } }) } func NewTimeSeries() *TimeSeries { return &TimeSeries{ Labels: make([]Label, 0), Samples: make([]Sample, 0), } } ================================================ FILE: autoscaler/pkg/server/status.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package server import ( "encoding/json" "fmt" "net/http" "time" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/autoscaler/pkg/version" ) func getInfo(w http.ResponseWriter, r *http.Request) { scalerInfo := map[string]string{"version": version.GetEnvdVersion()} jsonOut, marshalErr := json.Marshal(scalerInfo) if marshalErr != nil { logrus.Infof("Error during unmarshal of autoscaler info request %s\n", marshalErr.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(jsonOut) } func RunInfoServe() { tcpPort := 8080 serverMux := http.NewServeMux() serverMux.HandleFunc("/system/info", getInfo) s := &http.Server{ Addr: fmt.Sprintf(":%d", tcpPort), ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: http.DefaultMaxHeaderBytes, // 1MB - can be overridden by setting Server.MaxHeaderBytes. Handler: serverMux, } s.ListenAndServe() } ================================================ FILE: autoscaler/pkg/version/version.go ================================================ /* Copyright The TensorChord Inc. Copyright The BuildKit Authors. Copyright The containerd 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 version import ( "fmt" "regexp" "runtime" "strings" "sync" ) var ( // Package is filled at linking time Package = "github.com/tensorchord/openmodelz/autoscaler" // Revision is filled with the VCS (e.g. git) revision being used to build // the program at linking time. Revision = "" version = "0.0.0+unknown" buildDate = "1970-01-01T00:00:00Z" // output from `date -u +'%Y-%m-%dT%H:%M:%SZ'` gitCommit = "" // output from `git rev-parse HEAD` gitTag = "" // output from `git describe --exact-match --tags HEAD` (if clean tree state) gitTreeState = "" // determined from `git status --porcelain`. either 'clean' or 'dirty' developmentFlag = "false" ) // Version contains envd version information type Version struct { Version string BuildDate string GitCommit string GitTag string GitTreeState string GoVersion string Compiler string Platform string } func (v Version) String() string { return v.Version } // SetGitTagForE2ETest sets the gitTag for test purpose. func SetGitTagForE2ETest(tag string) { gitTag = tag } // GetEnvdVersion gets Envd version information func GetEnvdVersion() string { var versionStr string if gitCommit != "" && gitTag != "" && gitTreeState == "clean" && developmentFlag == "false" { // if we have a clean tree state and the current commit is tagged, // this is an official release. versionStr = gitTag } else { // otherwise formulate a version string based on as much metadata // information we have available. if strings.HasPrefix(version, "v") { versionStr = version } else { versionStr = "v" + version } if len(gitCommit) >= 7 { versionStr += "+" + gitCommit[0:7] if gitTreeState != "clean" { versionStr += ".dirty" } } else { versionStr += "+unknown" } } return versionStr } // GetVersion returns the version information func GetVersion() Version { return Version{ Version: GetEnvdVersion(), BuildDate: buildDate, GitCommit: gitCommit, GitTag: gitTag, GitTreeState: gitTreeState, GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } } var ( reRelease *regexp.Regexp reDev *regexp.Regexp reOnce sync.Once ) func UserAgent() string { version := GetVersion().String() reOnce.Do(func() { reRelease = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+$`) reDev = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+`) }) if matches := reRelease.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] } else if matches := reDev.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] + "-dev" } return "envd/" + version } ================================================ FILE: go.mod ================================================ module github.com/tensorchord/openmodelz go 1.20 replace ( github.com/anthhub/forwarder => github.com/tensorchord/forwarder v0.0.0-20230713171536-b1b52b398d3a github.com/senthilrch/kube-fledged v0.10.0 => github.com/tensorchord/kube-fledged v0.2.0 ) require ( github.com/anthhub/forwarder v1.1.0 github.com/cockroachdb/errors v1.10.0 github.com/dchest/uniuri v1.2.0 github.com/dgraph-io/ristretto v0.1.1 github.com/docker/docker v23.0.2+incompatible github.com/docker/go-connections v0.4.0 github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 github.com/gin-contrib/cors v1.4.0 github.com/gin-gonic/gin v1.9.1 github.com/golang/mock v1.5.0 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.2 github.com/jackc/pgconn v1.14.1 github.com/jackc/pgx/v4 v4.18.1 github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/moby/moby v24.0.4+incompatible github.com/moby/term v0.5.0 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.16.0 github.com/prometheus/common v0.44.0 github.com/segmentio/analytics-go/v3 v3.2.1 github.com/senthilrch/kube-fledged v0.10.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.8.12 github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f github.com/urfave/cli/v2 v2.3.0 golang.org/x/net v0.14.0 golang.org/x/term v0.11.0 k8s.io/api v0.27.4 k8s.io/apimachinery v0.27.4 k8s.io/client-go v0.27.4 k8s.io/code-generator v0.27.4 k8s.io/klog v1.0.0 ) require ( github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect ) require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/redact v1.1.5 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.1 // indirect github.com/go-openapi/spec v0.20.8 // indirect github.com/go-openapi/swag v0.22.3 // 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.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/puddle v1.3.0 // 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.4 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect github.com/rancher/remotedialer v0.3.0 github.com/rivo/uniseg v0.4.2 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/backo-go v1.0.0 // 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.11 // indirect github.com/xlab/treeprint v1.1.0 // indirect go.starlark.net v0.0.0-20221019144234-6ce4ce37fe55 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.12.0 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.1.0 // indirect golang.org/x/tools v0.9.3 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // 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 gotest.tools/v3 v3.5.0 // indirect k8s.io/cli-runtime v0.27.4 // indirect k8s.io/gengo v0.0.0-20220902162205-c0856e24416d // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.2 // indirect sigs.k8s.io/kustomize/kyaml v0.14.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/errors v1.10.0 h1:lfxS8zZz1+OjtV4MtNWgboi/W5tyLEB6VQZBXN+0VUU= github.com/cockroachdb/errors v1.10.0/go.mod h1:lknhIsEVQ9Ss/qKDBQS/UqFSvPQjOwNq2qyKAxtHRqE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v23.0.2+incompatible h1:q81C2qQ/EhPm8COZMUGOQYh4qLv4Xu6CXELJ3WK/mlU= github.com/docker/docker v23.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49 h1:6SNWi8VxQeCSwmLuTbEvJd7xvPmdS//zvMBWweZLgck= github.com/dustinkirkland/golang-petname v0.0.0-20230626224747-e794b9370d49/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 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.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 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.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU= github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 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.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 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.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4= github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 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.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/moby v24.0.4+incompatible h1:20Bf1sfJpspHMAUrxRFplG31Sriaw7Z9/jUEuJk6mqI= github.com/moby/moby v24.0.4+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rancher/remotedialer v0.3.0 h1:y1EO8JCsgZo0RcqTUp6U8FXcBAv27R+TLnWRcpvX1sM= github.com/rancher/remotedialer v0.3.0/go.mod h1:BwwztuvViX2JrLLUwDlsYt5DiyUwHLlzynRwkZLAY0Q= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/analytics-go/v3 v3.2.1 h1:G+f90zxtc1p9G+WigVyTR0xNfOghOGs/PYAlljLOyeg= github.com/segmentio/analytics-go/v3 v3.2.1/go.mod h1:p8owAF8X+5o27jmvUognuXxdtqvSGtD0ZrfY2kcS9bE= github.com/segmentio/backo-go v1.0.0 h1:kbOAtGJY2DqOR0jfRkYEorx/b18RgtepGtY3+Cpe6qA= github.com/segmentio/backo-go v1.0.0/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/tensorchord/forwarder v0.0.0-20230713171536-b1b52b398d3a h1:q4GoeuagHfbdl7JGSU0AcArYXnsA3p2+dzBdx7AZHP0= github.com/tensorchord/forwarder v0.0.0-20230713171536-b1b52b398d3a/go.mod h1:PfpNmyy0g95SWDoSxXH5MPAlFJ9S04w7cBmIMS6U89U= github.com/tensorchord/kube-fledged v0.2.0 h1:wNJNcot0/CxLRtdnG34/EVwVI8WgLoJ2uxAfiUtVfNg= github.com/tensorchord/kube-fledged v0.2.0/go.mod h1:m4ncbmr05mQDRdJAl+UuCcgf6cpipU333En8lbob2u4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY= github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ= 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 v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= go.starlark.net v0.0.0-20221019144234-6ce4ce37fe55 h1:UETCDFV7xVE6L29SnwA1vzkJEYGwffjjmxURPkstP6A= go.starlark.net v0.0.0-20221019144234-6ce4ce37fe55/go.mod h1:kIVgS18CjmEC3PqMd5kaJSGEifyV/CeB9x506ZJ1Vbk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 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/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/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= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= k8s.io/api v0.27.4 h1:0pCo/AN9hONazBKlNUdhQymmnfLRbSZjd5H5H3f0bSs= k8s.io/api v0.27.4/go.mod h1:O3smaaX15NfxjzILfiln1D8Z3+gEYpjEpiNA/1EVK1Y= k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= k8s.io/cli-runtime v0.23.5/go.mod h1:oY6QDF2qo9xndSq32tqcmRp2UyXssdGrLfjAVymgbx4= k8s.io/cli-runtime v0.27.4 h1:Zb0eci+58eHZNnoHhjRFc7W88s8dlG12VtIl3Nv2Hto= k8s.io/cli-runtime v0.27.4/go.mod h1:k9Z1xiZq2xNplQmehpDquLgc+rE+pubpO1cK4al4Mlw= k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk= k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc= k8s.io/code-generator v0.27.4 h1:bw2xFEBnthhCSC7Bt6FFHhPTfWX21IJ30GXxOzywsFE= k8s.io/code-generator v0.27.4/go.mod h1:DPung1sI5vBgn4AGKtlPRQAyagj/ir/4jI55ipZHVww= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20220902162205-c0856e24416d h1:U9tB195lKdzwqicbJvyJeOXV7Klv+wNAWENRnXEGi08= k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= 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/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= sigs.k8s.io/kustomize/api v0.13.2 h1:kejWfLeJhUsTGioDoFNJET5LQe/ajzXhJGYoU+pJsiA= sigs.k8s.io/kustomize/api v0.13.2/go.mod h1:DUp325VVMFVcQSq+ZxyDisA8wtldwHxLZbr1g94UHsw= sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= sigs.k8s.io/kustomize/kyaml v0.14.1 h1:c8iibius7l24G2wVAGZn/Va2wNys03GXLjYVIcFVxKA= sigs.k8s.io/kustomize/kyaml v0.14.1/go.mod h1:AN1/IpawKilWD7V+YvQwRGUvuUOOWpjsHu6uHwonSF4= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= ================================================ FILE: ingress-operator/.DEREK.yml ================================================ redirect: https://raw.githubusercontent.com/openfaas/faas/master/.DEREK.yml ================================================ FILE: ingress-operator/.dockerignore ================================================ .git .github .vscode .tools artifacts examples hack ================================================ FILE: ingress-operator/.gitignore ================================================ faas-o6s # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ .idea/ bin/ password.txt faas-netes/** test.yaml cluster.yaml ================================================ FILE: ingress-operator/.tools/README.md ================================================ # Tools folder The tools folder allows us a space to define and pin external dependencies. If they are go based tools we can create individual `mod` files that allow us to download or install these tools independent of the main package. ## Tools 1. `k8s.io/code-gen` this package needs to be _downloaded_ not installed. But we can not use the copy created in the `vendor` folder because vendor does not make a complete clone, it only keeps the Go files, and the code-gen project has several bash scripts that we need to reference. The main project `Makefile` will attempt to keep the `code-generator.mod` file in sync with the `go.mod`. It should not need to be manually edited, but it does need to be committed. ================================================ FILE: ingress-operator/.tools/code-generator.mod ================================================ module _ // Fake module so that we can install code-generator separate from the project go 1.16 require ( k8s.io/code-generator v0.21.3 ) ================================================ FILE: ingress-operator/.tools/code-generator.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= ================================================ FILE: ingress-operator/.vscode/settings.json ================================================ { "go.inferGopath": false, "cSpell.words": [ "Infof", "appsv", "corev", "faas", "faasclientset", "faasscheme", "faasv", "handler", "klog", "kube", "kubeclientset", "kubeconfig", "kubeinformers", "logtostderr", "metav", "netv", "networkingv", "sync", "syncer", "threadiness", "traefik" ] } ================================================ FILE: ingress-operator/Dockerfile ================================================ FROM ubuntu:22.04 LABEL maintainer="modelz-support@tensorchord.ai" COPY ingress-operator /usr/bin/ingress-operator ENTRYPOINT ["/usr/bin/ingress-operator"] ================================================ FILE: ingress-operator/LICENSE ================================================ MIT License Copyright (c) 2023 TensorChord Inc. Copyright (c) 2017-2019 OpenFaaS Author(s) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: ingress-operator/Makefile ================================================ # Copyright 2022 TensorChord Inc. # # The old school Makefile, following are required targets. The Makefile is written # to allow building multiple binaries. You are free to add more targets or change # existing implementations, as long as the semantics are preserved. # # make - default to 'build' target # make lint - code analysis # make test - run unit test (or plus integration test) # make build - alias to build-local target # make build-local - build local binary targets # make build-linux - build linux binary targets # make container - build containers # $ docker login registry -u username -p xxxxx # make push - push containers # make clean - clean up targets # # Not included but recommended targets: # make e2e-test # # The makefile is also responsible to populate project version information. # # # Tweak the variables based on your project. # # This repo's root import path (under GOPATH). ROOT := github.com/tensorchord/openmodelz/ingress-operator # Target binaries. You can build multiple binaries for a single project. TARGETS := ingress-operator # Container image prefix and suffix added to targets. # The final built images are: # $[REGISTRY]/$[IMAGE_PREFIX]$[TARGET]$[IMAGE_SUFFIX]:$[VERSION] # $[REGISTRY] is an item from $[REGISTRIES], $[TARGET] is an item from $[TARGETS]. IMAGE_PREFIX ?= $(strip ) IMAGE_SUFFIX ?= $(strip ) # Container registries. REGISTRY ?= ghcr.io/tensorchord # Container registry for base images. BASE_REGISTRY ?= docker.io BASE_REGISTRY_USER ?= modelzai # Disable CGO by default. CGO_ENABLED ?= 0 # # These variables should not need tweaking. # # It's necessary to set this because some environments don't link sh -> bash. export SHELL := bash # It's necessary to set the errexit flags for the bash shell. export SHELLOPTS := errexit PACKAGE_NAME := github.com/tensorchord/openmodelz/ingress-operator GOLANG_CROSS_VERSION ?= v1.17.6 # Project main package location (can be multiple ones). CMD_DIR := ./cmd # Project output directory. OUTPUT_DIR := ./bin DEBUG_DIR := ./debug-bin # Build directory. BUILD_DIR := ./build # Current version of the project. VERSION ?= $(shell git describe --match 'v[0-9]*' --always --tags --abbrev=0) BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') GIT_COMMIT=$(shell git rev-parse HEAD) GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi) GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi) GITSHA ?= $(shell git rev-parse --short HEAD) # Track code version with Docker Label. DOCKER_LABELS ?= git-describe="$(shell date -u +v%Y%m%d)-$(shell git describe --tags --always --dirty)" # Golang standard bin directory. GOPATH ?= $(shell go env GOPATH) GOROOT ?= $(shell go env GOROOT) BIN_DIR := $(GOPATH)/bin GOLANGCI_LINT := $(BIN_DIR)/golangci-lint # check if we need embed the dashboard DASHBOARD_BUILD ?= debug # Default golang flags used in build and test # -mod=vendor: force go to use the vendor files instead of using the `$GOPATH/pkg/mod` # -p: the number of programs that can be run in parallel # -count: run each test and benchmark 1 times. Set this flag to disable test cache export GOFLAGS ?= -count=1 # # Define all targets. At least the following commands are required: # # All targets. .PHONY: help lint test build container push addlicense debug debug-local build-local generate clean test-local addlicense-install release build-image .DEFAULT_GOAL:=build build: build-local ## Build the release version of envd help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) debug: debug-local ## Build the debug version of envd # more info about `GOGC` env: https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint lint: $(GOLANGCI_LINT) ## Lint GO code @$(GOLANGCI_LINT) run $(GOLANGCI_LINT): curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin mockgen-install: go install github.com/golang/mock/mockgen@v1.6.0 addlicense-install: go install github.com/google/addlicense@latest build-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags $(DASHBOARD_BUILD) -trimpath -v -o $(OUTPUT_DIR)/$${target} \ -ldflags "-s -w -X $(ROOT)/pkg/version.version=$(VERSION) -X $(ROOT)/pkg/version.buildDate=$(BUILD_DATE) -X $(ROOT)/pkg/version.gitCommit=$(GIT_COMMIT) -X $(ROOT)/pkg/version.gitTreeState=$(GIT_TREE_STATE)" \ $(CMD_DIR)/$${target}; \ done # It is used by vscode to attach into the process. debug-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) go build -tags $(DASHBOARD_BUILD) -trimpath \ -v -o $(DEBUG_DIR)/$${target} \ -gcflags='all=-N -l' \ $(CMD_DIR)/$${target}; \ done addlicense: addlicense-install ## Add license to GO code files addlicense -l mpl -c "TensorChord Inc." $$(find . -type f -name '*.go' | grep -v pkg/docs/docs.go) test-local: @go test -tags=$(DASHBOARD_BUILD) -v -race -coverprofile=coverage.out ./... test: ## Run the tests @go test -tags=$(DASHBOARD_BUILD) -race -coverpkg=./pkg/... -coverprofile=coverage.out ./... @go tool cover -func coverage.out | tail -n 1 | awk '{ print "Total coverage: " $$3 }' clean: ## Clean the outputs and artifacts @-rm -vrf ${OUTPUT_DIR} @-rm -vrf ${DEBUG_DIR} @-rm -vrf build dist .eggs *.egg-info fmt: ## Run go fmt against code. go fmt ./... vet: ## Run go vet against code. go vet ./... build-image: build-local docker build -t ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/ingress-operator:dev -f Dockerfile ./bin docker push ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/ingress-operator:dev release: @if [ ! -f ".release-env" ]; then \ echo "\033[91m.release-env is required for release\033[0m";\ exit 1;\ fi docker run \ --rm \ --privileged \ -e CGO_ENABLED=1 \ --env-file .release-env \ -v /var/run/docker.sock:/var/run/docker.sock \ -v `pwd`:/go/src/$(PACKAGE_NAME) \ -v `pwd`/sysroot:/sysroot \ -w /go/src/$(PACKAGE_NAME) \ goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ release --rm-dist ================================================ FILE: ingress-operator/artifacts/.gitignore ================================================ ================================================ FILE: ingress-operator/artifacts/crds/tensorchord.ai_inferenceingresses.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.5.0 creationTimestamp: null name: inferenceingresses.tensorchord.ai spec: group: tensorchord.ai names: kind: InferenceIngress listKind: InferenceIngressList plural: inferenceingresses singular: inferenceingress scope: Namespaced versions: - additionalPrinterColumns: - jsonPath: .spec.domain name: Domain type: string name: v1 schema: openAPIV3Schema: description: InferenceIngress describes an OpenFaaS function type: object required: - spec properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: InferenceIngressSpec is the spec for a InferenceIngressSpec resource. It must be created in the same namespace as the gateway, i.e. openfaas. type: object required: - domain - framework - function properties: bypassGateway: description: BypassGateway, when true creates an Ingress record directly for the Function name without using the gateway in the hot path type: boolean domain: description: Domain such as "api.example.com" type: string framework: type: string function: description: Function such as "nodeinfo" type: string ingressType: description: IngressType such as "nginx" type: string path: description: Path such as "/v1/profiles/view/(.*)", or leave empty for default type: string tls: description: Enable TLS via cert-manager type: object properties: enabled: type: boolean issuerRef: description: ObjectReference is a reference to an object with a given name and kind. type: object required: - name properties: kind: type: string name: type: string served: true storage: true subresources: {} status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: [] ================================================ FILE: ingress-operator/artifacts/operator-amd64.yaml ================================================ --- apiVersion: apps/v1 kind: Deployment metadata: name: ingress-operator namespace: openfaas spec: replicas: 1 selector: matchLabels: app: ingress-operator template: metadata: labels: app: ingress-operator annotations: prometheus.io.scrape: 'false' spec: serviceAccountName: ingress-operator containers: - name: operator image: docker.io/alexellis2/ingress-operator:1 imagePullPolicy: Always command: - ./ingress-operator env: - name: ingress_namespace value: openfaas resources: limits: memory: 128Mi requests: memory: 25Mi ================================================ FILE: ingress-operator/artifacts/operator-rbac.yaml ================================================ --- apiVersion: v1 kind: ServiceAccount metadata: name: ingress-operator namespace: openfaas --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: ingress-operator-rw namespace: openfaas rules: - apiGroups: ["openfaas.com"] resources: ["functioningresses"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["extensions", "networking", "networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # - apiGroups: ["certmanager.k8s.io"] # resources: ["certificates"] # verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: ingress-operator-rw namespace: openfaas roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: ingress-operator-rw subjects: - kind: ServiceAccount name: ingress-operator namespace: openfaas ================================================ FILE: ingress-operator/cmd/ingress-operator/main.go ================================================ package main import ( "fmt" "os" cli "github.com/urfave/cli/v2" klog "k8s.io/klog" // required to authenticate against GKE clusters _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "github.com/tensorchord/openmodelz/ingress-operator/pkg/app" "github.com/tensorchord/openmodelz/ingress-operator/pkg/version" ) func run(args []string) error { cli.VersionPrinter = func(c *cli.Context) { fmt.Println(c.App.Name, version.Package, c.App.Version, version.Revision) } klog.InitFlags(nil) a := app.New() return a.Run(args) } func handleErr(err error) { if err == nil { return } klog.Error(err) os.Exit(1) } func main() { err := run(os.Args) handleErr(err) } ================================================ FILE: ingress-operator/hack/boilerplate.go.txt ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ ================================================ FILE: ingress-operator/hack/custom-boilerplate.go.txt ================================================ /* Copyright 2023 OpenFaaS Author(s) Licensed under the MIT license. See LICENSE file in the project root for full license information. */ ================================================ FILE: ingress-operator/hack/print-codegen-version.sh ================================================ #!/bin/bash # This scripts exists primarily so that it can be used in the Makefile. # It is needed because the `($shell ...)` command was having issues with the pipe. # Extracting it to a script was the simplest solution. grep 'k8s.io/code-generator' go.mod | awk '{print $2}' ================================================ FILE: ingress-operator/hack/update-codegen.sh ================================================ #!/usr/bin/env bash # copied from: https://github.com/weaveworks/flagger/tree/master/hack set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(git rev-parse --show-toplevel)/ingress-operator echo "SCRIPT_ROOT is ${SCRIPT_ROOT}" # Grab code-generator version from go.sum. CODEGEN_VERSION=$(grep 'k8s.io/code-generator' go.sum | awk '{print $2}' | head -1) CODEGEN_PKG=$(echo `go env GOPATH`"/pkg/mod/k8s.io/code-generator@${CODEGEN_VERSION}") echo ">> Using ${CODEGEN_PKG}" # code-generator does work with go.mod but makes assumptions about # the project living in `$GOPATH/src`. To work around this and support # any location; create a temporary directory, use this as an output # base, and copy everything back once generated. TEMP_DIR=$(mktemp -d) cleanup() { echo ">> Removing ${TEMP_DIR}" rm -rf ${TEMP_DIR} } trap "cleanup" EXIT SIGINT echo ">> Temporary output directory ${TEMP_DIR}" # Ensure we can execute. chmod +x ${CODEGEN_PKG}/generate-groups.sh ${CODEGEN_PKG}/generate-groups.sh all \ github.com/tensorchord/openmodelz/ingress-operator/pkg/client github.com/tensorchord/openmodelz/ingress-operator/pkg/apis \ modelzetes:v1 \ --output-base "${TEMP_DIR}" \ --go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt # Copy everything back. cp -r "${TEMP_DIR}/github.com/tensorchord/openmodelz/ingress-operator/." "${SCRIPT_ROOT}/" ================================================ FILE: ingress-operator/hack/update-crds.sh ================================================ #!/bin/bash export controllergen="$GOPATH/bin/controller-gen" export PKG=sigs.k8s.io/controller-tools/cmd/controller-gen@v0.7.0 if [ ! -e "$controllergen" ]; then echo "Getting $PKG" go install $PKG fi "$controllergen" \ crd \ schemapatch:manifests=./artifacts/crds \ paths=./pkg/apis/... \ output:dir=./artifacts/crds ================================================ FILE: ingress-operator/hack/verify-codegen.sh ================================================ #!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(git rev-parse --show-toplevel) DIFFROOT="${SCRIPT_ROOT}/pkg" TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/pkg" _tmp="${SCRIPT_ROOT}/_tmp" cleanup() { rm -rf "${_tmp}" } trap "cleanup" EXIT SIGINT cleanup mkdir -p "${TMP_DIFFROOT}" cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" "${SCRIPT_ROOT}/hack/update-codegen.sh" echo "diffing ${DIFFROOT} against freshly generated codegen" ret=0 diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" if [[ $ret -eq 0 ]] then echo "${DIFFROOT} up to date." else echo "${DIFFROOT} is out of date. Please run hack/update-codegen.sh" exit 1 fi ================================================ FILE: ingress-operator/pkg/apis/modelzetes/register.go ================================================ package modelzetes const ( GroupName = "tensorchord.ai" ) ================================================ FILE: ingress-operator/pkg/apis/modelzetes/v1/doc.go ================================================ // +k8s:deepcopy-gen=package,register // Package v1 is the OpenFaaS v1 version of the API. // +groupName=tensorchord.ai package v1 ================================================ FILE: ingress-operator/pkg/apis/modelzetes/v1/register.go ================================================ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" controller "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes" ) // SchemeGroupVersion is group version used to register these objects var SchemeGroupVersion = schema.GroupVersion{Group: controller.GroupName, Version: "v1"} // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder AddToScheme = localSchemeBuilder.AddToScheme ) func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. localSchemeBuilder.Register(addKnownTypes) } // Adds the list of known types to api.Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &InferenceIngress{}, &InferenceIngressList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ================================================ FILE: ingress-operator/pkg/apis/modelzetes/v1/types.go ================================================ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +genclient:noStatus // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:printcolumn:name="Domain",type=string,JSONPath=`.spec.domain` // InferenceIngress describes an OpenFaaS function type InferenceIngress struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec InferenceIngressSpec `json:"spec"` } // InferenceIngressSpec is the spec for a InferenceIngressSpec resource. It must // be created in the same namespace as the gateway, i.e. openfaas. type InferenceIngressSpec struct { // Domain such as "api.example.com" Domain string `json:"domain"` // Function such as "nodeinfo" Function string `json:"function"` Framework string `json:"framework"` // Path such as "/v1/profiles/view/(.*)", or leave empty for default // +optional Path string `json:"path"` // IngressType such as "nginx" // +optional IngressType string `json:"ingressType,omitempty"` // Enable TLS via cert-manager // +optional TLS *InferenceIngressTLS `json:"tls,omitempty"` // BypassGateway, when true creates an Ingress record // directly for the Function name without using the gateway // in the hot path // +optional BypassGateway bool `json:"bypassGateway,omitempty"` } // InferenceIngressSpec TLS options type InferenceIngressTLS struct { // +optional Enabled bool `json:"enabled"` // +optional IssuerRef ObjectReference `json:"issuerRef"` } // UseTLS if TLS is enabled func (f *InferenceIngressSpec) UseTLS() bool { return f.TLS != nil && f.TLS.Enabled } // ObjectReference is a reference to an object with a given name and kind. type ObjectReference struct { Name string `json:"name"` // +optional Kind string `json:"kind,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // InferenceIngress is a list of Function resources type InferenceIngressList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` Items []InferenceIngress `json:"items"` } ================================================ FILE: ingress-operator/pkg/apis/modelzetes/v1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by deepcopy-gen. DO NOT EDIT. package v1 import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InferenceIngress) DeepCopyInto(out *InferenceIngress) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceIngress. func (in *InferenceIngress) DeepCopy() *InferenceIngress { if in == nil { return nil } out := new(InferenceIngress) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *InferenceIngress) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InferenceIngressList) DeepCopyInto(out *InferenceIngressList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]InferenceIngress, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceIngressList. func (in *InferenceIngressList) DeepCopy() *InferenceIngressList { if in == nil { return nil } out := new(InferenceIngressList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *InferenceIngressList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InferenceIngressSpec) DeepCopyInto(out *InferenceIngressSpec) { *out = *in if in.TLS != nil { in, out := &in.TLS, &out.TLS *out = new(InferenceIngressTLS) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceIngressSpec. func (in *InferenceIngressSpec) DeepCopy() *InferenceIngressSpec { if in == nil { return nil } out := new(InferenceIngressSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InferenceIngressTLS) DeepCopyInto(out *InferenceIngressTLS) { *out = *in out.IssuerRef = in.IssuerRef return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceIngressTLS. func (in *InferenceIngressTLS) DeepCopy() *InferenceIngressTLS { if in == nil { return nil } out := new(InferenceIngressTLS) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectReference) DeepCopyInto(out *ObjectReference) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectReference. func (in *ObjectReference) DeepCopy() *ObjectReference { if in == nil { return nil } out := new(ObjectReference) in.DeepCopyInto(out) return out } ================================================ FILE: ingress-operator/pkg/app/config.go ================================================ package app import ( cli "github.com/urfave/cli/v2" "github.com/tensorchord/openmodelz/ingress-operator/pkg/config" ) func configFromCLI(c *cli.Context) config.Config { cfg := config.Config{} // kubernetes cfg.KubeConfig.Kubeconfig = c.String(flagKubeConfig) cfg.KubeConfig.MasterURL = c.String(flagMasterURL) cfg.KubeConfig.QPS = c.Int(flagQPS) cfg.KubeConfig.Burst = c.Int(flagBurst) cfg.KubeConfig.ResyncPeriod = c.Duration(flagResyncPeriod) // controller cfg.Controller.ThreadCount = c.Int(flagControllerThreads) cfg.Controller.Namespace = c.String(flagNamespace) cfg.Controller.Host = c.String(flagHost) return cfg } ================================================ FILE: ingress-operator/pkg/app/root.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package app import ( "time" "github.com/gin-gonic/gin" "github.com/pkg/errors" "github.com/sirupsen/logrus" cli "github.com/urfave/cli/v2" controller "github.com/tensorchord/openmodelz/ingress-operator/pkg/controller/v1" "github.com/tensorchord/openmodelz/ingress-operator/pkg/signals" "github.com/tensorchord/openmodelz/ingress-operator/pkg/version" ) const ( flagDebug = "debug" // kubernetes flagMasterURL = "master-url" flagKubeConfig = "kube-config" flagQPS = "kube-qps" flagBurst = "kube-burst" flagResyncPeriod = "kube-resync-period" // controller flagControllerThreads = "controller-thread-count" flagNamespace = "namespace" flagHost = "host" ) type App struct { *cli.App } func New() App { internalApp := cli.NewApp() internalApp.EnableBashCompletion = true internalApp.Name = "ingress-operator" internalApp.Usage = "kubernetes operator for inference ingress" internalApp.HideHelpCommand = true internalApp.HideVersion = false internalApp.Version = version.GetVersion().String() internalApp.Flags = []cli.Flag{ &cli.BoolFlag{ Name: flagDebug, Usage: "enable debug output in logs", EnvVars: []string{"DEBUG"}, }, &cli.StringFlag{ Name: flagMasterURL, Usage: "URL to master for kubernetes cluster", EnvVars: []string{"MODELZ_MASTER_URL"}, Aliases: []string{"mu"}, }, &cli.StringFlag{ Name: flagKubeConfig, Usage: "Path to kubeconfig file. If not provided, will use in-cluster config", EnvVars: []string{"MODELZ_KUBE_CONFIG"}, Aliases: []string{"kc"}, }, &cli.IntFlag{ Name: flagQPS, Usage: "QPS for kubernetes client", Value: 100, EnvVars: []string{"MODELZ_KUBE_QPS"}, Aliases: []string{"kq"}, }, &cli.IntFlag{ Name: flagBurst, Value: 250, Usage: "Burst for kubernetes client", EnvVars: []string{"MODELZ_KUBE_BURST"}, Aliases: []string{"kb"}, }, &cli.DurationFlag{ Name: flagResyncPeriod, Value: time.Minute * 5, Usage: "Resync period for kubernetes client", EnvVars: []string{"MODELZ_KUBE_RESYNC_PERIOD"}, Aliases: []string{"kr"}, }, &cli.IntFlag{ Name: flagControllerThreads, Value: 1, Usage: "Number of threads to use for controller", EnvVars: []string{"MODELZ_CONTROLLER_THREAD_COUNT"}, Aliases: []string{"ct"}, }, &cli.StringFlag{ Name: flagNamespace, Value: "default", Usage: "Namespace to create the ingress in. (We need to keep the same namespace as the inference ingress, because kubernetes does not allow cross namespace owner references)", EnvVars: []string{"MODELZ_NAMESPACE"}, Aliases: []string{"ns"}, }, &cli.StringFlag{ Name: flagHost, Value: "apiserver", Usage: "Host to redirect the request to. (apiserver, agent)", EnvVars: []string{"MODELZ_HOST"}, }, } internalApp.Action = runServer // Deal with debug flag. var debugEnabled bool internalApp.Before = func(context *cli.Context) error { debugEnabled = context.Bool(flagDebug) if debugEnabled { logrus.SetReportCaller(true) logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) logrus.SetLevel(logrus.DebugLevel) gin.SetMode(gin.DebugMode) } else { logrus.SetFormatter(&logrus.JSONFormatter{}) } return nil } return App{ App: internalApp, } } func runServer(clicontext *cli.Context) error { c := configFromCLI(clicontext) cfgString, _ := c.GetString() logrus.WithField("config", c).Info("starting ingress operator") if err := c.Validate(); err != nil { if clicontext.Bool(flagDebug) { return errors.Wrap(err, "invalid config: "+cfgString) } else { return errors.Wrap(err, "invalid config") } } // set up signals so we handle the first shutdown signal gracefully stopCh := signals.SetupSignalHandler() s, err := controller.New(c, stopCh) if err != nil { return errors.Wrap(err, "failed to create server") } return s.Run(c.Controller.ThreadCount, stopCh) } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/clientset.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package versioned import ( "fmt" tensorchordv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" ) type Interface interface { Discovery() discovery.DiscoveryInterface TensorchordV1() tensorchordv1.TensorchordV1Interface } // Clientset contains the clients for groups. Each group has exactly one // version included in a Clientset. type Clientset struct { *discovery.DiscoveryClient tensorchordV1 *tensorchordv1.TensorchordV1Client } // TensorchordV1 retrieves the TensorchordV1Client func (c *Clientset) TensorchordV1() tensorchordv1.TensorchordV1Interface { return c.tensorchordV1 } // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { return nil } return c.DiscoveryClient } // NewForConfig creates a new Clientset for the given config. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfig will generate a rate-limiter in configShallowCopy. func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { if configShallowCopy.Burst <= 0 { return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") } configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) } var cs Clientset var err error cs.tensorchordV1, err = tensorchordv1.NewForConfig(&configShallowCopy) if err != nil { return nil, err } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) if err != nil { return nil, err } return &cs, nil } // NewForConfigOrDie creates a new Clientset for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *Clientset { var cs Clientset cs.tensorchordV1 = tensorchordv1.NewForConfigOrDie(c) cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) return &cs } // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset cs.tensorchordV1 = tensorchordv1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/doc.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated clientset. package versioned ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/fake/clientset_generated.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( clientset "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" tensorchordv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1" faketensorchordv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/testing" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) } } cs := &Clientset{tracker: o} cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { gvr := action.GetResource() ns := action.GetNamespace() watch, err := o.Watch(gvr, ns) if err != nil { return false, nil, err } return true, watch, nil }) return cs } // Clientset implements clientset.Interface. Meant to be embedded into a // struct to get a default implementation. This makes faking out just the method // you want to test easier. type Clientset struct { testing.Fake discovery *fakediscovery.FakeDiscovery tracker testing.ObjectTracker } func (c *Clientset) Discovery() discovery.DiscoveryInterface { return c.discovery } func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} ) // TensorchordV1 retrieves the TensorchordV1Client func (c *Clientset) TensorchordV1() tensorchordv1.TensorchordV1Interface { return &faketensorchordv1.FakeTensorchordV1{Fake: &c.Fake} } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/fake/doc.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated fake clientset. package fake ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/fake/register.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( tensorchordv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ tensorchordv1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(scheme)) } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/scheme/doc.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package contains the scheme of the automatically generated clientset. package scheme ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/scheme/register.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package scheme import ( tensorchordv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ tensorchordv1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(Scheme)) } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/doc.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated typed clients. package v1 ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/fake/doc.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // Package fake has the automatically generated clients. package fake ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/fake/fake_inferenceingress.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( "context" modelzetesv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" labels "k8s.io/apimachinery/pkg/labels" schema "k8s.io/apimachinery/pkg/runtime/schema" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" testing "k8s.io/client-go/testing" ) // FakeInferenceIngresses implements InferenceIngressInterface type FakeInferenceIngresses struct { Fake *FakeTensorchordV1 ns string } var inferenceingressesResource = schema.GroupVersionResource{Group: "tensorchord.ai", Version: "v1", Resource: "inferenceingresses"} var inferenceingressesKind = schema.GroupVersionKind{Group: "tensorchord.ai", Version: "v1", Kind: "InferenceIngress"} // Get takes name of the inferenceIngress, and returns the corresponding inferenceIngress object, and an error if there is any. func (c *FakeInferenceIngresses) Get(ctx context.Context, name string, options v1.GetOptions) (result *modelzetesv1.InferenceIngress, err error) { obj, err := c.Fake. Invokes(testing.NewGetAction(inferenceingressesResource, c.ns, name), &modelzetesv1.InferenceIngress{}) if obj == nil { return nil, err } return obj.(*modelzetesv1.InferenceIngress), err } // List takes label and field selectors, and returns the list of InferenceIngresses that match those selectors. func (c *FakeInferenceIngresses) List(ctx context.Context, opts v1.ListOptions) (result *modelzetesv1.InferenceIngressList, err error) { obj, err := c.Fake. Invokes(testing.NewListAction(inferenceingressesResource, inferenceingressesKind, c.ns, opts), &modelzetesv1.InferenceIngressList{}) if obj == nil { return nil, err } label, _, _ := testing.ExtractFromListOptions(opts) if label == nil { label = labels.Everything() } list := &modelzetesv1.InferenceIngressList{ListMeta: obj.(*modelzetesv1.InferenceIngressList).ListMeta} for _, item := range obj.(*modelzetesv1.InferenceIngressList).Items { if label.Matches(labels.Set(item.Labels)) { list.Items = append(list.Items, item) } } return list, err } // Watch returns a watch.Interface that watches the requested inferenceIngresses. func (c *FakeInferenceIngresses) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(inferenceingressesResource, c.ns, opts)) } // Create takes the representation of a inferenceIngress and creates it. Returns the server's representation of the inferenceIngress, and an error, if there is any. func (c *FakeInferenceIngresses) Create(ctx context.Context, inferenceIngress *modelzetesv1.InferenceIngress, opts v1.CreateOptions) (result *modelzetesv1.InferenceIngress, err error) { obj, err := c.Fake. Invokes(testing.NewCreateAction(inferenceingressesResource, c.ns, inferenceIngress), &modelzetesv1.InferenceIngress{}) if obj == nil { return nil, err } return obj.(*modelzetesv1.InferenceIngress), err } // Update takes the representation of a inferenceIngress and updates it. Returns the server's representation of the inferenceIngress, and an error, if there is any. func (c *FakeInferenceIngresses) Update(ctx context.Context, inferenceIngress *modelzetesv1.InferenceIngress, opts v1.UpdateOptions) (result *modelzetesv1.InferenceIngress, err error) { obj, err := c.Fake. Invokes(testing.NewUpdateAction(inferenceingressesResource, c.ns, inferenceIngress), &modelzetesv1.InferenceIngress{}) if obj == nil { return nil, err } return obj.(*modelzetesv1.InferenceIngress), err } // Delete takes name of the inferenceIngress and deletes it. Returns an error if one occurs. func (c *FakeInferenceIngresses) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { _, err := c.Fake. Invokes(testing.NewDeleteAction(inferenceingressesResource, c.ns, name), &modelzetesv1.InferenceIngress{}) return err } // DeleteCollection deletes a collection of objects. func (c *FakeInferenceIngresses) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { action := testing.NewDeleteCollectionAction(inferenceingressesResource, c.ns, listOpts) _, err := c.Fake.Invokes(action, &modelzetesv1.InferenceIngressList{}) return err } // Patch applies the patch and returns the patched inferenceIngress. func (c *FakeInferenceIngresses) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *modelzetesv1.InferenceIngress, err error) { obj, err := c.Fake. Invokes(testing.NewPatchSubresourceAction(inferenceingressesResource, c.ns, name, pt, data, subresources...), &modelzetesv1.InferenceIngress{}) if obj == nil { return nil, err } return obj.(*modelzetesv1.InferenceIngress), err } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/fake/fake_modelzetes_client.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" ) type FakeTensorchordV1 struct { *testing.Fake } func (c *FakeTensorchordV1) InferenceIngresses(namespace string) v1.InferenceIngressInterface { return &FakeInferenceIngresses{c, namespace} } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeTensorchordV1) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/generated_expansion.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package v1 type InferenceIngressExpansion interface{} ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/inferenceingress.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package v1 import ( "context" "time" v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" scheme "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" rest "k8s.io/client-go/rest" ) // InferenceIngressesGetter has a method to return a InferenceIngressInterface. // A group's client should implement this interface. type InferenceIngressesGetter interface { InferenceIngresses(namespace string) InferenceIngressInterface } // InferenceIngressInterface has methods to work with InferenceIngress resources. type InferenceIngressInterface interface { Create(ctx context.Context, inferenceIngress *v1.InferenceIngress, opts metav1.CreateOptions) (*v1.InferenceIngress, error) Update(ctx context.Context, inferenceIngress *v1.InferenceIngress, opts metav1.UpdateOptions) (*v1.InferenceIngress, error) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.InferenceIngress, error) List(ctx context.Context, opts metav1.ListOptions) (*v1.InferenceIngressList, error) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.InferenceIngress, err error) InferenceIngressExpansion } // inferenceIngresses implements InferenceIngressInterface type inferenceIngresses struct { client rest.Interface ns string } // newInferenceIngresses returns a InferenceIngresses func newInferenceIngresses(c *TensorchordV1Client, namespace string) *inferenceIngresses { return &inferenceIngresses{ client: c.RESTClient(), ns: namespace, } } // Get takes name of the inferenceIngress, and returns the corresponding inferenceIngress object, and an error if there is any. func (c *inferenceIngresses) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.InferenceIngress, err error) { result = &v1.InferenceIngress{} err = c.client.Get(). Namespace(c.ns). Resource("inferenceingresses"). Name(name). VersionedParams(&options, scheme.ParameterCodec). Do(ctx). Into(result) return } // List takes label and field selectors, and returns the list of InferenceIngresses that match those selectors. func (c *inferenceIngresses) List(ctx context.Context, opts metav1.ListOptions) (result *v1.InferenceIngressList, err error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } result = &v1.InferenceIngressList{} err = c.client.Get(). Namespace(c.ns). Resource("inferenceingresses"). VersionedParams(&opts, scheme.ParameterCodec). Timeout(timeout). Do(ctx). Into(result) return } // Watch returns a watch.Interface that watches the requested inferenceIngresses. func (c *inferenceIngresses) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } opts.Watch = true return c.client.Get(). Namespace(c.ns). Resource("inferenceingresses"). VersionedParams(&opts, scheme.ParameterCodec). Timeout(timeout). Watch(ctx) } // Create takes the representation of a inferenceIngress and creates it. Returns the server's representation of the inferenceIngress, and an error, if there is any. func (c *inferenceIngresses) Create(ctx context.Context, inferenceIngress *v1.InferenceIngress, opts metav1.CreateOptions) (result *v1.InferenceIngress, err error) { result = &v1.InferenceIngress{} err = c.client.Post(). Namespace(c.ns). Resource("inferenceingresses"). VersionedParams(&opts, scheme.ParameterCodec). Body(inferenceIngress). Do(ctx). Into(result) return } // Update takes the representation of a inferenceIngress and updates it. Returns the server's representation of the inferenceIngress, and an error, if there is any. func (c *inferenceIngresses) Update(ctx context.Context, inferenceIngress *v1.InferenceIngress, opts metav1.UpdateOptions) (result *v1.InferenceIngress, err error) { result = &v1.InferenceIngress{} err = c.client.Put(). Namespace(c.ns). Resource("inferenceingresses"). Name(inferenceIngress.Name). VersionedParams(&opts, scheme.ParameterCodec). Body(inferenceIngress). Do(ctx). Into(result) return } // Delete takes name of the inferenceIngress and deletes it. Returns an error if one occurs. func (c *inferenceIngresses) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { return c.client.Delete(). Namespace(c.ns). Resource("inferenceingresses"). Name(name). Body(&opts). Do(ctx). Error() } // DeleteCollection deletes a collection of objects. func (c *inferenceIngresses) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { var timeout time.Duration if listOpts.TimeoutSeconds != nil { timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second } return c.client.Delete(). Namespace(c.ns). Resource("inferenceingresses"). VersionedParams(&listOpts, scheme.ParameterCodec). Timeout(timeout). Body(&opts). Do(ctx). Error() } // Patch applies the patch and returns the patched inferenceIngress. func (c *inferenceIngresses) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.InferenceIngress, err error) { result = &v1.InferenceIngress{} err = c.client.Patch(pt). Namespace(c.ns). Resource("inferenceingresses"). Name(name). SubResource(subresources...). VersionedParams(&opts, scheme.ParameterCodec). Body(data). Do(ctx). Into(result) return } ================================================ FILE: ingress-operator/pkg/client/clientset/versioned/typed/modelzetes/v1/modelzetes_client.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package v1 import ( v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/scheme" rest "k8s.io/client-go/rest" ) type TensorchordV1Interface interface { RESTClient() rest.Interface InferenceIngressesGetter } // TensorchordV1Client is used to interact with features provided by the tensorchord.ai group. type TensorchordV1Client struct { restClient rest.Interface } func (c *TensorchordV1Client) InferenceIngresses(namespace string) InferenceIngressInterface { return newInferenceIngresses(c, namespace) } // NewForConfig creates a new TensorchordV1Client for the given config. func NewForConfig(c *rest.Config) (*TensorchordV1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } client, err := rest.RESTClientFor(&config) if err != nil { return nil, err } return &TensorchordV1Client{client}, nil } // NewForConfigOrDie creates a new TensorchordV1Client for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *TensorchordV1Client { client, err := NewForConfig(c) if err != nil { panic(err) } return client } // New creates a new TensorchordV1Client for the given RESTClient. func New(c rest.Interface) *TensorchordV1Client { return &TensorchordV1Client{c} } func setConfigDefaults(config *rest.Config) error { gv := v1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } return nil } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *TensorchordV1Client) RESTClient() rest.Interface { if c == nil { return nil } return c.restClient } ================================================ FILE: ingress-operator/pkg/client/informers/externalversions/factory.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( reflect "reflect" sync "sync" time "time" versioned "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" internalinterfaces "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions/internalinterfaces" modelzetes "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions/modelzetes" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory type sharedInformerFactory struct { client versioned.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool } // WithCustomResyncConfig sets a custom resync period for the specified informer types. func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { for k, v := range resyncConfig { factory.customResync[reflect.TypeOf(k)] = v } return factory } } // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.tweakListOptions = tweakListOptions return factory } } // WithNamespace limits the SharedInformerFactory to the specified namespace. func WithNamespace(namespace string) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.namespace = namespace return factory } } // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync) } // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. // Listers obtained via this SharedInformerFactory will be subject to the same filters // as specified here. // Deprecated: Please use NewSharedInformerFactoryWithOptions instead func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) } // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { factory := &sharedInformerFactory{ client: client, namespace: v1.NamespaceAll, defaultResync: defaultResync, informers: make(map[reflect.Type]cache.SharedIndexInformer), startedInformers: make(map[reflect.Type]bool), customResync: make(map[reflect.Type]time.Duration), } // Apply all options for _, opt := range options { factory = opt(factory) } return factory } // Start initializes all requested informers. func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Run(stopCh) f.startedInformers[informerType] = true } } } // WaitForCacheSync waits for all started informers' cache were synced. func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { informers := func() map[reflect.Type]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informers := map[reflect.Type]cache.SharedIndexInformer{} for informerType, informer := range f.informers { if f.startedInformers[informerType] { informers[informerType] = informer } } return informers }() res := map[reflect.Type]bool{} for informType, informer := range informers { res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) } return res } // InternalInformerFor returns the SharedIndexInformer for obj using an internal // client. func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) f.informers[informerType] = informer return informer } // SharedInformerFactory provides shared informers for resources in all known // API group versions. type SharedInformerFactory interface { internalinterfaces.SharedInformerFactory ForResource(resource schema.GroupVersionResource) (GenericInformer, error) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool Tensorchord() modelzetes.Interface } func (f *sharedInformerFactory) Tensorchord() modelzetes.Interface { return modelzetes.New(f, f.namespace, f.tweakListOptions) } ================================================ FILE: ingress-operator/pkg/client/informers/externalversions/generic.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( "fmt" v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // GenericInformer is type of SharedIndexInformer which will locate and delegate to other // sharedInformers based on type type GenericInformer interface { Informer() cache.SharedIndexInformer Lister() cache.GenericLister } type genericInformer struct { informer cache.SharedIndexInformer resource schema.GroupResource } // Informer returns the SharedIndexInformer. func (f *genericInformer) Informer() cache.SharedIndexInformer { return f.informer } // Lister returns the GenericLister. func (f *genericInformer) Lister() cache.GenericLister { return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) } // ForResource gives generic access to a shared informer of the matching type // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=tensorchord.ai, Version=v1 case v1.SchemeGroupVersion.WithResource("inferenceingresses"): return &genericInformer{resource: resource.GroupResource(), informer: f.Tensorchord().V1().InferenceIngresses().Informer()}, nil } return nil, fmt.Errorf("no informer found for %v", resource) } ================================================ FILE: ingress-operator/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package internalinterfaces import ( time "time" versioned "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" cache "k8s.io/client-go/tools/cache" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer // SharedInformerFactory a small interface to allow for adding an informer without an import cycle type SharedInformerFactory interface { Start(stopCh <-chan struct{}) InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer } // TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) ================================================ FILE: ingress-operator/pkg/client/informers/externalversions/modelzetes/interface.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package modelzetes import ( internalinterfaces "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions/internalinterfaces" v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions/modelzetes/v1" ) // Interface provides access to each of this group's versions. type Interface interface { // V1 provides access to shared informers for resources in V1. V1() v1.Interface } type group struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // V1 returns a new v1.Interface. func (g *group) V1() v1.Interface { return v1.New(g.factory, g.namespace, g.tweakListOptions) } ================================================ FILE: ingress-operator/pkg/client/informers/externalversions/modelzetes/v1/inferenceingress.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package v1 import ( "context" time "time" modelzetesv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" versioned "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" internalinterfaces "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions/internalinterfaces" v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/listers/modelzetes/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // InferenceIngressInformer provides access to a shared informer and lister for // InferenceIngresses. type InferenceIngressInformer interface { Informer() cache.SharedIndexInformer Lister() v1.InferenceIngressLister } type inferenceIngressInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewInferenceIngressInformer constructs a new informer for InferenceIngress type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewInferenceIngressInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredInferenceIngressInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredInferenceIngressInformer constructs a new informer for InferenceIngress type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredInferenceIngressInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.TensorchordV1().InferenceIngresses(namespace).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.TensorchordV1().InferenceIngresses(namespace).Watch(context.TODO(), options) }, }, &modelzetesv1.InferenceIngress{}, resyncPeriod, indexers, ) } func (f *inferenceIngressInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredInferenceIngressInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *inferenceIngressInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&modelzetesv1.InferenceIngress{}, f.defaultInformer) } func (f *inferenceIngressInformer) Lister() v1.InferenceIngressLister { return v1.NewInferenceIngressLister(f.Informer().GetIndexer()) } ================================================ FILE: ingress-operator/pkg/client/informers/externalversions/modelzetes/v1/interface.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package v1 import ( internalinterfaces "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. type Interface interface { // InferenceIngresses returns a InferenceIngressInformer. InferenceIngresses() InferenceIngressInformer } type version struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // InferenceIngresses returns a InferenceIngressInformer. func (v *version) InferenceIngresses() InferenceIngressInformer { return &inferenceIngressInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } ================================================ FILE: ingress-operator/pkg/client/listers/modelzetes/v1/expansion_generated.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by lister-gen. DO NOT EDIT. package v1 // InferenceIngressListerExpansion allows custom methods to be added to // InferenceIngressLister. type InferenceIngressListerExpansion interface{} // InferenceIngressNamespaceListerExpansion allows custom methods to be added to // InferenceIngressNamespaceLister. type InferenceIngressNamespaceListerExpansion interface{} ================================================ FILE: ingress-operator/pkg/client/listers/modelzetes/v1/inferenceingress.go ================================================ /* Copyright 2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by lister-gen. DO NOT EDIT. package v1 import ( v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/tools/cache" ) // InferenceIngressLister helps list InferenceIngresses. // All objects returned here must be treated as read-only. type InferenceIngressLister interface { // List lists all InferenceIngresses in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*v1.InferenceIngress, err error) // InferenceIngresses returns an object that can list and get InferenceIngresses. InferenceIngresses(namespace string) InferenceIngressNamespaceLister InferenceIngressListerExpansion } // inferenceIngressLister implements the InferenceIngressLister interface. type inferenceIngressLister struct { indexer cache.Indexer } // NewInferenceIngressLister returns a new InferenceIngressLister. func NewInferenceIngressLister(indexer cache.Indexer) InferenceIngressLister { return &inferenceIngressLister{indexer: indexer} } // List lists all InferenceIngresses in the indexer. func (s *inferenceIngressLister) List(selector labels.Selector) (ret []*v1.InferenceIngress, err error) { err = cache.ListAll(s.indexer, selector, func(m interface{}) { ret = append(ret, m.(*v1.InferenceIngress)) }) return ret, err } // InferenceIngresses returns an object that can list and get InferenceIngresses. func (s *inferenceIngressLister) InferenceIngresses(namespace string) InferenceIngressNamespaceLister { return inferenceIngressNamespaceLister{indexer: s.indexer, namespace: namespace} } // InferenceIngressNamespaceLister helps list and get InferenceIngresses. // All objects returned here must be treated as read-only. type InferenceIngressNamespaceLister interface { // List lists all InferenceIngresses in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*v1.InferenceIngress, err error) // Get retrieves the InferenceIngress from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*v1.InferenceIngress, error) InferenceIngressNamespaceListerExpansion } // inferenceIngressNamespaceLister implements the InferenceIngressNamespaceLister // interface. type inferenceIngressNamespaceLister struct { indexer cache.Indexer namespace string } // List lists all InferenceIngresses in the indexer for a given namespace. func (s inferenceIngressNamespaceLister) List(selector labels.Selector) (ret []*v1.InferenceIngress, err error) { err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { ret = append(ret, m.(*v1.InferenceIngress)) }) return ret, err } // Get retrieves the InferenceIngress from the indexer for a given namespace and name. func (s inferenceIngressNamespaceLister) Get(name string) (*v1.InferenceIngress, error) { obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) if err != nil { return nil, err } if !exists { return nil, errors.NewNotFound(v1.Resource("inferenceingress"), name) } return obj.(*v1.InferenceIngress), nil } ================================================ FILE: ingress-operator/pkg/config/config.go ================================================ package config import ( "encoding/json" "errors" "time" ) type Config struct { KubeConfig KubeConfig `json:"kube_config,omitempty"` Controller ControllerConfig `json:"controller,omitempty"` } type ControllerConfig struct { ThreadCount int `json:"thread_count,omitempty"` Namespace string `json:"namespace,omitempty"` Host string `json:"host,omitempty"` } type KubeConfig struct { Kubeconfig string `json:"kubeconfig,omitempty"` MasterURL string `json:"master_url,omitempty"` QPS int `json:"qps,omitempty"` Burst int `json:"burst,omitempty"` ResyncPeriod time.Duration `json:"resync_period,omitempty"` } func New() Config { return Config{} } func (c Config) GetString() (string, error) { bytes, err := json.Marshal(c) return string(bytes), err } func (c Config) Validate() error { if c.KubeConfig.QPS == 0 || c.KubeConfig.Burst == 0 || c.KubeConfig.ResyncPeriod == 0 { return errors.New("invalid kubeconfig") } if c.Controller.ThreadCount == 0 || c.Controller.Namespace == "" || c.Controller.Host == "" { return errors.New("invalid controller config") } return nil } ================================================ FILE: ingress-operator/pkg/consts/consts.go ================================================ package consts const ( KeyCert = "cert" EnvironmentPrefix = "MODELZ" ) ================================================ FILE: ingress-operator/pkg/controller/core.go ================================================ package controller import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" klog "k8s.io/klog" faasv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/scheme" faasscheme "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned/scheme" v1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions/modelzetes/v1" listers "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/listers/modelzetes/v1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) const AgentName = "ingress-operator" const FaasIngressKind = "InferenceIngress" const OpenfaasWorkloadPort = 8080 const ( // SuccessSynced is used as part of the Event 'reason' when a Function is synced SuccessSynced = "Synced" // ErrResourceExists is used as part of the Event 'reason' when a Function fails // to sync due to a Deployment of the same name already existing. ErrResourceExists = "ErrResourceExists" // MessageResourceExists is the message used for Events when a resource // fails to sync due to a Deployment already existing MessageResourceExists = "Resource %q already exists and is not managed by controller" // MessageResourceSynced is the message used for an Event fired when a Function // is synced successfully MessageResourceSynced = "FunctionIngress synced successfully" ) // BaseController is the controller contains the common function ingress // implementation that is shared between the various versions of k8s. type BaseController struct { FunctionsLister listers.InferenceIngressLister FunctionsSynced cache.InformerSynced // Workqueue is a rate limited work queue. This is used to queue work to be // processed instead of performing it as soon as a change happens. This // means we can ensure we only process a fixed amount of resources at a // time, and makes it easy to ensure we are never processing the same item // simultaneously in two different workers. Workqueue workqueue.RateLimitingInterface SyncHandler func(ctx context.Context, key string) error } func (c BaseController) Run(threadiness int, stopCh <-chan struct{}) error { ctx, cancel := context.WithCancel(context.Background()) defer runtime.HandleCrash() defer c.Workqueue.ShutDown() defer cancel() // Start the informer factories to begin populating the informer caches // Wait for the caches to be synced before starting workers klog.Info("Waiting for informer caches to sync") if ok := cache.WaitForCacheSync(stopCh, c.FunctionsSynced); !ok { return fmt.Errorf("failed to wait for caches to sync") } klog.Info("Starting workers") // Launch two workers to process Function resources for i := 0; i < threadiness; i++ { go wait.Until(c.runWorker(ctx), time.Second, stopCh) } klog.Info("Started workers") <-stopCh klog.Info("Shutting down workers") return nil } // runWorker is a long-running function that will continually call the // processNextWorkItem function in order to read and process a message on the workqueue. func (c BaseController) runWorker(ctx context.Context) func() { return func() { for c.processNextWorkItem(ctx) { } } } // processNextWorkItem will read a single work item off the workqueue and // attempt to process it, by calling the syncHandler. func (c BaseController) processNextWorkItem(ctx context.Context) bool { obj, shutdown := c.Workqueue.Get() if shutdown { return false } err := func(obj interface{}) error { defer c.Workqueue.Done(obj) var key string var ok bool if key, ok = obj.(string); !ok { c.Workqueue.Forget(obj) runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) return nil } if err := c.SyncHandler(ctx, key); err != nil { return fmt.Errorf("error syncing '%s': %s", key, err.Error()) } c.Workqueue.Forget(obj) return nil }(obj) if err != nil { runtime.HandleError(err) return true } return true } // enqueueFunction takes a fni resource and converts it into a namespace/name // string which is then put onto the work queue. This method should *not* be // passed resources of any type other than fni. func (c *BaseController) EnqueueFunction(obj interface{}) { var key string var err error if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { runtime.HandleError(err) return } c.Workqueue.AddRateLimited(key) } // handleObject will take any resource implementing metav1.Object and attempt // to find the fni resource that 'owns' it. It does this by looking at the // objects metadata.ownerReferences field for an appropriate OwnerReference. // It then enqueues that fni resource to be processed. If the object does not // have an appropriate OwnerReference, it will simply be skipped. func (c BaseController) HandleObject(obj interface{}) { var object metav1.Object var ok bool if object, ok = obj.(metav1.Object); !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { runtime.HandleError(fmt.Errorf("error decoding object, invalid type")) return } object, ok = tombstone.Obj.(metav1.Object) if !ok { runtime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) return } klog.V(4).Infof("Recovered deleted object '%s' from tombstone", object.GetName()) } klog.V(4).Infof("Processing object: %s", object.GetName()) if ownerRef := metav1.GetControllerOf(object); ownerRef != nil { // If this object is not owned by a fni, we should not do anything more // with it. if ownerRef.Kind != FaasIngressKind { return } fni, err := c.FunctionsLister.InferenceIngresses(object.GetNamespace()).Get(ownerRef.Name) if err != nil { klog.Infof("FunctionIngress '%s' deleted. Ignoring orphaned object '%s': %v", ownerRef.Name, object.GetSelfLink(), err) return } c.EnqueueFunction(fni) return } } func (c BaseController) SetupEventHandlers( functionIngress v1.InferenceIngressInformer, kubeInformerFactory kubeinformers.SharedInformerFactory, ) { functionIngress.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: c.EnqueueFunction, UpdateFunc: func(old, new interface{}) { oldFn, ok := CheckCustomResourceType(old) if !ok { return } newFn, ok := CheckCustomResourceType(new) if !ok { return } diffSpec := cmp.Diff(oldFn.Spec, newFn.Spec) diffAnnotations := cmp.Diff(oldFn.ObjectMeta.Annotations, newFn.ObjectMeta.Annotations) if diffSpec != "" || diffAnnotations != "" { c.EnqueueFunction(new) } }, }) // Set up an event handler for when functions related resources like pods, deployments, replica sets // can't be materialized. This logs abnormal events like ImagePullBackOff, back-off restarting failed container, // failed to start container, oci runtime errors, etc // Enable this with -v=3 kubeInformerFactory.Core().V1().Events().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { key, err := cache.MetaNamespaceKeyFunc(obj) if err == nil { event := obj.(*corev1.Event) since := time.Since(event.LastTimestamp.Time) // log abnormal events occurred in the last minute if since.Seconds() < 61 && strings.Contains(event.Type, "Warning") { klog.V(3).Infof("Abnormal event detected on %s %s: %s", event.LastTimestamp, key, event.Message) } } }, }) } func GetClass(ingressType string) string { switch ingressType { case "": case "nginx": return "nginx" default: return ingressType } return "nginx" } func GetIssuerKind(issuerType string) string { switch issuerType { case "ClusterIssuer": return "cert-manager.io/cluster-issuer" default: return "cert-manager.io/issuer" } } func MakeAnnotations(fni *faasv1.InferenceIngress, host string) map[string]string { controlPlane, exist := fni.Annotations[consts.AnnotationControlPlaneKey] class := GetClass(fni.Spec.IngressType) specJSON, _ := json.Marshal(fni) annotations := make(map[string]string) annotations["ai.tensorchord.spec"] = string(specJSON) inferenceNamespace := fni.Labels[consts.LabelInferenceNamespace] if !fni.Spec.BypassGateway { switch class { case "nginx": switch host { // TODO: make this configurable case "apiserver": annotations["nginx.ingress.kubernetes.io/rewrite-target"] = "/api/v1/" + fni.Spec.Framework + "/" + fni.Spec.Function + "/$1" annotations["nginx.ingress.kubernetes.io/use-regex"] = "true" default: // for inference created by modelz apiserver if exist && controlPlane == consts.ModelzAnnotationValue { annotations["nginx.ingress.kubernetes.io/rewrite-target"] = "/api/v1/" + fni.Spec.Framework + "/" + fni.Spec.Function + "/$1" annotations["nginx.ingress.kubernetes.io/use-regex"] = "true" annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" } else { annotations["nginx.ingress.kubernetes.io/rewrite-target"] = "/inference/" + fni.Name + "." + inferenceNamespace + "/$1" annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" annotations["nginx.ingress.kubernetes.io/use-regex"] = "true" } } } } annotations["nginx.ingress.kubernetes.io/proxy-send-timeout"] = "300" annotations["nginx.ingress.kubernetes.io/proxy-read-timeout"] = "300" annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "16m" // We use the default certificate for now. // if fni.Spec.UseTLS() { // issuerType := GetIssuerKind(fni.Spec.TLS.IssuerRef.Kind) // annotations[issuerType] = fni.Spec.TLS.IssuerRef.Name // } // Set annotations with overrides from FunctionIngress // annotations for k, v := range fni.ObjectMeta.Annotations { annotations[k] = v } return annotations } func MakeOwnerRef(fni *faasv1.InferenceIngress) []metav1.OwnerReference { ref := []metav1.OwnerReference{ *metav1.NewControllerRef(fni, schema.GroupVersionKind{ Group: faasv1.SchemeGroupVersion.Group, Version: faasv1.SchemeGroupVersion.Version, Kind: FaasIngressKind, }), } return ref } func CheckCustomResourceType(obj interface{}) (faasv1.InferenceIngress, bool) { var fn *faasv1.InferenceIngress var ok bool if fn, ok = obj.(*faasv1.InferenceIngress); !ok { klog.Errorf("Event Watch received an invalid object: %#v", obj) return faasv1.InferenceIngress{}, false } return *fn, true } func IngressNeedsUpdate(old, fni *faasv1.InferenceIngress) bool { return !cmp.Equal(old.Spec, fni.Spec) || !cmp.Equal(old.ObjectMeta.Annotations, fni.ObjectMeta.Annotations) } func EventRecorder(client kubernetes.Interface) record.EventRecorder { // Create event broadcaster // Add o6s types to the default Kubernetes Scheme so Events can be // logged for faas-controller types. faasscheme.AddToScheme(scheme.Scheme) klog.V(4).Info("Creating event broadcaster") eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(klog.V(4).Infof) eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: client.CoreV1().Events("")}) return eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: AgentName}) } ================================================ FILE: ingress-operator/pkg/controller/core_test.go ================================================ package controller import ( "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" faasv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" ) func TestMakeAnnotations(t *testing.T) { cases := []struct { name string ingress faasv1.InferenceIngress expected map[string]string excluded []string }{ { name: "can override ingress class value", ingress: faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ "kubernetes.io/ingress.class": "awesome-nginx", }, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "awesome-nginx", }, }, expected: map[string]string{ "kubernetes.io/ingress.class": "awesome-nginx", }, }, { name: "bypass removes rewrite target", ingress: faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ "kubernetes.io/ingress.class": "nginx", }, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "nginx", Function: "nodeinfo", BypassGateway: true, Domain: "nodeinfo.example.com", }, }, excluded: []string{"nginx.ingress.kubernetes.io/rewrite-target"}, }, { name: "default annotations includes a rewrite-target", ingress: faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "nginx", }, }, expected: map[string]string{ "nginx.ingress.kubernetes.io/rewrite-target": "/api/v1///$1", }, }, { name: "creates required nginx annotations", ingress: faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ "kubernetes.io/ingress.class": "nginx", }, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "nginx", Framework: "mosec", Function: "main", }, }, expected: map[string]string{ "nginx.ingress.kubernetes.io/rewrite-target": "/api/v1/mosec/main/$1", }, }, { name: "creates required skipper annotations", ingress: faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ "kubernetes.io/ingress.class": "skipper", "zalando.org/skipper-filter": `setPath("/function/nodeinfo")`, }, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "skipper", Function: "nodeinfo", BypassGateway: false, Domain: "nodeinfo.example.com", }, }, expected: map[string]string{ "kubernetes.io/ingress.class": "skipper", "zalando.org/skipper-filter": `setPath("/function/nodeinfo")`, }, }, // { // name: "creates tls issuer annotation", // ingress: faasv1.InferenceIngress{ // ObjectMeta: metav1.ObjectMeta{ // Annotations: map[string]string{ // "kubernetes.io/ingress.class": "nginx", // }, // }, // Spec: faasv1.InferenceIngressSpec{ // IngressType: "nginx", // Function: "nodeinfo", // BypassGateway: false, // Domain: "nodeinfo.example.com", // TLS: &faasv1.InferenceIngressTLS{ // IssuerRef: faasv1.ObjectReference{ // Name: "clusterFoo", // Kind: "ClusterIssuer", // }, // Enabled: true, // }, // }, // }, // expected: map[string]string{ // "cert-manager.io/cluster-issuer": "clusterFoo", // }, // }, // { // name: "default tls issuer is local", // ingress: faasv1.InferenceIngress{ // ObjectMeta: metav1.ObjectMeta{ // Annotations: map[string]string{ // "kubernetes.io/ingress.class": "nginx", // }, // }, // Spec: faasv1.InferenceIngressSpec{ // IngressType: "nginx", // Function: "nodeinfo", // BypassGateway: false, // Domain: "nodeinfo.example.com", // TLS: &faasv1.InferenceIngressTLS{ // IssuerRef: faasv1.ObjectReference{ // Name: "clusterFoo", // }, // Enabled: true, // }, // }, // }, // expected: map[string]string{ // "cert-manager.io/issuer": "clusterFoo", // }, // }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { result := MakeAnnotations(&tc.ingress, "apiserver") for key, value := range tc.expected { found, ok := result[key] if !ok { t.Fatalf("Failed to find expected annotation: %q", key) } if found != value { t.Fatalf("expected annotation value %q, got %q", value, found) } } for _, key := range tc.excluded { value, ok := result[key] if ok { t.Fatalf("annotations should not include %q, but it was found with value %q", key, value) } } }) } } ================================================ FILE: ingress-operator/pkg/controller/v1/controller.go ================================================ package v1 import ( "context" "encoding/json" "fmt" "strings" pkgerrors "github.com/pkg/errors" "github.com/sirupsen/logrus" faasv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" clientset "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" informers "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions" listers "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/listers/modelzetes/v1" "github.com/tensorchord/openmodelz/ingress-operator/pkg/config" "github.com/tensorchord/openmodelz/ingress-operator/pkg/controller" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/runtime" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" networkingv1 "k8s.io/client-go/listers/networking/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" ) // SyncHandler is the controller implementation for Function resources type SyncHandler struct { config config.Config // kubeclientset is a standard kubernetes clientset kubeclientset kubernetes.Interface functionsLister listers.InferenceIngressLister ingressLister networkingv1.IngressLister // recorder is an event recorder for recording Event resources to the // Kubernetes API. recorder record.EventRecorder } // NewController returns a new OpenFaaS controller func NewController( cfg config.Config, kubeclientset kubernetes.Interface, faasclientset clientset.Interface, kubeInformerFactory kubeinformers.SharedInformerFactory, functionIngressFactory informers.SharedInformerFactory, ) controller.BaseController { recorder := controller.EventRecorder(kubeclientset) functionIngress := functionIngressFactory.Tensorchord().V1().InferenceIngresses() ingressInformer := kubeInformerFactory.Networking().V1().Ingresses() ingressLister := ingressInformer.Lister() syncer := SyncHandler{ config: cfg, kubeclientset: kubeclientset, functionsLister: functionIngress.Lister(), ingressLister: ingressLister, recorder: recorder, } ctrl := controller.BaseController{ FunctionsLister: functionIngress.Lister(), FunctionsSynced: functionIngress.Informer().HasSynced, Workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "FunctionIngresses"), SyncHandler: syncer.handler, } logrus.Info("Setting up event handlers") ctrl.SetupEventHandlers(functionIngress, kubeInformerFactory) ingressInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ DeleteFunc: ctrl.HandleObject, }) return ctrl } // handler compares the actual state with the desired, and attempts to // converge the two. It then updates the Status block of the fni resource // with the current status of the resource. func (h SyncHandler) handler(ctx context.Context, key string) error { // Convert the namespace/name string into a distinct namespace and name namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { runtime.HandleError(fmt.Errorf("invalid resource key: %s", key)) return nil } // Get the fni resource with this namespace/name fni, err := h.functionsLister.InferenceIngresses(namespace).Get(name) if err != nil { // The fni resource may no longer exist, in which case we stop processing. if errors.IsNotFound(err) { runtime.HandleError(fmt.Errorf("function ingress '%s' in work queue no longer exists", key)) return nil } return err } logger := logrus.WithFields(logrus.Fields{ "inference": fni.Name, "namespace": fni.Namespace, }) ingresses := h.ingressLister.Ingresses(namespace) ingress, getIngressErr := ingresses.Get(fni.Name) createIngress := errors.IsNotFound(getIngressErr) if !createIngress && ingress == nil { logrus.Errorf("cannot get ingress: %s in %s, error: %s", fni.Name, namespace, getIngressErr.Error()) } logger.Debugf("createIngress: %v", createIngress) if createIngress { host := h.config.Controller.Host rules := makeRules(fni, host) tls := makeTLS(fni) ns := h.config.Controller.Namespace newIngress := netv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns, Annotations: controller.MakeAnnotations(fni, host), OwnerReferences: controller.MakeOwnerRef(fni), }, Spec: netv1.IngressSpec{ Rules: rules, IngressClassName: &fni.Spec.IngressType, TLS: tls, }, } _, createErr := h.kubeclientset.NetworkingV1().Ingresses(ns).Create(ctx, &newIngress, metav1.CreateOptions{}) if createErr != nil { logger.Errorf("cannot create ingress: %v in %v, error: %v", name, namespace, createErr.Error()) } h.recorder.Event(fni, corev1.EventTypeNormal, controller.SuccessSynced, controller.MessageResourceSynced) return nil } old := faasv1.InferenceIngress{} if val, ok := ingress.Annotations["ai.tensorchord.spec"]; ok && len(val) > 0 { unmarshalErr := json.Unmarshal([]byte(val), &old) if unmarshalErr != nil { return pkgerrors.Wrap(unmarshalErr, "unable to unmarshal from field inference") } } // Update the Deployment resource if the fni definition differs if controller.IngressNeedsUpdate(&old, fni) { logger.Debugf("updating ingress: %s in %s", fni.Name, namespace) if old.ObjectMeta.Name != fni.ObjectMeta.Name { return fmt.Errorf("cannot rename object") } updated := ingress.DeepCopy() rules := makeRules(fni, h.config.Controller.Host) annotations := controller.MakeAnnotations(fni, h.config.Controller.Host) for k, v := range annotations { updated.Annotations[k] = v } updated.Spec.Rules = rules updated.Spec.TLS = makeTLS(fni) _, updateErr := h.kubeclientset.NetworkingV1().Ingresses(namespace).Update(ctx, updated, metav1.UpdateOptions{}) if updateErr != nil { logrus.Errorf("error updating ingress: %v", updateErr) return updateErr } } // If an error occurs during Get/Create, we'll requeue the item so we can // attempt processing again later. This could have been caused by a // temporary network failure, or any other transient reason. if err != nil { return fmt.Errorf("transient error: %v", err) } h.recorder.Event(fni, corev1.EventTypeNormal, controller.SuccessSynced, controller.MessageResourceSynced) return nil } func makeRules(fni *faasv1.InferenceIngress, host string) []netv1.IngressRule { path := "/(.*)" if fni.Spec.BypassGateway { path = "/" } if len(fni.Spec.Path) > 0 { path = fni.Spec.Path } if controller.GetClass(fni.Spec.IngressType) == "traefik" { // We have to trim the regex and the trailing slash for Traefik, // otherwise routing won't work path = strings.TrimRight(path, "/(.*)") if len(path) == 0 { path = "/" } } pathType := netv1.PathTypeImplementationSpecific return []netv1.IngressRule{ { Host: fni.Spec.Domain, IngressRuleValue: netv1.IngressRuleValue{ HTTP: &netv1.HTTPIngressRuleValue{ Paths: []netv1.HTTPIngressPath{ { Path: path, PathType: &pathType, Backend: netv1.IngressBackend{ Service: &netv1.IngressServiceBackend{ Name: host, Port: netv1.ServiceBackendPort{ Number: controller.OpenfaasWorkloadPort, }, }, }, }, }, }, }, }, } } func makeTLS(fni *faasv1.InferenceIngress) []netv1.IngressTLS { if !fni.Spec.UseTLS() { return []netv1.IngressTLS{} } return []netv1.IngressTLS{ { // Use default secret name, thus no need to specify SecretName. Hosts: []string{ fni.Spec.Domain, }, }, } } ================================================ FILE: ingress-operator/pkg/controller/v1/controller_factory.go ================================================ package v1 import ( "errors" "fmt" "strings" "github.com/sirupsen/logrus" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" clientset "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/clientset/versioned" informers "github.com/tensorchord/openmodelz/ingress-operator/pkg/client/informers/externalversions" "github.com/tensorchord/openmodelz/ingress-operator/pkg/config" "github.com/tensorchord/openmodelz/ingress-operator/pkg/controller" ) func New(c config.Config, stopCh <-chan struct{}) (*controller.BaseController, error) { clientCmdConfig, err := clientcmd.BuildConfigFromFlags( c.KubeConfig.MasterURL, c.KubeConfig.Kubeconfig) if err != nil { return nil, fmt.Errorf("error building kubeconfig: %s", err.Error()) } clientCmdConfig.QPS = float32(c.KubeConfig.QPS) clientCmdConfig.Burst = c.KubeConfig.Burst kubeClient, err := kubernetes.NewForConfig(clientCmdConfig) if err != nil { return nil, fmt.Errorf("error building Kubernetes clientset: %s", err.Error()) } ingressClient, err := clientset.NewForConfig(clientCmdConfig) if err != nil { return nil, fmt.Errorf("error building Inference clientset: %s", err.Error()) } kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, c.KubeConfig.ResyncPeriod) ingressInformerFactory := informers.NewSharedInformerFactoryWithOptions(ingressClient, c.KubeConfig.ResyncPeriod) capabilities, err := getPreferredAvailableAPIs(kubeClient, "Ingress") if err != nil { return nil, fmt.Errorf("error retrieving Kubernetes cluster capabilities: %s", err.Error()) } logrus.Infof("cluster supports ingress in: %s", capabilities) if !capabilities.Has("networking.k8s.io/v1") { return nil, errors.New("networking.k8s.io/v1 is not available") } inferenceIngresses := ingressInformerFactory.Tensorchord().V1().InferenceIngresses() go inferenceIngresses.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:inferenceingresses", "tensorchord"), stopCh, inferenceIngresses.Informer().HasSynced); !ok { return nil, errors.New("failed to wait for inferenceingresses caches to sync") } ingresses := kubeInformerFactory.Networking().V1().Ingresses() go ingresses.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:ingresses", "networking"), stopCh, ingresses.Informer().HasSynced); !ok { return nil, errors.New("failed to wait for ingresses caches to sync") } ctr := NewController(c, kubeClient, ingressClient, kubeInformerFactory, ingressInformerFactory) return &ctr, nil } // getPreferredAvailableAPIs queries the cluster for the preferred resources information and returns a Capabilities // instance containing those api groups that support the specified kind. // // kind should be the title case singular name of the kind. For example, "Ingress" is the kind for a resource "ingress". func getPreferredAvailableAPIs(client kubernetes.Interface, kind string) (Capabilities, error) { discoveryclient := client.Discovery() lists, err := discoveryclient.ServerPreferredResources() if err != nil { return nil, err } caps := Capabilities{} for _, list := range lists { if len(list.APIResources) == 0 { continue } for _, resource := range list.APIResources { if len(resource.Verbs) == 0 { continue } if resource.Kind == kind { caps[list.GroupVersion] = true } } } return caps, nil } type Capabilities map[string]bool func (c Capabilities) Has(wanted string) bool { return c[wanted] } func (c Capabilities) String() string { keys := make([]string, 0, len(c)) for k := range c { keys = append(keys, k) } return strings.Join(keys, ", ") } ================================================ FILE: ingress-operator/pkg/controller/v1/controller_test.go ================================================ package v1 import ( "reflect" "testing" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" faasv1 "github.com/tensorchord/openmodelz/ingress-operator/pkg/apis/modelzetes/v1" "github.com/tensorchord/openmodelz/ingress-operator/pkg/controller" ) func Test_makeRules_Nginx_RootPath_HasRegex(t *testing.T) { ingress := faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "nginx", }, } rules := makeRules(&ingress, "apiserver") if len(rules) == 0 { t.Errorf("Ingress should give at least one rule") t.Fail() } wantPath := "/(.*)" gotPath := rules[0].HTTP.Paths[0].Path if gotPath != wantPath { t.Errorf("want path %s, but got %s", wantPath, gotPath) } gotPort := rules[0].HTTP.Paths[0].Backend.Service.Port.Number if gotPort != controller.OpenfaasWorkloadPort { t.Errorf("want port %d, but got %d", controller.OpenfaasWorkloadPort, gotPort) } } func Test_makeRules_Nginx_RootPath_IsRootWithBypassMode(t *testing.T) { wantFunction := "apiserver" ingress := faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, Spec: faasv1.InferenceIngressSpec{ BypassGateway: true, IngressType: "nginx", Function: "nodeinfo", // Path: "/", }, } rules := makeRules(&ingress, "apiserver") if len(rules) == 0 { t.Errorf("Ingress should give at least one rule") t.Fail() } wantPath := "/" gotPath := rules[0].HTTP.Paths[0].Path if gotPath != wantPath { t.Errorf("want path %s, but got %s", wantPath, gotPath) } gotHost := rules[0].HTTP.Paths[0].Backend.Service.Name if gotHost != wantFunction { t.Errorf("want host to be function: %s, but got %s", wantFunction, gotHost) } } func Test_makeRules_Nginx_PathOverride(t *testing.T) { ingress := faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "nginx", Path: "/v1/profiles/view/(.*)", }, } rules := makeRules(&ingress, "apiserver") if len(rules) == 0 { t.Errorf("Ingress should give at least one rule") t.Fail() } wantPath := ingress.Spec.Path gotPath := rules[0].HTTP.Paths[0].Path if gotPath != wantPath { t.Errorf("want path %s, but got %s", wantPath, gotPath) } } func Test_makeRules_Traefik_RootPath_TrimsRegex(t *testing.T) { ingress := faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "traefik", }, } rules := makeRules(&ingress, "apiserver") if len(rules) == 0 { t.Errorf("Ingress should give at least one rule") t.Fail() } wantPath := "/" gotPath := rules[0].HTTP.Paths[0].Path if gotPath != wantPath { t.Errorf("want path %s, but got %s", wantPath, gotPath) } } func Test_makeRules_Traefik_NestedPath_TrimsRegex_And_TrailingSlash(t *testing.T) { ingress := faasv1.InferenceIngress{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, Spec: faasv1.InferenceIngressSpec{ IngressType: "traefik", Path: "/v1/profiles/view/(.*)", }, } rules := makeRules(&ingress, "apiserver") if len(rules) == 0 { t.Errorf("Ingress should give at least one rule") t.Fail() } wantPath := "/v1/profiles/view" gotPath := rules[0].HTTP.Paths[0].Path if gotPath != wantPath { t.Errorf("want path %s, but got %s", wantPath, gotPath) } } func Test_makeTLS(t *testing.T) { cases := []struct { name string fni *faasv1.InferenceIngress expected []netv1.IngressTLS }{ { name: "tls disabled results in empty tls config", fni: &faasv1.InferenceIngress{ Spec: faasv1.InferenceIngressSpec{ TLS: &faasv1.InferenceIngressTLS{ Enabled: false, }, }, }, expected: []netv1.IngressTLS{}, }, { name: "tls enabled creates TLS object with correct host and secret with matching the host", fni: &faasv1.InferenceIngress{ Spec: faasv1.InferenceIngressSpec{ Domain: "foo.example.com", TLS: &faasv1.InferenceIngressTLS{ Enabled: true, IssuerRef: faasv1.ObjectReference{ Name: "test-issuer", Kind: "ClusterIssuer", }, }, }, }, expected: []netv1.IngressTLS{ { Hosts: []string{ "foo.example.com", }, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := makeTLS(tc.fni) if !reflect.DeepEqual(tc.expected, got) { t.Fatalf("want tls config %v, got %v", tc.expected, got) } }) } } ================================================ FILE: ingress-operator/pkg/controller/v1/docs.go ================================================ package v1 /* v1 package provides the original ingress controller implementation. This provides support for ingress operator on k8s >= 1.19 networking/v1 api group */ ================================================ FILE: ingress-operator/pkg/signals/signal.go ================================================ package signals import ( "os" "os/signal" ) var onlyOneSignalHandler = make(chan struct{}) // SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned // which is closed on one of these signals. If a second signal is caught, the program // is terminated with exit code 1. func SetupSignalHandler() (stopCh <-chan struct{}) { close(onlyOneSignalHandler) // panics when called twice stop := make(chan struct{}) c := make(chan os.Signal, 2) signal.Notify(c, shutdownSignals...) go func() { <-c close(stop) <-c os.Exit(1) // second signal. Exit directly. }() return stop } ================================================ FILE: ingress-operator/pkg/signals/signal_posix.go ================================================ //go:build !windows // +build !windows package signals import ( "os" "syscall" ) var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} ================================================ FILE: ingress-operator/pkg/signals/signal_windows.go ================================================ package signals import ( "os" ) var shutdownSignals = []os.Signal{os.Interrupt} ================================================ FILE: ingress-operator/pkg/version/version.go ================================================ /* Copyright The TensorChord Inc. Copyright The BuildKit Authors. Copyright The containerd 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 version import ( "fmt" "regexp" "runtime" "strings" "sync" ) var ( // Package is filled at linking time Package = "github.com/tensorchord/openmodelz/modelzetes" // Revision is filled with the VCS (e.g. git) revision being used to build // the program at linking time. Revision = "" version = "0.0.0+unknown" buildDate = "1970-01-01T00:00:00Z" // output from `date -u +'%Y-%m-%dT%H:%M:%SZ'` gitCommit = "" // output from `git rev-parse HEAD` gitTag = "" // output from `git describe --exact-match --tags HEAD` (if clean tree state) gitTreeState = "" // determined from `git status --porcelain`. either 'clean' or 'dirty' developmentFlag = "false" ) // Version contains envd version information type Version struct { Version string BuildDate string GitCommit string GitTag string GitTreeState string GoVersion string Compiler string Platform string } func (v Version) String() string { return v.Version } // SetGitTagForE2ETest sets the gitTag for test purpose. func SetGitTagForE2ETest(tag string) { gitTag = tag } // GetEnvdVersion gets Envd version information func GetEnvdVersion() string { var versionStr string if gitCommit != "" && gitTag != "" && gitTreeState == "clean" && developmentFlag == "false" { // if we have a clean tree state and the current commit is tagged, // this is an official release. versionStr = gitTag } else { // otherwise formulate a version string based on as much metadata // information we have available. if strings.HasPrefix(version, "v") { versionStr = version } else { versionStr = "v" + version } if len(gitCommit) >= 7 { versionStr += "+" + gitCommit[0:7] if gitTreeState != "clean" { versionStr += ".dirty" } } else { versionStr += "+unknown" } } return versionStr } // GetVersion returns the version information func GetVersion() Version { return Version{ Version: GetEnvdVersion(), BuildDate: buildDate, GitCommit: gitCommit, GitTag: gitTag, GitTreeState: gitTreeState, GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } } var ( reRelease *regexp.Regexp reDev *regexp.Regexp reOnce sync.Once ) func UserAgent() string { version := GetVersion().String() reOnce.Do(func() { reRelease = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+$`) reDev = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+`) }) if matches := reRelease.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] } else if matches := reDev.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] + "-dev" } return "envd/" + version } ================================================ FILE: ingress-operator/vendor.go ================================================ //go:build vendor package main // This file exists to trick "go mod vendor" to include "main" packages. // It is not expected to build, the build tag above is only to prevent this // file from being included in builds. import ( _ "k8s.io/code-generator/cmd/client-gen" _ "k8s.io/code-generator/cmd/deepcopy-gen" _ "k8s.io/code-generator/cmd/defaulter-gen" _ "k8s.io/code-generator/cmd/informer-gen" _ "k8s.io/code-generator/cmd/lister-gen" _ "k8s.io/code-generator/cmd/openapi-gen" ) func main() {} ================================================ FILE: mdz/.gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ .idea bin/ debug-bin/ **/password.txt **/gateway-password.txt .vscode of_kind_portforward.pid /kind* /kubectl /yaml_armhf /yaml_arm64 /broker-* /chart/pro-builder/out /chart/pro-builder/payload.txt /pgconnector.yaml jwt_key jwt_key.pub /*.pid .tools/ ================================================ FILE: mdz/Makefile ================================================ # Copyright 2022 TensorChord Inc. # # The old school Makefile, following are required targets. The Makefile is written # to allow building multiple binaries. You are free to add more targets or change # existing implementations, as long as the semantics are preserved. # # make - default to 'build' target # make lint - code analysis # make test - run unit test (or plus integration test) # make build - alias to build-local target # make build-local - build local binary targets # make build-linux - build linux binary targets # make container - build containers # $ docker login registry -u username -p xxxxx # make push - push containers # make clean - clean up targets # # Not included but recommended targets: # make e2e-test # # The makefile is also responsible to populate project version information. # # # Tweak the variables based on your project. # # This repo's root import path (under GOPATH). ROOT := github.com/tensorchord/openmodelz/mdz # Target binaries. You can build multiple binaries for a single project. TARGETS := mdz # Container image prefix and suffix added to targets. # The final built images are: # $[REGISTRY]/$[IMAGE_PREFIX]$[TARGET]$[IMAGE_SUFFIX]:$[VERSION] # $[REGISTRY] is an item from $[REGISTRIES], $[TARGET] is an item from $[TARGETS]. IMAGE_PREFIX ?= $(strip ) IMAGE_SUFFIX ?= $(strip ) # Container registries. REGISTRY ?= ghcr.io/tensorchord # Container registry for base images. BASE_REGISTRY ?= docker.io BASE_REGISTRY_USER ?= modelzai # Disable CGO by default. CGO_ENABLED ?= 0 GOOS ?= $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH) # # These variables should not need tweaking. # # It's necessary to set this because some environments don't link sh -> bash. export SHELL := bash # It's necessary to set the errexit flags for the bash shell. export SHELLOPTS := errexit PACKAGE_NAME := github.com/tensorchord/openmodelz/mdz GOLANG_CROSS_VERSION ?= v1.17.6 # Project main package location (can be multiple ones). CMD_DIR := ./cmd # Project output directory. OUTPUT_DIR := ./bin DEBUG_DIR := ./debug-bin # Build directory. BUILD_DIR := ./build # Current version of the project. VERSION ?= $(shell git describe --match 'v[0-9]*' --always --tags --abbrev=0) BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') GIT_COMMIT=$(shell git rev-parse HEAD) GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi) GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi) GITSHA ?= $(shell git rev-parse --short HEAD) BUILD_FLAGS ?= -s -w \ -X $(ROOT)/pkg/version.version=$(VERSION) \ -X $(ROOT)/pkg/version.buildDate=$(BUILD_DATE) \ -X $(ROOT)/pkg/version.gitCommit=$(GIT_COMMIT) \ -X $(ROOT)/pkg/version.gitTreeState=$(GIT_TREE_STATE) # Track code version with Docker Label. DOCKER_LABELS ?= git-describe="$(shell date -u +v%Y%m%d)-$(shell git describe --tags --always --dirty)" # Golang standard bin directory. GOPATH ?= $(shell go env GOPATH) GOROOT ?= $(shell go env GOROOT) BIN_DIR := $(GOPATH)/bin GOLANGCI_LINT := $(BIN_DIR)/golangci-lint # check if we need embed the dashboard DASHBOARD_BUILD ?= debug # Default golang flags used in build and test # -mod=vendor: force go to use the vendor files instead of using the `$GOPATH/pkg/mod` # -p: the number of programs that can be run in parallel # -count: run each test and benchmark 1 times. Set this flag to disable test cache export GOFLAGS ?= -count=1 # # Define all targets. At least the following commands are required: # # All targets. .PHONY: help lint test build container push addlicense debug debug-local build-local generate clean test-local addlicense-install release build-image .DEFAULT_GOAL:=build build: build-release ## Build the release version help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) debug: debug-local ## Build the debug version # more info about `GOGC` env: https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint lint: $(GOLANGCI_LINT) ## Lint GO code @$(GOLANGCI_LINT) run $(GOLANGCI_LINT): curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin mockgen-install: go install github.com/golang/mock/mockgen@v1.6.0 addlicense-install: go install github.com/google/addlicense@latest sqlc-install: go install github.com/kyleconroy/sqlc/cmd/sqlc@latest # https://github.com/swaggo/swag/pull/1322, we should use master instead of latest for now. swag-install: go install github.com/swaggo/swag/cmd/swag@v1.8.7 build-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build \ -trimpath -v -o $(OUTPUT_DIR)/$${target} \ -ldflags "$(BUILD_FLAGS)" \ $(CMD_DIR)/$${target}; \ done build-release: BUILD_FLAGS += -X $(ROOT)/pkg/version.gitTag=$(GIT_TAG) build-release: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -o $(OUTPUT_DIR)/$${target} \ -ldflags "$(BUILD_FLAGS)" \ $(CMD_DIR)/$${target}; \ done # It is used by vscode to attach into the process. debug-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) go build -tags $(DASHBOARD_BUILD) -trimpath \ -v -o $(DEBUG_DIR)/$${target} \ -ldflags "$(BUILD_FLAGS)" \ -gcflags='all=-N -l' \ $(CMD_DIR)/$${target}; \ done addlicense: addlicense-install ## Add license to GO code files addlicense -l mpl -c "TensorChord Inc." $$(find . -type f -name '*.go' | grep -v pkg/docs/docs.go) test-local: @go test -tags=$(DASHBOARD_BUILD) -v -race -coverprofile=coverage.out ./... test: ## Run the tests @go test -tags=$(DASHBOARD_BUILD) -race -coverpkg=./pkg/... -coverprofile=coverage.out ./... @go tool cover -func coverage.out | tail -n 1 | awk '{ print "Total coverage: " $$3 }' clean: ## Clean the outputs and artifacts @-rm -vrf ${OUTPUT_DIR} @-rm -vrf ${DEBUG_DIR} @-rm -vrf build dist .eggs *.egg-info fmt: swag-install ## Run go fmt against code. go fmt ./... swag fmt vet: ## Run go vet against code. go vet ./... build-image: build-local docker build -t ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/modelz-autoscaler:dev -f Dockerfile ./bin docker push ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/autoscaler:dev release: @if [ ! -f ".release-env" ]; then \ echo "\033[91m.release-env is required for release\033[0m";\ exit 1;\ fi docker run \ --rm \ --privileged \ -e CGO_ENABLED=1 \ --env-file .release-env \ -v /var/run/docker.sock:/var/run/docker.sock \ -v `pwd`:/go/src/$(PACKAGE_NAME) \ -v `pwd`/sysroot:/sysroot \ -w /go/src/$(PACKAGE_NAME) \ goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ release --rm-dist tsschema: swag @cd dashboard; pnpm tsschema generate: mockgen-install sqlc-install swag tsschema @mockgen -source pkg/query/querier.go -destination pkg/query/mock/mock.go -package mock @sqlc generate dashboard-build: @cd dashboard; pnpm build ================================================ FILE: mdz/README.md ================================================
# mdz CLI for OpenModelZ.

discord invitation link trackgit-views

## Installation ``` pip install openmodelz ``` ## CLI Reference Please check out our [CLI Documentation](./docs/cli/mdz.md) ================================================ FILE: mdz/cmd/mdz/main.go ================================================ /* Copyright © 2023 NAME HERE */ package main import "github.com/tensorchord/openmodelz/mdz/pkg/cmd" func main() { cmd.Execute() } ================================================ FILE: mdz/docs/cli/mdz.md ================================================ ## mdz mdz manages your deployments ### Synopsis mdz helps you deploy applications, manage servers, and troubleshoot issues. ### Examples ``` mdz server start mdz deploy --image modelzai/llm-bloomz-560m:23.06.13 --name llm mdz list mdz logs llm mdz port-forward llm 7860 mdz exec llm ps mdz exec llm --tty bash mdz delete llm ``` ### Options ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -h, --help help for mdz -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz delete](mdz_delete.md) - Delete OpenModelz inferences * [mdz deploy](mdz_deploy.md) - Deploy a new deployment * [mdz exec](mdz_exec.md) - Execute a command in a deployment * [mdz list](mdz_list.md) - List the deployments * [mdz logs](mdz_logs.md) - Print the logs for a deployment * [mdz port-forward](mdz_port-forward.md) - Forward one local port to a deployment * [mdz scale](mdz_scale.md) - Scale a deployment * [mdz server](mdz_server.md) - Manage the servers * [mdz version](mdz_version.md) - Print the client and agent version information ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_delete.md ================================================ ## mdz delete Delete OpenModelz inferences ### Synopsis Deletes OpenModelZ inferences ``` mdz delete [flags] ``` ### Examples ``` mdz delete blomdz-560m ``` ### Options ``` -h, --help help for delete ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_deploy.md ================================================ ## mdz deploy Deploy a new deployment ### Synopsis Deploys a new deployment directly via flags. ``` mdz deploy [flags] ``` ### Examples ``` mdz deploy --image=modelzai/llm-blomdz-560m:23.06.13 mdz deploy --image=modelzai/llm-blomdz-560m:23.06.13 --name blomdz-560m --node-labels gpu=true,name=node-name ``` ### Options ``` --command string Command to run --gpu int Number of GPUs -h, --help help for deploy --image string Image to deploy --max-replicas int32 Maximum number of replicas (default 1) --min-replicas int32 Minimum number of replicas (can be 0) (default 1) --name string Name of inference -l, --node-labels strings Node labels --port int32 Port to deploy on (default 8080) --probe-path string HTTP Health probe path ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_exec.md ================================================ ## mdz exec Execute a command in a deployment ### Synopsis Execute a command in a deployment. If no instance is specified, the first instance is used. ``` mdz exec [flags] ``` ### Examples ``` mdz exec bloomz-560m ps mdz exec bloomz-560m --instance bloomz-560m-abcde-abcde ps mdz exec bllomz-560m -ti bash mdz exec bloomz-560m --instance bloomz-560m-abcde-abcde -ti bash ``` ### Options ``` -h, --help help for exec -s, --instance string Instance name -i, --interactive Keep stdin open even if not attached -t, --tty Allocate a TTY for the container ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_list.md ================================================ ## mdz list List the deployments ### Synopsis List the deployments ``` mdz list [flags] ``` ### Examples ``` mdz list mdz list -v mdz list -q ``` ### Options ``` -h, --help help for list -q, --quiet Quiet mode - print out only the inference names -v, --verbose Verbose mode - print out all inference details ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments * [mdz list instance](mdz_list_instance.md) - List all instances for the given deployment ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_list_instance.md ================================================ ## mdz list instance List all instances for the given deployment ### Synopsis List all instances for the given deployment ``` mdz list instance [flags] ``` ### Examples ``` mdz list instance bloomz-560m mdz list instance bloomz-560m -v mdz list instance bloomz-560m -q ``` ### Options ``` -h, --help help for instance -q, --quiet Quiet mode - print out only the instance names -v, --verbose Verbose mode - print out all instance details ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz list](mdz_list.md) - List the deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_logs.md ================================================ ## mdz logs Print the logs for a deployment ### Synopsis Print the logs for a deployment ``` mdz logs [flags] ``` ### Examples ``` mdz logs blomdz-560m ``` ### Options ``` -e, --end string Only return logs before this timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) -f, --follow Follow log output -h, --help help for logs -s, --since string Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes) (default "2006-01-02T15:04:05Z") -t, --tail int Number of lines to show from the end of the logs ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_port-forward.md ================================================ ## mdz port-forward Forward one local port to a deployment ### Synopsis Forward one local port to a deployment ``` mdz port-forward [flags] ``` ### Examples ``` mdz port-forward blomdz-560m 7860 ``` ### Options ``` -h, --help help for port-forward ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_scale.md ================================================ ## mdz scale Scale a deployment ### Synopsis Scale a deployment ``` mdz scale [flags] ``` ### Examples ``` mdz scale bloomz-560m --replicas 3 ``` ### Options ``` -h, --help help for scale -r, --replicas int32 Number of replicas to scale to ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server.md ================================================ ## mdz server Manage the servers ### Synopsis Manage the servers ### Examples ``` mdz server start ``` ### Options ``` -h, --help help for server -v, --verbose Verbose output ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments * [mdz server delete](mdz_server_delete.md) - Delete a node from the cluster * [mdz server destroy](mdz_server_destroy.md) - Destroy the cluster * [mdz server join](mdz_server_join.md) - Join to the cluster * [mdz server label](mdz_server_label.md) - Update the labels on a server * [mdz server list](mdz_server_list.md) - List all servers in the cluster * [mdz server start](mdz_server_start.md) - Start the server * [mdz server stop](mdz_server_stop.md) - Stop the server ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server_delete.md ================================================ ## mdz server delete Delete a node from the cluster ### Synopsis Delete a node from the cluster ``` mdz server delete [flags] ``` ### Examples ``` mdz server delete gpu-node-1 ``` ### Options ``` -h, --help help for delete ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) -v, --verbose Verbose output ``` ### SEE ALSO * [mdz server](mdz_server.md) - Manage the servers ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server_destroy.md ================================================ ## mdz server destroy Destroy the cluster ### Synopsis Destroy the cluster ``` mdz server destroy [flags] ``` ### Examples ``` mdz server destroy ``` ### Options ``` -h, --help help for destroy ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) -v, --verbose Verbose output ``` ### SEE ALSO * [mdz server](mdz_server.md) - Manage the servers ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server_join.md ================================================ ## mdz server join Join to the cluster ### Synopsis Join to the cluster ``` mdz server join [flags] ``` ### Examples ``` mdz server join 192.168.31.192 ``` ### Options ``` -h, --help help for join --mirror-endpoints https://quay.io Mirror URL endpoints of the registry like https://quay.io --mirror-name string Mirror domain name of the registry (default "docker.io") ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) -v, --verbose Verbose output ``` ### SEE ALSO * [mdz server](mdz_server.md) - Manage the servers ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server_label.md ================================================ ## mdz server label Update the labels on a server ### Synopsis Update the labels on a server * A label key and value must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to 63 characters each. * Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app. ``` mdz server label [flags] ``` ### Examples ``` mdz server label node-name key=value [key=value...] ``` ### Options ``` -h, --help help for label ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) -v, --verbose Verbose output ``` ### SEE ALSO * [mdz server](mdz_server.md) - Manage the servers ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server_list.md ================================================ ## mdz server list List all servers in the cluster ### Synopsis List all servers in the cluster ``` mdz server list [flags] ``` ### Examples ``` mdz server list ``` ### Options ``` -h, --help help for list -q, --quiet Quiet mode - print out only the server names -v, --verbose Verbose mode - print out all server details ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz server](mdz_server.md) - Manage the servers ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server_start.md ================================================ ## mdz server start Start the server ### Synopsis Start the server with the public IP of the machine. If not provided, the internal IP will be used automatically. ``` mdz server start [flags] ``` ### Examples ``` mdz server start mdz server start -v mdz server start 1.2.3.4 ``` ### Options ``` -g, --force-gpu Start the server with GPU support (ignore the GPU detection) -h, --help help for start --mirror-endpoints https://quay.io Mirror URL endpoints of the registry like https://quay.io --mirror-name string Mirror domain name of the registry (default "docker.io") ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) -v, --verbose Verbose output ``` ### SEE ALSO * [mdz server](mdz_server.md) - Manage the servers ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_server_stop.md ================================================ ## mdz server stop Stop the server ### Synopsis Stop the server ``` mdz server stop [flags] ``` ### Examples ``` mdz server stop ``` ### Options ``` -h, --help help for stop ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) -v, --verbose Verbose output ``` ### SEE ALSO * [mdz server](mdz_server.md) - Manage the servers ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/cli/mdz_version.md ================================================ ## mdz version Print the client and agent version information ### Synopsis Print the client and server version information ``` mdz version [flags] ``` ### Examples ``` mdz version ``` ### Options ``` -h, --help help for version ``` ### Options inherited from parent commands ``` --debug Enable debug logging --disable-telemetry Disable anonymous telemetry -u, --url string URL to use for the server (MDZ_URL) (default http://localhost:80) ``` ### SEE ALSO * [mdz](mdz.md) - mdz manages your deployments ###### Auto generated by spf13/cobra on 11-Aug-2023 ================================================ FILE: mdz/docs/macOS-quickstart.md ================================================ # `mdz` Quick Start Guide for macOS `mdz` only runs on Linux. To use it on macOS, you'll need a Linux VM. This guide will show you how to set up a Linux VM on macOS, install and use `mdz` on it. - [`mdz` Quick Start Guide for macOS](#mdz-quick-start-guide-for-macos) - [VM Setup](#vm-setup) - [Install `mdz`](#install-mdz) - [Drop into the VM shell](#drop-into-the-vm-shell) - [Install build dependencies](#install-build-dependencies) - [Clone the repository and build](#clone-the-repository-and-build) - [`mdz` Usage](#mdz-usage) ## VM Setup I use [OrbStack](https://docs.orbstack.dev/machines/) to create and manage my VMs. ![](images/orbstack-vm-create.png) Open OrbStack and go to the `Linux Machines` tab, then click `Create` button to create a new VM if you don't already have one. This guide uses Archlinux, but you're free to use any distribution you like. ## Install `mdz` ### Drop into the VM shell ``` orb # now you're in the Linux VM's shell ``` ### Install build dependencies ``` # or use your favorite package manager sudo pacman -Sy go git ``` ### Clone the repository and build ``` git clone https://github.com/tensorchord/OpenModelZ.git cd OpenModelZ/mdz && make # `make` will build the `mdz` binary under `bin/` for you ``` ## `mdz` Usage ``` # replace with your `mdz` path, typically it's `./bin/mdz` mdz --help ``` ================================================ FILE: mdz/examples/bloomz-560m-openai/README.md ================================================ # Bloomz 560M OpenAI Compatible API This is a simple API that allows you to use the Bloomz 560M as a OpenAI Gym environment. ## Deploy ```bash $ mdz deploy --image modelzai/llm-bloomz-560m:23.06.13 --name llm ``` ### Get the deployment ```bash $ mdz list NAME ENDPOINT STATUS REPLICAS llm http://localhost:31112/inference/llm.default Ready 1/1 ``` ### Test the deployment ```python import openai openai.api_base="http://localhost:31112/inference/llm.default" openai.api_key="any" openai.debug = True # create a chat completion chat_completion = openai.ChatCompletion.create(model="", messages=[ {"role": "user", "content": "Who are you?"}, {"role": "assistant", "content": "I am a student"}, {"role": "user", "content": "What do you learn?"}, {"role": "assistant", "content": "I learn math"}, {"role": "user", "content": "Do you like english?"} ], max_tokens=100) ``` ================================================ FILE: mdz/hack/cli-doc-gen/main.go ================================================ package main import ( "fmt" "os" "path/filepath" cmd "github.com/tensorchord/openmodelz/mdz/pkg/cmd" ) func main() { path, err := os.Getwd() if err != nil { panic(err) } fmt.Println("Generating docs in", filepath.Join(path, "docs", "cli")) if err := cmd.GenMarkdownTree(filepath.Join(path, "docs", "cli")); err != nil { panic(err) } } ================================================ FILE: mdz/pkg/agentd/runtime/create.go ================================================ package runtime import ( "context" "fmt" "os" "time" dockertypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" "github.com/moby/moby/pkg/jsonmessage" "github.com/moby/term" "github.com/phayes/freeport" "github.com/tensorchord/openmodelz/agent/api/types" ) func (r *Runtime) InferenceCreate(ctx context.Context, req types.InferenceDeployment) error { cfg := &container.Config{ Image: req.Spec.Image, ExposedPorts: nat.PortSet{}, } var port int32 = 8080 if req.Spec.Port != nil { port = *req.Spec.Port } now := time.Now() req.Status = types.InferenceDeploymentStatus{ Phase: types.PhaseNotReady, Replicas: 1, CreatedAt: &now, } // Lock the mutex and set cache r.mutex.Lock() r.cache[req.Spec.Name] = req r.mutex.Unlock() go func() error { body, err := r.cli.ImagePull(context.TODO(), req.Spec.Image, dockertypes.ImagePullOptions{}) if err != nil { return err } defer body.Close() termFd, isTerm := term.GetFdInfo(os.Stdout) err = jsonmessage.DisplayJSONMessagesStream(body, os.Stdout, termFd, isTerm, nil) if err != nil { return err } hostCfg := &container.HostConfig{ RestartPolicy: container.RestartPolicy{ Name: "always", }, PortBindings: nat.PortMap{}, } natPort := nat.Port(fmt.Sprintf("%d/tcp", port)) hostPort, err := freeport.GetFreePort() if err != nil { return err } hostCfg.PortBindings[natPort] = []nat.PortBinding{ { HostIP: Localhost, HostPort: fmt.Sprintf("%d", hostPort), }, } cfg.ExposedPorts[natPort] = struct{}{} cfg.Labels = expectedLabels(req) ctr, err := r.cli.ContainerCreate(context.TODO(), cfg, hostCfg, nil, nil, req.Spec.Name) if err != nil { return err } if err := r.cli.ContainerStart(context.TODO(), ctr.ID, dockertypes.ContainerStartOptions{}); err != nil { return err } r.mutex.Lock() new := r.cache[req.Spec.Name] new.Status = types.InferenceDeploymentStatus{ Phase: types.PhaseReady, Replicas: 1, AvailableReplicas: 1, CreatedAt: &now, } r.mutex.Unlock() return nil }() return nil } ================================================ FILE: mdz/pkg/agentd/runtime/delete.go ================================================ package runtime import ( "context" dockertypes "github.com/docker/docker/api/types" "github.com/tensorchord/openmodelz/agent/client" ) func (r *Runtime) InferenceDelete(ctx context.Context, name string) error { defer func() { r.mutex.Lock() delete(r.cache, name) r.mutex.Unlock() }() ctr, err := r.cli.ContainerInspect(ctx, name) if err != nil { if !client.IsErrNotFound(err) { return nil } } if ctr.Config.Labels[labelVendor] != valueVendor { return nil } if err := r.cli.ContainerRemove(ctx, name, dockertypes.ContainerRemoveOptions{ Force: true, }); err != nil { return err } return nil } ================================================ FILE: mdz/pkg/agentd/runtime/label.go ================================================ package runtime import "github.com/tensorchord/openmodelz/agent/api/types" const ( labelVendor = "ai.modelz.open.vendor" valueVendor = "openmodelz" labelName = "ai.modelz.open.name" ) func expectedLabels(inf types.InferenceDeployment) map[string]string { return map[string]string{ labelVendor: valueVendor, labelName: inf.Spec.Name, } } ================================================ FILE: mdz/pkg/agentd/runtime/list.go ================================================ package runtime import ( "context" dockertypes "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/tensorchord/openmodelz/agent/api/types" ) func (r *Runtime) InferenceList(ns string) ([]types.InferenceDeployment, error) { r.mutex.Lock() res := r.cache r.mutex.Unlock() ctrs, err := r.cli.ContainerList(context.TODO(), dockertypes.ContainerListOptions{ Filters: filters.NewArgs(filters.Arg("label", labelVendor+"="+valueVendor)), }) if err != nil { return nil, err } for _, ctr := range ctrs { inf := types.InferenceDeployment{ Spec: types.InferenceDeploymentSpec{ Name: ctr.Labels[labelName], Image: ctr.Image, Namespace: "default", }, Status: types.InferenceDeploymentStatus{}, } if ctr.State == "running" { inf.Status.Phase = types.PhaseReady inf.Status.AvailableReplicas = 1 inf.Status.Replicas = 1 } else { inf.Status.Phase = types.PhaseNotReady inf.Status.Replicas = 1 } res[inf.Spec.Name] = inf } l := []types.InferenceDeployment{} for _, inf := range res { l = append(l, inf) } return l, nil } ================================================ FILE: mdz/pkg/agentd/runtime/proxy.go ================================================ package runtime import ( "errors" "net" "net/http" "net/http/httputil" "net/url" "time" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/errdefs" ) func (r *Runtime) InfereceProxy(c *gin.Context, name string) error { ctr, err := r.cli.ContainerInspect(c.Request.Context(), name) if err != nil { return errdefs.System(err) } if ctr.Config.Labels[labelVendor] != valueVendor { return errdefs.NotFound(errors.New("container not found")) } port := "" for _, c := range ctr.HostConfig.PortBindings { if len(c) > 0 { port = c[0].HostPort break } } if port == "" { return errdefs.NotFound(errors.New("port not found")) } url, err := url.Parse("http://" + Localhost + ":" + port) if err != nil { return errdefs.System(err) } proxyServer := httputil.NewSingleHostReverseProxy(url) proxyServer.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: time.Minute * 5, KeepAlive: time.Minute * 5, DualStack: true, }).DialContext, } proxyServer.Director = func(req *http.Request) { targetQuery := url.RawQuery req.URL.Scheme = url.Scheme req.URL.Host = url.Host // req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL) if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery } else { req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery } req.URL.Path = c.Param("proxyPath") if req.URL.Path == "" { req.URL.Path = "/" } } proxyServer.ServeHTTP(c.Writer, c.Request) return nil } ================================================ FILE: mdz/pkg/agentd/runtime/runtime.go ================================================ package runtime import ( "context" "sync" "github.com/docker/docker/client" "github.com/tensorchord/openmodelz/agent/api/types" ) type Runtime struct { cli client.APIClient cache map[string]types.InferenceDeployment mutex sync.Mutex } const ( Localhost = "127.0.0.1" ) func New() (*Runtime, error) { cli, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return nil, err } cli.NegotiateAPIVersion(context.Background()) return &Runtime{ cli: cli, cache: map[string]types.InferenceDeployment{}, }, nil } ================================================ FILE: mdz/pkg/agentd/server/error.go ================================================ package server import ( "bytes" "fmt" "net/http" "github.com/tensorchord/openmodelz/agent/errdefs" ) // Error defines a standard application error. type Error struct { // Machine-readable error code. HTTPStatusCode int `json:"http_status_code,omitempty"` // Human-readable message. Message string `json:"message,omitempty"` Request string `json:"request,omitempty"` // Logical operation and nested error. Op string `json:"op,omitempty"` Err error `json:"error,omitempty"` } // Error returns the string representation of the error message. func (e *Error) Error() string { var buf bytes.Buffer // Print the current operation in our stack, if any. if e.Op != "" { fmt.Fprintf(&buf, "%s: ", e.Op) } // If wrapping an error, print its Error() message. // Otherwise print the error code & message. if e.Err != nil { buf.WriteString(e.Err.Error()) } else { if e.HTTPStatusCode != 0 { fmt.Fprintf(&buf, "<%s> ", http.StatusText(e.HTTPStatusCode)) } buf.WriteString(e.Message) } return buf.String() } func NewError(code int, err error, op string) error { return &Error{ HTTPStatusCode: code, Err: err, Message: err.Error(), Op: op, } } func errFromErrDefs(err error, op string) error { if errdefs.IsCancelled(err) { return NewError(http.StatusRequestTimeout, err, op) } else if errdefs.IsConflict(err) { return NewError(http.StatusConflict, err, op) } else if errdefs.IsDataLoss(err) { return NewError(http.StatusInternalServerError, err, op) } else if errdefs.IsDeadline(err) { return NewError(http.StatusRequestTimeout, err, op) } else if errdefs.IsForbidden(err) { return NewError(http.StatusForbidden, err, op) } else if errdefs.IsInvalidParameter(err) { return NewError(http.StatusBadRequest, err, op) } else if errdefs.IsNotFound(err) { return NewError(http.StatusNotFound, err, op) } else if errdefs.IsNotImplemented(err) { return NewError(http.StatusNotImplemented, err, op) } else if errdefs.IsNotModified(err) { return NewError(http.StatusNotModified, err, op) } else if errdefs.IsSystem(err) { return NewError(http.StatusInternalServerError, err, op) } else if errdefs.IsUnauthorized(err) { return NewError(http.StatusUnauthorized, err, op) } else if errdefs.IsUnavailable(err) { return NewError(http.StatusServiceUnavailable, err, op) } else if errdefs.IsUnknown(err) { return NewError(http.StatusInternalServerError, err, op) } return NewError(http.StatusInternalServerError, err, op) } ================================================ FILE: mdz/pkg/agentd/server/handler_healthz.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" ) // @Summary Healthz // @Description Healthz // @Tags system // @Accept json // @Produce json // @Success 200 // @Router /healthz [get] func (s *Server) handleHealthz(c *gin.Context) error { c.Status(http.StatusOK) return nil } ================================================ FILE: mdz/pkg/agentd/server/handler_inference_create.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Create the inferences. // @Description Create the inferences. // @Tags inference // @Accept json // @Produce json // @Param request body types.InferenceDeployment true "query params" // @Success 201 {object} types.InferenceDeployment // @Router /system/inferences [post] func (s *Server) handleInferenceCreate(c *gin.Context) error { event := types.DeploymentCreateEvent var req types.InferenceDeployment if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } // Set the default values. s.validator.DefaultDeployRequest(&req) // Validate the request. if err := s.validator.ValidateDeployRequest(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } // Create the inference. if err := s.runtime.InferenceCreate(c.Request.Context(), req); err != nil { return errFromErrDefs(err, event) } c.JSON(http.StatusCreated, req) return nil } ================================================ FILE: mdz/pkg/agentd/server/handler_inference_delete.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Delete the inferences. // @Description Delete the inferences. // @Tags inference // @Accept json // @Produce json // @Param request body types.DeleteFunctionRequest true "query params" // @Param namespace query string true "Namespace" example("modelz-d3524a71-c17c-4c92-8faf-8603f02f4713") // @Success 202 {object} types.DeleteFunctionRequest // @Router /system/inferences [delete] func (s *Server) handleInferenceDelete(c *gin.Context) error { event := types.DeploymentDeleteEvent var req types.DeleteFunctionRequest if err := c.ShouldBindJSON(&req); err != nil { return NewError(http.StatusBadRequest, err, event) } if req.FunctionName == "" { return NewError( http.StatusBadRequest, errors.New("function name is required"), event) } if err := s.runtime.InferenceDelete(c.Request.Context(), req.FunctionName); err != nil { return errFromErrDefs(err, event) } c.JSON(http.StatusAccepted, req) return nil } ================================================ FILE: mdz/pkg/agentd/server/handler_inference_get.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary Get the inference by name. // @Description Get the inference by name. // @Tags inference // @Accept json // @Produce json // @Param namespace query string true "Namespace" example("modelz-d3524a71-c17c-4c92-8faf-8603f02f4713") // @Param name path string true "inference id" example("e50886f3-caa6-449f-9fa8-7849c6ba2e08") // @Success 200 {object} types.InferenceDeployment // @Router /system/inference/{name} [get] func (s *Server) handleInferenceGet(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), "inference-get") } name := c.Param("name") if name == "" { return NewError( http.StatusBadRequest, errors.New("name is required"), "inference-get") } // function, err := s.runtime.InferenceGet(namespace, name) // if err != nil { // return errFromErrDefs(err, "inference-get") // } // c.JSON(http.StatusOK, function) return nil } ================================================ FILE: mdz/pkg/agentd/server/handler_inference_list.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" _ "github.com/tensorchord/openmodelz/agent/api/types" ) // @Summary List the inferences. // @Description List the inferences. // @Tags inference // @Accept json // @Produce json // @Param namespace query string true "Namespace" example("modelz-d3524a71-c17c-4c92-8faf-8603f02f4713") // @Success 200 {object} []types.InferenceDeployment // @Router /system/inferences [get] func (s *Server) handleInferenceList(c *gin.Context) error { namespace := c.Query("namespace") if namespace == "" { return NewError( http.StatusBadRequest, errors.New("namespace is required"), "inference-list") } inferenes, err := s.runtime.InferenceList(namespace) if err != nil { return errFromErrDefs(err, "inference-list") } c.JSON(http.StatusOK, inferenes) return nil } ================================================ FILE: mdz/pkg/agentd/server/handler_inference_logs.go ================================================ package server import ( "github.com/gin-gonic/gin" ) // @Summary Get the inference logs. // @Description Get the inference logs. // @Tags log // @Accept json // @Produce json // @Param namespace query string true "Namespace" example("modelz-d3524a71-c17c-4c92-8faf-8603f02f4713") // @Param name query string true "Name" // @Param instance query string false "Instance" // @Param tail query int false "Tail" // @Param follow query bool false "Follow" // @Param since query string false "Since" example("2023-04-01T00:06:31+08:00") // @Param end query string false "End" example("2023-05-31T00:06:31+08:00") // @Success 200 {object} []types.Message // @Router /system/logs/inference [get] func (s *Server) handleInferenceLogs(c *gin.Context) error { return nil } ================================================ FILE: mdz/pkg/agentd/server/handler_inference_proxy.go ================================================ package server import ( "errors" "fmt" "net/http" "strings" "github.com/gin-gonic/gin" ) // @Summary Inference. // @Description Inference proxy. // @Tags inference-proxy // @Accept json // @Produce json // @Router /inference/{name} [post] // @Router /inference/{name} [get] // @Router /inference/{name} [put] // @Router /inference/{name} [delete] func (s *Server) handleInferenceProxy(c *gin.Context) error { namespacedName := c.Param("name") if namespacedName == "" { return NewError( http.StatusBadRequest, errors.New("name is required"), "inference-proxy") } _, name, err := getNamespaceAndName(namespacedName) if err != nil { return NewError( http.StatusBadRequest, err, "inference-proxy") } return s.runtime.InfereceProxy(c, name) } func getNamespaceAndName(name string) (string, string, error) { if !strings.Contains(name, ".") { return "", "", fmt.Errorf("name is not namespaced") } namespace := name[strings.LastIndexAny(name, ".")+1:] infName := strings.TrimSuffix(name, "."+namespace) if namespace == "" { return "", "", fmt.Errorf("namespace is empty") } if infName == "" { return "", "", fmt.Errorf("inference name is empty") } return namespace, infName, nil } ================================================ FILE: mdz/pkg/agentd/server/handler_info.go ================================================ package server import ( "net/http" "github.com/gin-gonic/gin" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/mdz/pkg/version" ) // @Summary Get system info. // @Description Get system info. // @Tags system // @Accept json // @Produce json // @Success 200 {object} types.ProviderInfo // @Router /system/info [get] func (s *Server) handleInfo(c *gin.Context) error { v := version.GetVersion() c.JSON(http.StatusOK, types.ProviderInfo{ Name: "local agent", Orchestration: "docker", Version: &types.VersionInfo{ Version: v.Version, BuildDate: v.BuildDate, GitCommit: v.GitCommit, GitTag: v.GitTag, GitTreeState: v.GitTreeState, GoVersion: v.GoVersion, Compiler: v.Compiler, Platform: v.Platform, }, }) return nil } ================================================ FILE: mdz/pkg/agentd/server/middleware_callid.go ================================================ package server import ( "fmt" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" ) func (s Server) middlewareCallID(c *gin.Context) error { start := time.Now() if len(c.Request.Header.Get("X-Call-Id")) == 0 { callID := uuid.New().String() c.Request.Header.Add("X-Call-Id", callID) c.Writer.Header().Add("X-Call-Id", callID) } c.Request.Header.Add("X-Start-Time", fmt.Sprintf("%d", start.UTC().UnixNano())) c.Writer.Header().Add("X-Start-Time", fmt.Sprintf("%d", start.UTC().UnixNano())) c.Next() return nil } ================================================ FILE: mdz/pkg/agentd/server/server_factory.go ================================================ package server import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/pkg/server/validator" ginlogrus "github.com/toorop/gin-logrus" "github.com/tensorchord/openmodelz/mdz/pkg/agentd/runtime" ) type Server struct { router *gin.Engine metricsRouter *gin.Engine logger *logrus.Entry validator *validator.Validator runtime *runtime.Runtime } func New() (*Server, error) { router := gin.New() router.Use(ginlogrus.Logger(logrus.StandardLogger(), "/healthz")) router.Use(gin.Recovery()) // metrics server metricsRouter := gin.New() metricsRouter.Use(gin.Recovery()) if gin.Mode() == gin.DebugMode { logrus.SetLevel(logrus.DebugLevel) logrus.Debug("Allow CORS") router.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, AllowHeaders: []string{"*"}, })) } logger := logrus.WithField("component", "agentd") r, err := runtime.New() if err != nil { return nil, err } s := &Server{ router: router, metricsRouter: metricsRouter, logger: logger, validator: validator.New(), runtime: r, } s.registerRoutes() return s, nil } ================================================ FILE: mdz/pkg/agentd/server/server_handlerfunc.go ================================================ package server import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) type HandlerFunc func(c *gin.Context) error func WrapHandler(handler HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { err := handler(c) if err != nil { var serverErr *Error if !errors.As(err, &serverErr) { serverErr = &Error{ HTTPStatusCode: http.StatusInternalServerError, Err: err, Message: err.Error(), } } serverErr.Request = c.Request.Method + " " + c.Request.URL.String() if gin.Mode() == "debug" { logrus.Debugf("error: %+v", err) } else { // Remove detailed info when in the release mode serverErr.Op = "" serverErr.Err = nil } c.JSON(serverErr.HTTPStatusCode, serverErr) c.Abort() return } } } ================================================ FILE: mdz/pkg/agentd/server/server_init_route.go ================================================ package server import ( swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) const ( endpointInferencePlural = "/inferences" endpointInference = "/inference" endpointScaleInference = "/scale-inference" endpointInfo = "/info" endpointLogPlural = "/logs" endpointNamespacePlural = "/namespaces" endpointHealthz = "/healthz" endpointBuild = "/build" ) func (s *Server) registerRoutes() { root := s.router.Group("/") // swagger root.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) // dataplane root.Any("/inference/:name", WrapHandler(s.middlewareCallID), WrapHandler(s.handleInferenceProxy)) root.Any("/inference/:name/*proxyPath", WrapHandler(s.middlewareCallID), WrapHandler(s.handleInferenceProxy)) // healthz root.GET(endpointHealthz, WrapHandler(s.handleHealthz)) // control plane controlPlane := root.Group("/system") // inferences controlPlane.GET(endpointInferencePlural, WrapHandler(s.handleInferenceList)) controlPlane.POST(endpointInferencePlural, WrapHandler(s.handleInferenceCreate)) // controlPlane.PUT(endpointInferencePlural, // WrapHandler(s.handleInferenceUpdate)) controlPlane.DELETE(endpointInferencePlural, WrapHandler(s.handleInferenceDelete)) // controlPlane.POST(endpointScaleInference, // WrapHandler(s.handleInferenceScale)) controlPlane.GET(endpointInference+"/:name", WrapHandler(s.handleInferenceGet)) // instances // controlPlane.GET(endpointInference+"/:name/instances", // WrapHandler(s.handleInferenceInstance)) // info controlPlane.GET(endpointInfo, WrapHandler(s.handleInfo)) // logs controlPlane.GET(endpointLogPlural+endpointInference, WrapHandler(s.handleInferenceLogs)) } ================================================ FILE: mdz/pkg/agentd/server/server_run.go ================================================ package server import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/sirupsen/logrus" ) func (s *Server) Run(port int) error { srv := &http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: s.router, WriteTimeout: time.Hour * 24, ReadTimeout: time.Hour * 24, } go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logrus.Errorf("listen on port %d error: %v", port, err) } }() logrus.WithField("port", port). Info("server is running...") quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logrus.Info("shutdown server") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return srv.Shutdown(ctx) } ================================================ FILE: mdz/pkg/cmd/delete.go ================================================ package cmd import ( "github.com/cockroachdb/errors" "github.com/spf13/cobra" ) // deleteCmd represents the delete command var deleteCmd = &cobra.Command{ Use: "delete", Short: "Delete OpenModelz inferences", Long: `Deletes OpenModelZ inferences`, Example: ` mdz delete blomdz-560m`, GroupID: "basic", PreRunE: commandInit, Args: cobra.ExactArgs(1), RunE: commandDelete, } func init() { rootCmd.AddCommand(deleteCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: } func commandDelete(cmd *cobra.Command, args []string) error { name := args[0] if err := agentClient.InferenceRemove( cmd.Context(), namespace, name); err != nil { cmd.PrintErrf("Failed to remove the inference: %s\n", errors.Cause(err)) return err } cmd.Printf("Inference %s is deleted\n", name) return nil } ================================================ FILE: mdz/pkg/cmd/deploy.go ================================================ package cmd import ( "math/rand" "strconv" "time" "github.com/cockroachdb/errors" petname "github.com/dustinkirkland/golang-petname" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/mdz/pkg/telemetry" ) var ( // Used for flags. deployImage string deployPort int32 deployMinReplicas int32 deployMaxReplicas int32 deployName string deployGPU int deployNodeLabel []string deployCommand string deployProbePath string ) // deployCmd represents the deploy command var deployCmd = &cobra.Command{ Use: "deploy", Short: "Deploy a new deployment", Long: `Deploys a new deployment directly via flags.`, Example: ` mdz deploy --image=modelzai/llm-blomdz-560m:23.06.13 mdz deploy --image=modelzai/llm-blomdz-560m:23.06.13 --name blomdz-560m --node-labels gpu=true,name=node-name`, GroupID: "basic", PreRunE: commandInit, RunE: commandDeploy, } func init() { rand.Seed(time.Now().UTC().UnixNano()) rootCmd.AddCommand(deployCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: // deployCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") deployCmd.Flags().StringVar(&deployImage, "image", "", "Image to deploy") deployCmd.Flags().Int32Var(&deployPort, "port", 8080, "Port to deploy on") deployCmd.Flags().Int32Var(&deployMinReplicas, "min-replicas", 1, "Minimum number of replicas (can be 0)") deployCmd.Flags().Int32Var(&deployMaxReplicas, "max-replicas", 1, "Maximum number of replicas") deployCmd.Flags().IntVar(&deployGPU, "gpu", 0, "Number of GPUs") deployCmd.Flags().StringVar(&deployName, "name", "", "Name of inference") deployCmd.Flags().StringSliceVarP(&deployNodeLabel, "node-labels", "l", []string{}, "Node labels") deployCmd.Flags().StringVar(&deployCommand, "command", "", "Command to run") deployCmd.Flags().StringVar(&deployProbePath, "probe-path", "", "HTTP Health probe path") } func commandDeploy(cmd *cobra.Command, args []string) error { if deployImage == "" { return cmd.Help() } name := deployName if name == "" { name = petname.Generate(2, "-") } var typ types.ScalingType = types.ScalingTypeCapacity inf := types.InferenceDeployment{ Spec: types.InferenceDeploymentSpec{ Image: deployImage, Namespace: namespace, Name: name, Labels: map[string]string{ "ai.tensorchord.name": name, }, Framework: types.FrameworkOther, Scaling: &types.ScalingConfig{ MinReplicas: int32Ptr(deployMinReplicas), MaxReplicas: int32Ptr(deployMaxReplicas), TargetLoad: int32Ptr(10), Type: &typ, StartupDuration: int32Ptr(600), ZeroDuration: int32Ptr(600), }, Port: int32Ptr(deployPort), }, } if deployCommand != "" { inf.Spec.Command = &deployCommand } if deployProbePath != "" { inf.Spec.HTTPProbePath = &deployProbePath } if len(deployNodeLabel) > 0 { inf.Spec.Constraints = []string{} for _, label := range deployNodeLabel { inf.Spec.Constraints = append(inf.Spec.Constraints, "tensorchord.ai/"+label) } } if deployGPU > 0 { GPUNum := types.Quantity(strconv.Itoa(deployGPU)) inf.Spec.Resources = &types.ResourceRequirements{ // no need to set Requests for GPU Limits: types.ResourceList{ types.ResourceGPU: GPUNum, }, } } telemetry.GetTelemetry().Record( "deploy", telemetry.AddField("GPU", deployGPU), telemetry.AddField("FromZero", deployMinReplicas == 0), ) if _, err := agentClient.InferenceCreate( cmd.Context(), namespace, inf); err != nil { cmd.PrintErrf("Failed to create the inference: %s\n", errors.Cause(err)) return err } cmd.Printf("Inference %s is created\n", inf.Spec.Name) return nil } func int32Ptr(i int32) *int32 { return &i } ================================================ FILE: mdz/pkg/cmd/exec.go ================================================ package cmd import ( "fmt" "io" "os" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/agent/client" "github.com/tensorchord/openmodelz/mdz/pkg/cmd/streams" terminal "golang.org/x/term" "k8s.io/apimachinery/pkg/util/rand" ) var ( execInstance string execTTY bool execInteractive bool ) // execCommand represents the exec command var execCommand = &cobra.Command{ Use: "exec", Short: "Execute a command in a deployment", Long: `Execute a command in a deployment. If no instance is specified, the first instance is used.`, Example: ` mdz exec bloomz-560m ps mdz exec bloomz-560m --instance bloomz-560m-abcde-abcde ps mdz exec bllomz-560m -ti bash mdz exec bloomz-560m --instance bloomz-560m-abcde-abcde -ti bash`, GroupID: "debug", PreRunE: commandInit, Args: cobra.MinimumNArgs(1), RunE: commandExec, } func init() { rootCmd.AddCommand(execCommand) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: execCommand.Flags().StringVarP(&execInstance, "instance", "s", "", "Instance name") execCommand.Flags().BoolVarP(&execTTY, "tty", "t", false, "Allocate a TTY for the container") execCommand.Flags().BoolVarP(&execInteractive, "interactive", "i", false, "Keep stdin open even if not attached") } func commandExec(cmd *cobra.Command, args []string) error { name := args[0] if execInstance == "" { instances, err := agentClient.InstanceList(cmd.Context(), namespace, name) if err != nil { cmd.PrintErrf("Failed to list instances: %s\n", errors.Cause(err)) return err } if len(instances) == 0 { cmd.PrintErrf("instance %s not found\n", name) return errors.Newf("instance %s not found", name) } else if len(instances) > 1 { cmd.PrintErrf("inference %s has multiple instances, please specify with -i\n", name) return errors.Newf("inference %s has multiple instances, please specify with -i", name) } execInstance = instances[0].Spec.Name } if execTTY { shell := "sh" if len(args) > 1 { shell = args[1] } else if len(args) > 2 { cmd.PrintErrf("too many arguments in tty mode, please use a shell program e.g. bash\n") return fmt.Errorf("too many arguments") } if !isAvailableShell(shell) { cmd.PrintErrf("shell %s is not available, try `sh` or `bash`\n", shell) return fmt.Errorf("shell %s is not available, try `sh` or `bash`", shell) } resp, err := agentClient.InstanceExecTTY(cmd.Context(), namespace, name, execInstance, []string{shell}) if err != nil { cmd.PrintErrf("Failed to execute the shell: %s\n", errors.Cause(err)) return err } defer resp.Conn.Close() c := resp.Conn if !terminal.IsTerminal(0) || !terminal.IsTerminal(1) { cmd.PrintErrf("stdin/stdout should be terminal\n") return fmt.Errorf("stdin/stdout should be terminal") } // oldState, err := terminal.MakeRaw(0) // if err != nil { // cmd.PrintErrf("Failed to make raw terminal: %s\n", errors.Cause(err)) // return err // } // oldOutState, err := terminal.MakeRaw(1) // if err != nil { // cmd.PrintErrf("Failed to make raw terminal: %s\n", errors.Cause(err)) // return err // } // defer func() { // terminal.Restore(0, oldState) // terminal.Restore(1, oldOutState) // }() // Send terminal size. w, h, err := terminal.GetSize(0) if err != nil { cmd.PrintErrf("Failed to get terminal size: %s\n", errors.Cause(err)) return err } msg := &client.TerminalMessage{ ID: rand.String(5), Op: "resize", Data: "", Rows: uint16(h), Cols: uint16(w), } if err := c.WriteJSON(msg); err != nil { cmd.PrintErrf("Failed to send terminal message: %s\n", errors.Cause(err)) return err } errCh := make(chan error, 1) cli := newMDZCLI() go func() { defer close(errCh) errCh <- func() error { streamer := hijackedIOStreamer{ streams: cli, inputStream: cli.In(), outputStream: cli.Out(), errorStream: cli.Err(), resp: resp, tty: true, detachKeys: "", } return streamer.stream(cmd.Context()) }() }() if err := <-errCh; err != nil { logrus.Debugf("Error hijack: %s", err) return err } return nil } else { res, err := agentClient.InstanceExec(cmd.Context(), namespace, name, execInstance, args[1:], false) if err != nil { cmd.PrintErrf("Failed to execute the command: %s\n", errors.Cause(err)) return err } cmd.Printf("%s", res) return nil } } func isAvailableShell(shell string) bool { switch shell { case "sh", "bash", "zsh", "fish": return true default: return false } } type mdzCli struct { in *streams.In out *streams.Out err io.Writer } func newMDZCLI() *mdzCli { return &mdzCli{ in: streams.NewIn(os.Stdin), out: streams.NewOut(os.Stdout), err: os.Stderr, } } func (c mdzCli) In() *streams.In { return c.in } func (c mdzCli) Out() *streams.Out { return c.out } func (c mdzCli) Err() io.Writer { return c.err } ================================================ FILE: mdz/pkg/cmd/exec_stream.go ================================================ package cmd import ( "context" "fmt" "io" "runtime" "sync" "github.com/moby/term" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/agent/client" "github.com/tensorchord/openmodelz/mdz/pkg/cmd/ioutils" "github.com/tensorchord/openmodelz/mdz/pkg/cmd/streams" ) // Streams is an interface which exposes the standard input and output streams type Streams interface { In() *streams.In Out() *streams.Out Err() io.Writer } // The default escape key sequence: ctrl-p, ctrl-q // TODO: This could be moved to `pkg/term`. var defaultEscapeKeys = []byte{16, 17} // A hijackedIOStreamer handles copying input to and output from streams to the // connection. type hijackedIOStreamer struct { streams Streams inputStream io.ReadCloser outputStream io.Writer errorStream io.Writer resp client.HijackedResponse tty bool detachKeys string } // stream handles setting up the IO and then begins streaming stdin/stdout // to/from the hijacked connection, blocking until it is either done reading // output, the user inputs the detach key sequence when in TTY mode, or when // the given context is cancelled. func (h *hijackedIOStreamer) stream(ctx context.Context) error { restoreInput, err := h.setupInput() if err != nil { return fmt.Errorf("unable to setup input stream: %s", err) } defer restoreInput() outputDone := h.beginOutputStream(restoreInput) inputDone, detached := h.beginInputStream(restoreInput) select { case err := <-outputDone: return err case <-inputDone: // Input stream has closed. if h.outputStream != nil || h.errorStream != nil { // Wait for output to complete streaming. select { case err := <-outputDone: return err case <-ctx.Done(): return ctx.Err() } } return nil case err := <-detached: // Got a detach key sequence. return err case <-ctx.Done(): return ctx.Err() } } func (h *hijackedIOStreamer) setupInput() (restore func(), err error) { if h.inputStream == nil || !h.tty { // No need to setup input TTY. // The restore func is a nop. return func() {}, nil } if err := setRawTerminal(h.streams); err != nil { return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err) } // Use sync.Once so we may call restore multiple times but ensure we // only restore the terminal once. var restoreOnce sync.Once restore = func() { restoreOnce.Do(func() { _ = restoreTerminal(h.streams, h.inputStream) }) } // Wrap the input to detect detach escape sequence. // Use default escape keys if an invalid sequence is given. escapeKeys := defaultEscapeKeys if h.detachKeys != "" { customEscapeKeys, err := term.ToBytes(h.detachKeys) if err != nil { logrus.Warnf("invalid detach escape keys, using default: %s", err) } else { escapeKeys = customEscapeKeys } } h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close) return restore, nil } func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error { if h.outputStream == nil && h.errorStream == nil { // There is no need to copy output. return nil } outputDone := make(chan error) go func() { var err error // When TTY is ON, use regular copy if h.outputStream != nil && h.tty { _, err = io.Copy(h.outputStream, h.resp) // We should restore the terminal as soon as possible // once the connection ends so any following print // messages will be in normal type. restoreInput() } logrus.Debug("[hijack] End of stdout") if err != nil { logrus.Debugf("Error receiveStdout: %s", err) } outputDone <- err }() return outputDone } func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) { inputDone := make(chan struct{}) detached := make(chan error) go func() { if h.inputStream != nil { _, err := io.Copy(h.resp, h.inputStream) // We should restore the terminal as soon as possible // once the connection ends so any following print // messages will be in normal type. restoreInput() logrus.Debug("[hijack] End of stdin") if _, ok := err.(term.EscapeError); ok { detached <- err return } if err != nil { // This error will also occur on the receive // side (from stdout) where it will be // propagated back to the caller. logrus.Debugf("Error sendStdin: %s", err) } } // if err := h.resp.CloseWrite(); err != nil { // logrus.Debugf("Couldn't send EOF: %s", err) // } close(inputDone) }() return inputDone, detached } func setRawTerminal(streams Streams) error { if err := streams.In().SetRawTerminal(); err != nil { return err } return streams.Out().SetRawTerminal() } func restoreTerminal(streams Streams, in io.Closer) error { streams.In().RestoreTerminal() streams.Out().RestoreTerminal() // WARNING: DO NOT REMOVE THE OS CHECKS !!! // For some reason this Close call blocks on darwin.. // As the client exits right after, simply discard the close // until we find a better solution. // // This can also cause the client on Windows to get stuck in Win32 CloseHandle() // in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442 // Tracked internally at Microsoft by VSO #11352156. In the // Windows case, you hit this if you are using the native/v2 console, // not the "legacy" console, and you start the client in a new window. eg // `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar` // will hang. Remove start, and it won't repro. if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" { return in.Close() } return nil } ================================================ FILE: mdz/pkg/cmd/ioutils/reader.go ================================================ package ioutils // import "github.com/docker/docker/pkg/ioutils" import ( "context" "io" // make sure crypto.SHA256, crypto.sha512 and crypto.SHA384 are registered // TODO remove once https://github.com/opencontainers/go-digest/pull/64 is merged. _ "crypto/sha256" _ "crypto/sha512" ) // ReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser // It calls the given callback function when closed. It should be constructed // with NewReadCloserWrapper type ReadCloserWrapper struct { io.Reader closer func() error } // Close calls back the passed closer function func (r *ReadCloserWrapper) Close() error { return r.closer() } // NewReadCloserWrapper returns a new io.ReadCloser. func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser { return &ReadCloserWrapper{ Reader: r, closer: closer, } } type readerErrWrapper struct { reader io.Reader closer func() } func (r *readerErrWrapper) Read(p []byte) (int, error) { n, err := r.reader.Read(p) if err != nil { r.closer() } return n, err } // NewReaderErrWrapper returns a new io.Reader. func NewReaderErrWrapper(r io.Reader, closer func()) io.Reader { return &readerErrWrapper{ reader: r, closer: closer, } } // OnEOFReader wraps an io.ReadCloser and a function // the function will run at the end of file or close the file. type OnEOFReader struct { Rc io.ReadCloser Fn func() } func (r *OnEOFReader) Read(p []byte) (n int, err error) { n, err = r.Rc.Read(p) if err == io.EOF { r.runFunc() } return } // Close closes the file and run the function. func (r *OnEOFReader) Close() error { err := r.Rc.Close() r.runFunc() return err } func (r *OnEOFReader) runFunc() { if fn := r.Fn; fn != nil { fn() r.Fn = nil } } // cancelReadCloser wraps an io.ReadCloser with a context for cancelling read // operations. type cancelReadCloser struct { cancel func() pR *io.PipeReader // Stream to read from pW *io.PipeWriter } // NewCancelReadCloser creates a wrapper that closes the ReadCloser when the // context is cancelled. The returned io.ReadCloser must be closed when it is // no longer needed. func NewCancelReadCloser(ctx context.Context, in io.ReadCloser) io.ReadCloser { pR, pW := io.Pipe() // Create a context used to signal when the pipe is closed doneCtx, cancel := context.WithCancel(context.Background()) p := &cancelReadCloser{ cancel: cancel, pR: pR, pW: pW, } go func() { _, err := io.Copy(pW, in) select { case <-ctx.Done(): // If the context was closed, p.closeWithError // was already called. Calling it again would // change the error that Read returns. default: p.closeWithError(err) } in.Close() }() go func() { for { select { case <-ctx.Done(): p.closeWithError(ctx.Err()) case <-doneCtx.Done(): return } } }() return p } // Read wraps the Read method of the pipe that provides data from the wrapped // ReadCloser. func (p *cancelReadCloser) Read(buf []byte) (n int, err error) { return p.pR.Read(buf) } // closeWithError closes the wrapper and its underlying reader. It will // cause future calls to Read to return err. func (p *cancelReadCloser) closeWithError(err error) { p.pW.CloseWithError(err) p.cancel() } // Close closes the wrapper its underlying reader. It will cause // future calls to Read to return io.EOF. func (p *cancelReadCloser) Close() error { p.closeWithError(io.EOF) return nil } ================================================ FILE: mdz/pkg/cmd/list.go ================================================ package cmd import ( "fmt" "sort" "strings" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/mdz/pkg/telemetry" ) const ( annotationDomain = "ai.tensorchord.domain" ) var ( // Used for flags. listQuiet bool listVerbose bool ) // listCommand represents the list command var listCommand = &cobra.Command{ Use: "list", Short: "List the deployments", Long: `List the deployments`, Example: ` mdz list mdz list -v mdz list -q`, GroupID: "basic", PreRunE: commandInit, RunE: commandList, } func init() { rootCmd.AddCommand(listCommand) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: listCommand.Flags().BoolVarP(&listQuiet, "quiet", "q", false, "Quiet mode - print out only the inference names") listCommand.Flags().BoolVarP(&listVerbose, "verbose", "v", false, "Verbose mode - print out all inference details") } func commandList(cmd *cobra.Command, args []string) error { telemetry.GetTelemetry().Record("list") infs, err := agentClient.InferenceList(cmd.Context(), namespace) if err != nil { cmd.PrintErrf("Failed to list inferences: %v\n", err) return err } sort.Sort(byName(infs)) if listQuiet { for _, inf := range infs { cmd.Printf("%s\n", inf.Spec.Name) } return nil } else if listVerbose { t := table.NewWriter() t.SetStyle(table.Style{ Box: table.StyleBoxDefault, Color: table.ColorOptionsDefault, Format: table.FormatOptionsDefault, HTML: table.DefaultHTMLOptions, Options: table.OptionsNoBordersAndSeparators, Title: table.TitleOptionsDefault, }) t.AppendHeader(table.Row{"Name", "Endpoint", "Image", "Status", "Invocations", "Replicas", "CreatedAt"}) for _, inf := range infs { functionImage := inf.Spec.Image createdAt := "" if inf.Status.CreatedAt != nil { createdAt = inf.Status.CreatedAt.String() } t.AppendRow(table.Row{ inf.Spec.Name, getEndpoint(inf), functionImage, inf.Status.Phase, int64(inf.Status.InvocationCount), fmt.Sprintf("%d/%d", inf.Status.AvailableReplicas, inf.Status.Replicas), createdAt, }) } cmd.Println(t.Render()) } else { t := table.NewWriter() t.SetStyle(table.Style{ Box: table.StyleBoxDefault, Color: table.ColorOptionsDefault, Format: table.FormatOptionsDefault, HTML: table.DefaultHTMLOptions, Options: table.OptionsNoBordersAndSeparators, Title: table.TitleOptionsDefault, }) t.AppendHeader(table.Row{"Name", "Endpoint", "Status", "Invocations", "Replicas"}) for _, inf := range infs { t.AppendRow(table.Row{ inf.Spec.Name, getEndpoint(inf), inf.Status.Phase, int64(inf.Status.InvocationCount), fmt.Sprintf("%d/%d", inf.Status.AvailableReplicas, inf.Status.Replicas), }) } cmd.Println(t.Render()) } return nil } type byName []types.InferenceDeployment func (a byName) Len() int { return len(a) } func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byName) Less(i, j int) bool { return a[i].Spec.Name < a[j].Spec.Name } func getEndpoint(inf types.InferenceDeployment) string { endpoint := fmt.Sprintf("%s/inference/%s.%s", mdzURL, inf.Spec.Name, inf.Spec.Namespace) if d, ok := inf.Spec.Annotations[annotationDomain]; ok { // Replace https with http now. rawHTTPDomain := strings.Replace(d, "https://", "http://", 1) endpoint = fmt.Sprintf("%s\n%s", rawHTTPDomain, endpoint) } return endpoint } ================================================ FILE: mdz/pkg/cmd/list_instance.go ================================================ package cmd import ( "sort" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/agent/api/types" ) var ( // Used for flags. listInstanceQuiet bool listInstanceVerbose bool ) // listInstanceCmd represents the list instance command var listInstanceCmd = &cobra.Command{ Use: "instance", Short: "List all instances for the given deployment", Long: `List all instances for the given deployment`, Example: ` mdz list instance bloomz-560m mdz list instance bloomz-560m -v mdz list instance bloomz-560m -q`, Args: cobra.ExactArgs(1), PreRunE: commandInit, RunE: commandListInstance, } func init() { listCommand.AddCommand(listInstanceCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: listInstanceCmd.Flags().BoolVarP(&listInstanceQuiet, "quiet", "q", false, "Quiet mode - print out only the instance names") listInstanceCmd.Flags().BoolVarP(&listInstanceVerbose, "verbose", "v", false, "Verbose mode - print out all instance details") } func commandListInstance(cmd *cobra.Command, args []string) error { instances, err := agentClient.InstanceList(cmd.Context(), namespace, args[0]) if err != nil { cmd.PrintErrf("Failed to list inference instances: %v\n", err) return err } sort.Sort(byInstanceName(instances)) if listInstanceQuiet { for _, i := range instances { cmd.Printf("%s\n", i.Spec.Name) } return nil } else if listInstanceVerbose { t := table.NewWriter() t.SetStyle(table.Style{ Box: table.StyleBoxDefault, Color: table.ColorOptionsDefault, Format: table.FormatOptionsDefault, HTML: table.DefaultHTMLOptions, Options: table.OptionsNoBordersAndSeparators, Title: table.TitleOptionsDefault, }) t.AppendHeader(table.Row{"Name", "Status", "Reason", "Message", "CreatedAt"}) for _, i := range instances { t.AppendRow(table.Row{i.Spec.Name, i.Status.Phase, i.Status.Reason, i.Status.Message, i.Status.StartTime}) } cmd.Println(t.Render()) return nil } else { t := table.NewWriter() t.SetStyle(table.Style{ Box: table.StyleBoxDefault, Color: table.ColorOptionsDefault, Format: table.FormatOptionsDefault, HTML: table.DefaultHTMLOptions, Options: table.OptionsNoBordersAndSeparators, Title: table.TitleOptionsDefault, }) t.AppendHeader(table.Row{"Name", "Status", "CreatedAt"}) for _, i := range instances { t.AppendRow(table.Row{i.Spec.Name, i.Status.Phase, i.Status.StartTime}) } cmd.Println(t.Render()) return nil } } type byInstanceName []types.InferenceDeploymentInstance func (a byInstanceName) Len() int { return len(a) } func (a byInstanceName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byInstanceName) Less(i, j int) bool { return a[i].Spec.Name < a[j].Spec.Name } ================================================ FILE: mdz/pkg/cmd/localagent.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/mdz/pkg/agentd/server" ) var ( localAgentPort int ) // localAgentCmd represents the local-agent command var localAgentCmd = &cobra.Command{ Use: "local-agent", Short: "Start agent with local docker runtime", Long: `Start agent with local docker runtime`, Example: ` mdz local-agent`, GroupID: "basic", PreRunE: commandInit, RunE: commandLocalAgent, Hidden: true, } func init() { rootCmd.AddCommand(localAgentCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: localAgentCmd.Flags().IntVarP(&localAgentPort, "port", "p", 31112, "Port to listen on") } func commandLocalAgent(cmd *cobra.Command, args []string) error { server, err := server.New() if err != nil { return err } return server.Run(localAgentPort) } ================================================ FILE: mdz/pkg/cmd/logs.go ================================================ package cmd import ( "github.com/spf13/cobra" ) var ( // Used for flags. tail int since string end string follow bool ) // logCmd represents the log command var logsCmd = &cobra.Command{ Use: "logs", Short: "Print the logs for a deployment", Long: `Print the logs for a deployment`, Example: ` mdz logs blomdz-560m`, GroupID: "debug", PreRunE: commandInit, Args: cobra.ExactArgs(1), RunE: commandLogs, } func init() { rootCmd.AddCommand(logsCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: logsCmd.Flags().IntVarP(&tail, "tail", "t", 0, "Number of lines to show from the end of the logs") logsCmd.Flags().StringVarP(&since, "since", "s", "2006-01-02T15:04:05Z", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") logsCmd.Flags().StringVarP(&end, "end", "e", "", "Only return logs before this timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") logsCmd.Flags().BoolVarP(&follow, "follow", "f", false, "Follow log output") } func commandLogs(cmd *cobra.Command, args []string) error { logStream, err := agentClient.DeploymentLogGet(cmd.Context(), namespace, args[0], since, tail, end, follow) if err != nil { cmd.PrintErrf("Failed to get logs: %s\n", err) return err } for log := range logStream { cmd.Printf("%s: %s\n", log.Instance, log.Text) } return nil } ================================================ FILE: mdz/pkg/cmd/portforward.go ================================================ package cmd import ( "fmt" "net/http" "net/http/httputil" "net/url" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // portForwardCmd represents the port-forward command var portForwardCmd = &cobra.Command{ Use: "port-forward", Short: "Forward one local port to a deployment", Long: `Forward one local port to a deployment`, Example: ` mdz port-forward blomdz-560m 7860`, GroupID: "debug", PreRunE: commandInit, Args: cobra.ExactArgs(2), RunE: commandForward, } func init() { rootCmd.AddCommand(portForwardCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: } func commandForward(cmd *cobra.Command, args []string) error { name := args[0] port := args[1] if _, err := agentClient.InferenceGet(cmd.Context(), namespace, name); err != nil { cmd.PrintErrf("Failed to get inference: %s\n", errors.Cause(err)) return err } url, err := url.Parse(fmt.Sprintf("%s/inference/%s.%s", mdzURL, name, namespace)) if err != nil { cmd.PrintErrf("Failed to parse URL: %s\n", errors.Cause(err)) return errors.Newf("failed to parse URL: %s\n", errors.Cause(err)) } rp := httputil.NewSingleHostReverseProxy(url) cmd.Printf("Forwarding inference %s to local port %s\n", name, port) logrus.WithField("url", url).Debugf( "Forwarding inference %s to local port %s\n", name, port) handler := func(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { cmd.Printf("Handling connection for %s\n", port) p.ServeHTTP(w, r) } } http.HandleFunc("/", handler(rp)) err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) if err != nil { cmd.PrintErrf("Failed to listen and serve: %s\n", errors.Cause(err)) return errors.Newf("failed to listen and serve: %s", errors.Cause(err)) } return nil } ================================================ FILE: mdz/pkg/cmd/root.go ================================================ package cmd import ( "os" "github.com/cockroachdb/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/tensorchord/openmodelz/agent/client" "github.com/tensorchord/openmodelz/mdz/pkg/telemetry" ) var ( // Used for flags. mdzURL string namespace string debug bool disableTelemetry bool agentClient *client.Client ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "mdz", Short: "mdz manages your deployments", Long: `mdz helps you deploy applications, manage servers, and troubleshoot issues.`, Example: ` mdz server start mdz deploy --image modelzai/llm-bloomz-560m:23.06.13 --name llm mdz list mdz logs llm mdz port-forward llm 7860 mdz exec llm ps mdz exec llm --tty bash mdz delete llm `, SilenceUsage: true, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { // Here you will define your flags and configuration settings. // Cobra supports persistent flags, which, if defined here, // will be global for your application. // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mdz.yaml)") rootCmd.PersistentFlags().StringVarP(&mdzURL, "url", "u", "", "URL to use for the server (MDZ_URL) (default http://localhost:80)") rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "Namespace to use for OpenModelZ inferences") rootCmd.PersistentFlags().MarkHidden("namespace") rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "Enable debug logging") rootCmd.PersistentFlags().BoolVarP(&disableTelemetry, "disable-telemetry", "", false, "Disable anonymous telemetry") // Cobra also supports local flags, which will only run // when this action is called directly. rootCmd.AddGroup(&cobra.Group{ID: "basic", Title: "Basic Commands:"}) rootCmd.AddGroup(&cobra.Group{ID: "debug", Title: "Troubleshooting and Debugging Commands:"}) rootCmd.AddGroup(&cobra.Group{ID: "management", Title: "Management Commands:"}) // telemetry if err := telemetry.Initialize(!disableTelemetry); err != nil { logrus.WithError(err).Debug("Failed to initialize telemetry") } } func commandInit(cmd *cobra.Command, args []string) error { if err := commandInitLog(cmd, args); err != nil { return err } if agentClient == nil { if mdzURL == "" { // Checkout environment variable MDZ_URL. mdzURL = os.Getenv("MDZ_URL") } if mdzURL == "" { mdzURL = "http://localhost:80" } var err error agentClient, err = client.NewClientWithOpts(client.WithHost(mdzURL)) if err != nil { cmd.PrintErrf("Failed to connect to agent: %s\n", errors.Cause(err)) return err } } return nil } func commandInitLog(cmd *cobra.Command, args []string) error { if debug { logrus.SetLevel(logrus.DebugLevel) logrus.Debug("Debug logging enabled") logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) } return nil } func GenMarkdownTree(dir string) error { return doc.GenMarkdownTree(rootCmd, dir) } ================================================ FILE: mdz/pkg/cmd/scale.go ================================================ package cmd import ( "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/mdz/pkg/telemetry" ) var ( // Used for flags. replicas int32 min int32 max int32 targetInflightRequests int32 ) // scaleCmd represents the scale command var scaleCmd = &cobra.Command{ Use: "scale", Short: "Scale a deployment", Long: `Scale a deployment`, Example: ` mdz scale bloomz-560m --replicas 3`, GroupID: "basic", PreRunE: commandInit, Args: cobra.ExactArgs(1), RunE: commandScale, } func init() { rootCmd.AddCommand(scaleCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: scaleCmd.Flags().Int32VarP(&replicas, "replicas", "r", 0, "Number of replicas to scale to") scaleCmd.Flags().Int32VarP(&min, "min", "m", 0, "Minimum number of replicas to scale to") scaleCmd.Flags().Int32VarP(&max, "max", "x", 0, "Maximum number of replicas to scale to") scaleCmd.Flags().Int32VarP(&targetInflightRequests, "target-inflight-requests", "t", 0, "Target number of inflight requests per replica") scaleCmd.MarkFlagRequired("replicas") scaleCmd.Flags().MarkHidden("min") scaleCmd.Flags().MarkHidden("max") scaleCmd.Flags().MarkHidden("target-inflight-requests") } func commandScale(cmd *cobra.Command, args []string) error { name := args[0] deployment, err := agentClient.InferenceGet(cmd.Context(), namespace, name) if err != nil { cmd.PrintErrf("Failed to get deployment: %s\n", err) return err } if replicas != 0 { deployment.Spec.Scaling.MinReplicas = int32Ptr(replicas) deployment.Spec.Scaling.MaxReplicas = int32Ptr(replicas) if _, err := agentClient.DeploymentUpdate(cmd.Context(), namespace, deployment); err != nil { cmd.PrintErrf("Failed to update deployment: %s\n", err) return err } return nil } if min != 0 { deployment.Spec.Scaling.MinReplicas = int32Ptr(min) } if max != 0 { deployment.Spec.Scaling.MaxReplicas = int32Ptr(max) } if targetInflightRequests != 0 { deployment.Spec.Scaling.TargetLoad = int32Ptr(targetInflightRequests) } telemetry.GetTelemetry().Record("scale") if _, err := agentClient.DeploymentUpdate(cmd.Context(), namespace, deployment); err != nil { cmd.PrintErrf("Failed to update deployment: %s\n", err) return err } return nil } ================================================ FILE: mdz/pkg/cmd/server.go ================================================ package cmd import ( "time" "github.com/spf13/cobra" ) var ( serverVerbose bool serverPollingInterval time.Duration = 3 * time.Second serverRegistryMirrorName string serverRegistryMirrorEndpoints []string ) // serverCmd represents the server command var serverCmd = &cobra.Command{ Use: "server", Short: "Manage the servers", Long: `Manage the servers`, Example: ` mdz server start`, GroupID: "management", PreRunE: commandInitLog, } func init() { rootCmd.AddCommand(serverCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: serverCmd.PersistentFlags().BoolVarP(&serverVerbose, "verbose", "v", false, "Verbose output") // Cobra supports local flags which will only run when this command // is called directly, e.g.: } ================================================ FILE: mdz/pkg/cmd/server_delete.go ================================================ package cmd import "github.com/spf13/cobra" var serverDeleteCmd = &cobra.Command{ Use: "delete", Short: "Delete a node from the cluster", Long: `Delete a node from the cluster`, Example: ` mdz server delete gpu-node-1`, PreRunE: commandInit, Args: cobra.MinimumNArgs(1), RunE: commandServerDelete, } func init() { serverCmd.AddCommand(serverDeleteCmd) } func commandServerDelete(cmd *cobra.Command, args []string) error { nodeName := args[0] if err := agentClient.ServerNodeDelete(cmd.Context(), nodeName); err != nil { return err } return nil } ================================================ FILE: mdz/pkg/cmd/server_destroy.go ================================================ package cmd import ( "github.com/cockroachdb/errors" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/mdz/pkg/server" ) // serverDestroyCmd represents the server destroy command var serverDestroyCmd = &cobra.Command{ Use: "destroy", Short: "Destroy the cluster", Long: `Destroy the cluster`, Example: ` mdz server destroy`, PreRunE: commandInitLog, RunE: commandServerDestroy, } func init() { serverCmd.AddCommand(serverDestroyCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: } func commandServerDestroy(cmd *cobra.Command, args []string) error { engine, err := server.NewDestroy(server.Options{ Verbose: serverVerbose, OutputStream: cmd.ErrOrStderr(), RetryInternal: serverPollingInterval, }) if err != nil { cmd.PrintErrf("Failed to destroy the server: %s\n", errors.Cause(err)) return err } _, err = engine.Run() if err != nil { cmd.PrintErrf("Failed to destroy the server: %s\n", errors.Cause(err)) return err } cmd.Printf("✅ Server destroyed\n") return nil } ================================================ FILE: mdz/pkg/cmd/server_join.go ================================================ package cmd import ( "github.com/cockroachdb/errors" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/mdz/pkg/server" "github.com/tensorchord/openmodelz/mdz/pkg/telemetry" ) // serverJoinCmd represents the server join command var serverJoinCmd = &cobra.Command{ Use: "join", Short: "Join to the cluster", Long: `Join to the cluster`, Example: ` mdz server join 192.168.31.192`, PreRunE: commandInitLog, Args: cobra.ExactArgs(1), RunE: commandServerJoin, } func init() { serverCmd.AddCommand(serverJoinCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: serverJoinCmd.Flags().StringVarP(&serverRegistryMirrorName, "mirror-name", "", "docker.io", "Mirror domain name of the registry") serverJoinCmd.Flags().StringArrayVarP(&serverRegistryMirrorEndpoints, "mirror-endpoints", "", []string{}, "Mirror URL endpoints of the registry like `https://quay.io`") } func commandServerJoin(cmd *cobra.Command, args []string) error { engine, err := server.NewJoin(server.Options{ Verbose: serverVerbose, OutputStream: cmd.ErrOrStderr(), RetryInternal: serverPollingInterval, ServerIP: args[0], Mirror: server.Mirror{ Name: serverRegistryMirrorName, Endpoints: serverRegistryMirrorEndpoints, }, }) if err != nil { cmd.PrintErrf("Failed to configure before join: %s\n", errors.Cause(err)) return err } telemetry.GetTelemetry().Record("server join") _, err = engine.Run() if err != nil { cmd.PrintErrf("Failed to join the cluster: %s\n", errors.Cause(err)) return err } cmd.Printf("✅ Server joined\n") return nil } ================================================ FILE: mdz/pkg/cmd/server_label.go ================================================ package cmd import ( "fmt" "strings" "github.com/spf13/cobra" ) // serverLabelCmd represents the server label command var serverLabelCmd = &cobra.Command{ Use: "label", Short: "Update the labels on a server", Long: `Update the labels on a server * A label key and value must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to 63 characters each. * Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app. `, Example: ` mdz server label node-name key=value [key=value...]`, PreRunE: commandInit, Args: cobra.MinimumNArgs(1), RunE: commandServerLabel, } func init() { serverCmd.AddCommand(serverLabelCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: } func commandServerLabel(cmd *cobra.Command, args []string) error { nodeName := args[0] labels := args[1:] nodeLabels, err := parseNodeLabels(labels) if err != nil { return err } if err := agentClient.ServerLabelCreate(cmd.Context(), nodeName, nodeLabels); err != nil { return err } return nil } func parseNodeLabels(labels []string) (map[string]string, error) { res := make(map[string]string) for _, label := range labels { if !strings.Contains(label, "=") { return nil, fmt.Errorf("label must be in the form of key=value") } // Split the label into key and value parts := strings.SplitN(label, "=", 2) key := parts[0] value := parts[1] if len(key) == 0 { return nil, fmt.Errorf("label key cannot be empty") } res[key] = value } return res, nil } ================================================ FILE: mdz/pkg/cmd/server_list.go ================================================ package cmd import ( "fmt" "math" "github.com/cockroachdb/errors" "github.com/jedib0t/go-pretty/v6/table" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/resource" "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/mdz/pkg/telemetry" ) var ( // Used for flags. serverListQuiet bool serverListVerbose bool ) // serverListCmd represents the server list command var serverListCmd = &cobra.Command{ Use: "list", Short: "List all servers in the cluster", Long: `List all servers in the cluster`, Example: ` mdz server list`, PreRunE: commandInit, RunE: commandServerList, } func init() { serverCmd.AddCommand(serverListCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: serverListCmd.Flags().BoolVarP(&serverListQuiet, "quiet", "q", false, "Quiet mode - print out only the server names") serverListCmd.Flags().BoolVarP(&serverListVerbose, "verbose", "v", false, "Verbose mode - print out all server details") } func commandServerList(cmd *cobra.Command, args []string) error { telemetry.GetTelemetry().Record("server list") servers, err := agentClient.ServerList(cmd.Context()) if err != nil { cmd.PrintErrf("Failed to list servers: %s\n", errors.Cause(err)) return err } if serverListQuiet { for _, server := range servers { cmd.Printf("%s\n", server.Spec.Name) } } else if serverListVerbose { t := table.NewWriter() t.SetStyle(table.Style{ Box: table.StyleBoxDefault, Color: table.ColorOptionsDefault, Format: table.FormatOptionsDefault, HTML: table.DefaultHTMLOptions, Options: table.OptionsNoBordersAndSeparators, Title: table.TitleOptionsDefault, }) t.AppendHeader(table.Row{"Name", "Phase", "Allocatable", "Capacity", "Distribution", "OS", "Kernel", "Labels"}) for _, server := range servers { t.AppendRow(table.Row{server.Spec.Name, server.Status.Phase, resourceListString(server.Status.Allocatable), resourceListString(server.Status.Capacity), server.Status.System.OSImage, server.Status.System.OperatingSystem, server.Status.System.KernelVersion, labelsString(server.Spec.Labels), }) } cmd.Println(t.Render()) } else { t := table.NewWriter() t.SetStyle(table.Style{ Box: table.StyleBoxDefault, Color: table.ColorOptionsDefault, Format: table.FormatOptionsDefault, HTML: table.DefaultHTMLOptions, Options: table.OptionsNoBordersAndSeparators, Title: table.TitleOptionsDefault, }) t.AppendHeader(table.Row{"Name", "Phase", "Allocatable", "Capacity"}) for _, server := range servers { t.AppendRow(table.Row{server.Spec.Name, server.Status.Phase, resourceListString(server.Status.Allocatable), resourceListString(server.Status.Capacity)}) } cmd.Println(t.Render()) } return nil } func labelsString(labels map[string]string) string { res := "" for k, v := range labels { res += fmt.Sprintf("%s=%s\n", k, v) } if len(res) == 0 { return res } return res[:len(res)-1] } func prettyByteSize(quantity string) (string, error) { r, err := resource.ParseQuantity(quantity) if err != nil { return "", err } bf := float64(r.Value()) for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti"} { if math.Abs(bf) < 1024.0 { return fmt.Sprintf("%3.1f%sB", bf, unit), nil } bf /= 1024.0 } return fmt.Sprintf("%.1fPiB", bf), nil } func resourceListString(l types.ResourceList) string { res := fmt.Sprintf("cpu: %s", l[types.ResourceCPU]) memory, ok := l[types.ResourceMemory] if ok { prettyMem, err := prettyByteSize(string(memory)) if err != nil { logrus.Infof("failed to parse the memory quantity: %s", memory) } else { memory = types.Quantity(prettyMem) } } res += fmt.Sprintf("\nmemory: %s", memory) if l[types.ResourceGPU] != "" { res += fmt.Sprintf("\ngpu: %s", l[types.ResourceGPU]) } return res } ================================================ FILE: mdz/pkg/cmd/server_start.go ================================================ package cmd import ( "fmt" "time" "github.com/cockroachdb/errors" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/agent/pkg/consts" "github.com/tensorchord/openmodelz/mdz/pkg/server" "github.com/tensorchord/openmodelz/mdz/pkg/telemetry" "github.com/tensorchord/openmodelz/mdz/pkg/version" ) var ( serverStartRuntime string serverStartDomain string = consts.Domain serverStartVersion string serverStartWithGPU bool enableModelZCloud bool modelzCloudUrl string modelzCloudAgentToken string modelzCloudRegion string ) // serverStartCmd represents the server start command var serverStartCmd = &cobra.Command{ Use: "start", Short: "Start the server", Long: `Start the server with the public IP of the machine. If not provided, the internal IP will be used automatically.`, Example: ` mdz server start mdz server start -v mdz server start 1.2.3.4`, PreRunE: preRunE, Args: cobra.RangeArgs(0, 1), RunE: commandServerStart, } func init() { serverCmd.AddCommand(serverStartCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: // serverStartCmd.Flags().StringVarP(&serverStartRuntime, "runtime", "r", "k3s", "Runtime to use (k3s, docker) in the started server") serverStartCmd.Flags().StringVarP(&serverStartVersion, "version", "", version.HelmChartVersion, "Version of the server to start") serverStartCmd.Flags().MarkHidden("version") serverStartCmd.Flags().BoolVarP(&serverStartWithGPU, "force-gpu", "g", false, "Start the server with GPU support (ignore the GPU detection)") serverStartCmd.Flags().StringVarP(&serverRegistryMirrorName, "mirror-name", "", "docker.io", "Mirror domain name of the registry") serverStartCmd.Flags().StringArrayVarP(&serverRegistryMirrorEndpoints, "mirror-endpoints", "", []string{}, "Mirror URL endpoints of the registry like `https://quay.io`") serverStartCmd.Flags().BoolVarP(&enableModelZCloud, "modelzcloud-enabled", "", false, "Enable ModelZ Cloud Management") serverStartCmd.Flags().StringVarP(&modelzCloudUrl, "modelzcloud-url", "", "https://cloud.modelz.ai", "ModelZ Cloud URL") serverStartCmd.Flags().StringVarP(&modelzCloudAgentToken, "modelzcloud-agent-token", "", "", "ModelZ Cloud Agent Token") serverStartCmd.Flags().StringVarP(&modelzCloudRegion, "modelzcloud-region", "", "on-premises", "ModelZ Cloud Region") } func preRunE(cmd *cobra.Command, args []string) error { err := commandInitLog(cmd, args) if err != nil { return err } // If enabled modelzcloud control plane, you need make configuration if enableModelZCloud { if modelzCloudUrl == "" || modelzCloudAgentToken == "" || modelzCloudRegion == "" { return fmt.Errorf("modelzcloud configuration is not complete") } } return nil } func commandServerStart(cmd *cobra.Command, args []string) error { var domain *string if len(args) > 0 { domainWithSuffix := fmt.Sprintf("%s.%s", args[0], serverStartDomain) domain = &domainWithSuffix } defer func(start time.Time) { telemetry.GetTelemetry().Record( "server start", telemetry.AddField("duration", time.Since(start).Seconds()), ) }(time.Now()) engine, err := server.NewStart(server.Options{ Verbose: serverVerbose, Runtime: server.Runtime(serverStartRuntime), OutputStream: cmd.ErrOrStderr(), RetryInternal: serverPollingInterval, Domain: domain, Version: serverStartVersion, ForceGPU: serverStartWithGPU, Mirror: server.Mirror{ Name: serverRegistryMirrorName, Endpoints: serverRegistryMirrorEndpoints, }, ModelZCloud: server.ModelZCloud{ Enabled: enableModelZCloud, URL: modelzCloudUrl, Token: modelzCloudAgentToken, Region: modelzCloudRegion, }, }) if err != nil { cmd.PrintErrf("Failed to start the server: %s\n", errors.Cause(err)) return err } result, err := engine.Run() if err != nil { cmd.PrintErrf("Failed to start the server: %s\n", errors.Cause(err)) return err } mdzURL = result.MDZURL if err := commandInit(cmd, args); err != nil { cmd.PrintErrf("Failed to start the server: %s\n", errors.Cause(err)) return err } cmd.Printf("🐋 Checking if the server is running...\n") // Retry until verify success. ticker := time.NewTicker(serverPollingInterval) for range ticker.C { if err := printServerVersion(cmd); err != nil { cmd.Printf("🐋 The server is not ready yet, retrying...\n") continue } break } cmd.Printf("🐳 The server is running at %s\n", mdzURL) cmd.Printf("🎉 You could set the environment variable to get started!\n\n") cmd.Printf("export MDZ_URL=%s\n", mdzURL) return nil } ================================================ FILE: mdz/pkg/cmd/server_stop.go ================================================ package cmd import ( "github.com/cockroachdb/errors" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/mdz/pkg/server" ) // serverStopCmd represents the server stop command var serverStopCmd = &cobra.Command{ Use: "stop", Short: "Stop the server", Long: `Stop the server`, Example: ` mdz server stop`, PreRunE: commandInitLog, RunE: commandServerStop, } func init() { serverCmd.AddCommand(serverStopCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: } func commandServerStop(cmd *cobra.Command, args []string) error { engine, err := server.NewStop(server.Options{ Verbose: serverVerbose, OutputStream: cmd.ErrOrStderr(), RetryInternal: serverPollingInterval, }) if err != nil { cmd.PrintErrf("Failed to stop the server: %s\n", errors.Cause(err)) return err } _, err = engine.Run() if err != nil { cmd.PrintErrf("Failed to stop the server: %s\n", errors.Cause(err)) return err } cmd.Printf("✅ Server stopped\n") return nil } ================================================ FILE: mdz/pkg/cmd/streams/in.go ================================================ package streams import ( "errors" "io" "os" "runtime" "github.com/moby/term" ) // In is an input stream to read user input. It implements [io.ReadCloser] // with additional utilities, such as putting the terminal in raw mode. type In struct { commonStream in io.ReadCloser } // Read implements the [io.Reader] interface. func (i *In) Read(p []byte) (int, error) { return i.in.Read(p) } // Close implements the [io.Closer] interface. func (i *In) Close() error { return i.in.Close() } // SetRawTerminal sets raw mode on the input terminal. It is a no-op if In // is not a TTY, or if the "NORAW" environment variable is set to a non-empty // value. func (i *In) SetRawTerminal() (err error) { if !i.isTerminal || os.Getenv("NORAW") != "" { return nil } i.state, err = term.SetRawTerminal(i.fd) return err } // CheckTty checks if we are trying to attach to a container TTY // from a non-TTY client input stream, and if so, returns an error. func (i *In) CheckTty(attachStdin, ttyMode bool) error { // In order to attach to a container tty, input stream for the client must // be a tty itself: redirecting or piping the client standard input is // incompatible with `docker run -t`, `docker exec -t` or `docker attach`. if ttyMode && attachStdin && !i.isTerminal { const eText = "the input device is not a TTY" if runtime.GOOS == "windows" { return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'") } return errors.New(eText) } return nil } // NewIn returns a new [In] from an [io.ReadCloser]. func NewIn(in io.ReadCloser) *In { i := &In{in: in} i.fd, i.isTerminal = term.GetFdInfo(in) return i } ================================================ FILE: mdz/pkg/cmd/streams/out.go ================================================ package streams import ( "io" "os" "github.com/moby/term" "github.com/sirupsen/logrus" ) // Out is an output stream to write normal program output. It implements // an [io.Writer], with additional utilities for detecting whether a terminal // is connected, getting the TTY size, and putting the terminal in raw mode. type Out struct { commonStream out io.Writer } func (o *Out) Write(p []byte) (int, error) { return o.out.Write(p) } // SetRawTerminal puts the output of the terminal connected to the stream // into raw mode. // // On UNIX, this does nothing. On Windows, it disables LF -> CRLF/ translation. // It is a no-op if Out is not a TTY, or if the "NORAW" environment variable is // set to a non-empty value. func (o *Out) SetRawTerminal() (err error) { if !o.isTerminal || os.Getenv("NORAW") != "" { return nil } o.state, err = term.SetRawTerminalOutput(o.fd) return err } // GetTtySize returns the height and width in characters of the TTY, or // zero for both if no TTY is connected. func (o *Out) GetTtySize() (height uint, width uint) { if !o.isTerminal { return 0, 0 } ws, err := term.GetWinsize(o.fd) if err != nil { logrus.WithError(err).Debug("Error getting TTY size") if ws == nil { return 0, 0 } } return uint(ws.Height), uint(ws.Width) } // NewOut returns a new [Out] from an [io.Writer]. func NewOut(out io.Writer) *Out { o := &Out{out: out} o.fd, o.isTerminal = term.GetFdInfo(out) return o } ================================================ FILE: mdz/pkg/cmd/streams/stream.go ================================================ package streams import ( "github.com/moby/term" ) type commonStream struct { fd uintptr isTerminal bool state *term.State } // FD returns the file descriptor number for this stream. func (s *commonStream) FD() uintptr { return s.fd } // IsTerminal returns true if this stream is connected to a terminal. func (s *commonStream) IsTerminal() bool { return s.isTerminal } // RestoreTerminal restores normal mode to the terminal. func (s *commonStream) RestoreTerminal() { if s.state != nil { _ = term.RestoreTerminal(s.fd, s.state) } } // SetIsTerminal overrides whether a terminal is connected. It is used to // override this property in unit-tests, and should not be depended on for // other purposes. func (s *commonStream) SetIsTerminal(isTerminal bool) { s.isTerminal = isTerminal } ================================================ FILE: mdz/pkg/cmd/version.go ================================================ package cmd import ( "github.com/cockroachdb/errors" "github.com/spf13/cobra" "github.com/tensorchord/openmodelz/mdz/pkg/version" ) // versionCmd represents the versionCmd var versionCmd = &cobra.Command{ Use: "version", Short: "Print the client and agent version information", Long: `Print the client and server version information`, Example: ` mdz version`, PreRunE: commandInit, RunE: commandVersion, } func init() { rootCmd.AddCommand(versionCmd) // Here you will define your flags and configuration settings. // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // Cobra supports local flags which will only run when this command // is called directly, e.g.: } func commandVersion(cmd *cobra.Command, args []string) error { v := version.GetVersion() cmd.Println("Client:") cmd.Printf(" Version: \t%s\n", v.Version) cmd.Printf(" Build Date: \t%s\n", v.BuildDate) cmd.Printf(" Git Commit: \t%s\n", v.GitCommit) cmd.Printf(" Git State: \t%s\n", v.GitTreeState) cmd.Printf(" Go Version: \t%s\n", v.GoVersion) cmd.Printf(" Compiler: \t%s\n", v.Compiler) cmd.Printf(" Platform: \t%s\n", v.Platform) if err := printServerVersion(cmd); err != nil { cmd.PrintErrf("Failed to get server version: %v\n", errors.Cause(err)) return err } return nil } func printServerVersion(cmd *cobra.Command) error { info, err := agentClient.InfoGet(cmd.Context()) if err != nil { return err } cmd.Println("Server:") cmd.Printf(" Version: \t%s\n", info.Version.Version) cmd.Printf(" Build Date: \t%s\n", info.Version.BuildDate) cmd.Printf(" Git Commit: \t%s\n", info.Version.GitCommit) cmd.Printf(" Git State: \t%s\n", info.Version.GitTreeState) cmd.Printf(" Go Version: \t%s\n", info.Version.GoVersion) cmd.Printf(" Compiler: \t%s\n", info.Version.Compiler) cmd.Printf(" Platform: \t%s\n", info.Version.Platform) return nil } ================================================ FILE: mdz/pkg/server/agentd_run.go ================================================ package server import ( "fmt" "os/exec" "syscall" ) type agentDRunStep struct { options Options } // TODO(gaocegege): There is still a bug, thus it cannot be used actually. // The process will exit after the command returns. We need to put it in systemd. func (s *agentDRunStep) Run() error { fmt.Fprintf(s.options.OutputStream, "🚧 Running the agent for docker runtime...\n") cmd := exec.Command("/bin/sh", "-c", "mdz local-agent &") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } err := cmd.Run() if err != nil { return err } return nil } func (s *agentDRunStep) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/engine.go ================================================ package server import ( "fmt" "io" "time" ) const ( AgentPort = 31112 ) type Options struct { Verbose bool OutputStream io.Writer Runtime Runtime Mirror Mirror RetryInternal time.Duration ServerIP string Domain *string Version string ForceGPU bool ModelZCloud ModelZCloud } type ModelZCloud struct { Enabled bool URL string Token string Region string } type Mirror struct { Name string Endpoints []string } func (m *Mirror) Configured() bool { return m.Name != "" && len(m.Endpoints) > 0 } type Runtime string var ( RuntimeK3s Runtime = "k3s" RuntimeDocker Runtime = "docker" ) type Engine struct { options Options Steps []Step } type Result struct { MDZURL string } func NewStart(o Options) (*Engine, error) { if o.Verbose { fmt.Fprintf(o.OutputStream, "Starting the server with config: %+v\n", o) } var engine *Engine switch o.Runtime { case RuntimeDocker: engine = &Engine{ options: o, Steps: []Step{ &agentDRunStep{ options: o, }, }, } default: engine = &Engine{ options: o, Steps: []Step{ // Install k3s and related tools. &k3sPrepare{ options: o, }, &k3sInstallStep{ options: o, }, &nginxInstallStep{ options: o, }, &gpuInstallStep{ options: o, }, &openModelZInstallStep{ options: o, }, }, } } return engine, nil } func NewStop(o Options) (*Engine, error) { return &Engine{ options: o, Steps: []Step{ // Kill all k3s and related tools. &k3sKillAllStep{ options: o, }, }, }, nil } func NewDestroy(o Options) (*Engine, error) { return &Engine{ options: o, Steps: []Step{ // Destroy all k3s and related tools. &k3sDestroyAllStep{ options: o, }, }, }, nil } func NewJoin(o Options) (*Engine, error) { return &Engine{ options: o, Steps: []Step{ &k3sJoinStep{ options: o, }, }, }, nil } type Step interface { Run() error Verify() error } func (e *Engine) Run() (*Result, error) { for _, step := range e.Steps { if err := step.Run(); err != nil { return nil, err } // Retry until verify success. ticker := time.NewTicker(e.options.RetryInternal) for range ticker.C { if err := step.Verify(); err == nil { ticker.Stop() break } } } if e.options.Domain != nil { return &Result{ MDZURL: fmt.Sprintf("http://%s", *e.options.Domain), }, nil } // Get the server IP. if resultDomain != "" { return &Result{ MDZURL: fmt.Sprintf("http://%s", resultDomain), }, nil } return &Result{ MDZURL: fmt.Sprintf("http://0.0.0.0:%d", AgentPort), }, nil } ================================================ FILE: mdz/pkg/server/gpu-resource.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: gpu-operator --- apiVersion: helm.cattle.io/v1 kind: HelmChart metadata: name: nvidia namespace: gpu-operator spec: chart: gpu-operator repo: https://helm.ngc.nvidia.com/nvidia targetNamespace: gpu-operator set: valuesContent: |- toolkit: env: - name: CONTAINERD_CONFIG value: /var/lib/rancher/k3s/agent/etc/containerd/config.toml - name: CONTAINERD_SOCKET value: /run/k3s/containerd/containerd.sock ================================================ FILE: mdz/pkg/server/gpu_install.go ================================================ package server import ( _ "embed" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "syscall" ) //go:embed gpu-resource.yaml var gpuYamlContent string // gpuInstallStep installs the GPU related resources. type gpuInstallStep struct { options Options } // check if the Nvidia Toolkit is installed on the host func (s *gpuInstallStep) hasNvidiaToolkit() bool { locations := []string{ "/usr/local/nvidia/toolkit", "/usr/bin", } binaryNames := []string{ "nvidia-container-runtime", "nvidia-container-runtime-experimental", } for _, location := range locations { for _, name := range binaryNames { path := filepath.Join(location, name) if _, err := os.Stat(path); err == nil { return true } } } return false } func (s *gpuInstallStep) hasNvidiaDevice() bool { output, err := exec.Command("/bin/sh", "-c", "lspci").Output() if err != nil { return false } regexNvidia := regexp.MustCompile("(?i)nvidia") return regexNvidia.Match(output) } func (s *gpuInstallStep) Run() error { if !s.options.ForceGPU { // detect GPU if !(s.hasNvidiaDevice() || s.hasNvidiaToolkit()) { fmt.Fprintf(s.options.OutputStream, "🚧 Nvidia Toolkit is missing, skip the GPU initialization.\n") return nil } } fmt.Fprintf(s.options.OutputStream, "🚧 Initializing the GPU resource...\n") cmd := exec.Command("/bin/sh", "-c", "sudo k3s kubectl apply -f -") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } stdin, err := cmd.StdinPipe() if err != nil { return err } if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } if err := cmd.Start(); err != nil { return err } if _, err := io.WriteString(stdin, gpuYamlContent); err != nil { return err } // Close the input stream to finish the pipe. Then the command will use the // input from the pipe to start the next process. stdin.Close() if err := cmd.Wait(); err != nil { return err } return nil } func (s *gpuInstallStep) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/k3s-install.sh ================================================ #!/bin/sh set -e set -o noglob # Usage: # curl ... | ENV_VAR=... sh - # or # ENV_VAR=... ./install.sh # # Example: # Installing a server without traefik: # curl ... | INSTALL_K3S_EXEC="--disable=traefik" sh - # Installing an agent to point at a server: # curl ... | K3S_TOKEN=xxx K3S_URL=https://server-url:6443 sh - # # Environment variables: # - K3S_* # Environment variables which begin with K3S_ will be preserved for the # systemd service to use. Setting K3S_URL without explicitly setting # a systemd exec command will default the command to "agent", and we # enforce that K3S_TOKEN is also set. # # - INSTALL_K3S_SKIP_DOWNLOAD # If set to true will not download k3s hash or binary. # # - INSTALL_K3S_FORCE_RESTART # If set to true will always restart the K3s service # # - INSTALL_K3S_SYMLINK # If set to 'skip' will not create symlinks, 'force' will overwrite, # default will symlink if command does not exist in path. # # - INSTALL_K3S_SKIP_ENABLE # If set to true will not enable or start k3s service. # # - INSTALL_K3S_SKIP_START # If set to true will not start k3s service. # # - INSTALL_K3S_VERSION # Version of k3s to download from github. Will attempt to download from the # stable channel if not specified. # # - INSTALL_K3S_COMMIT # Commit of k3s to download from temporary cloud storage. # * (for developer & QA use) # # - INSTALL_K3S_BIN_DIR # Directory to install k3s binary, links, and uninstall script to, or use # /usr/local/bin as the default # # - INSTALL_K3S_BIN_DIR_READ_ONLY # If set to true will not write files to INSTALL_K3S_BIN_DIR, forces # setting INSTALL_K3S_SKIP_DOWNLOAD=true # # - INSTALL_K3S_SYSTEMD_DIR # Directory to install systemd service and environment files to, or use # /etc/systemd/system as the default # # - INSTALL_K3S_EXEC or script arguments # Command with flags to use for launching k3s in the systemd service, if # the command is not specified will default to "agent" if K3S_URL is set # or "server" if not. The final systemd command resolves to a combination # of EXEC and script args ($@). # # The following commands result in the same behavior: # curl ... | INSTALL_K3S_EXEC="--disable=traefik" sh -s - # curl ... | INSTALL_K3S_EXEC="server --disable=traefik" sh -s - # curl ... | INSTALL_K3S_EXEC="server" sh -s - --disable=traefik # curl ... | sh -s - server --disable=traefik # curl ... | sh -s - --disable=traefik # # - INSTALL_K3S_NAME # Name of systemd service to create, will default from the k3s exec command # if not specified. If specified the name will be prefixed with 'k3s-'. # # - INSTALL_K3S_TYPE # Type of systemd service to create, will default from the k3s exec command # if not specified. # # - INSTALL_K3S_SELINUX_WARN # If set to true will continue if k3s-selinux policy is not found. # # - INSTALL_K3S_SKIP_SELINUX_RPM # If set to true will skip automatic installation of the k3s RPM. # # - INSTALL_K3S_CHANNEL_URL # Channel URL for fetching k3s download URL. # Defaults to 'https://update.k3s.io/v1-release/channels'. # # - INSTALL_K3S_CHANNEL # Channel to use for fetching k3s download URL. # Defaults to 'stable'. GITHUB_URL=https://github.com/k3s-io/k3s/releases STORAGE_URL=https://k3s-ci-builds.s3.amazonaws.com DOWNLOADER= # --- helper functions for logs --- info() { echo '[INFO] ' "$@" } warn() { echo '[WARN] ' "$@" >&2 } fatal() { echo '[ERROR] ' "$@" >&2 exit 1 } # --- fatal if no systemd or openrc --- verify_system() { if [ -x /sbin/openrc-run ]; then HAS_OPENRC=true return fi if [ -x /bin/systemctl ] || type systemctl > /dev/null 2>&1; then HAS_SYSTEMD=true return fi fatal 'Can not find systemd or openrc to use as a process supervisor for k3s' } # --- add quotes to command arguments --- quote() { for arg in "$@"; do printf '%s\n' "$arg" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/" done } # --- add indentation and trailing slash to quoted args --- quote_indent() { printf ' \\\n' for arg in "$@"; do printf '\t%s \\\n' "$(quote "$arg")" done } # --- escape most punctuation characters, except quotes, forward slash, and space --- escape() { printf '%s' "$@" | sed -e 's/\([][!#$%&()*;<=>?\_`{|}]\)/\\\1/g;' } # --- escape double quotes --- escape_dq() { printf '%s' "$@" | sed -e 's/"/\\"/g' } # --- ensures $K3S_URL is empty or begins with https://, exiting fatally otherwise --- verify_k3s_url() { case "${K3S_URL}" in "") ;; https://*) ;; *) fatal "Only https:// URLs are supported for K3S_URL (have ${K3S_URL})" ;; esac } # --- define needed environment variables --- setup_env() { # --- use command args if passed or create default --- case "$1" in # --- if we only have flags discover if command should be server or agent --- (-*|"") if [ -z "${K3S_URL}" ]; then CMD_K3S=server else if [ -z "${K3S_TOKEN}" ] && [ -z "${K3S_TOKEN_FILE}" ]; then fatal "Defaulted k3s exec command to 'agent' because K3S_URL is defined, but K3S_TOKEN or K3S_TOKEN_FILE is not defined." fi CMD_K3S=agent fi ;; # --- command is provided --- (*) CMD_K3S=$1 shift ;; esac verify_k3s_url CMD_K3S_EXEC="${CMD_K3S}$(quote_indent "$@")" # --- use systemd name if defined or create default --- if [ -n "${INSTALL_K3S_NAME}" ]; then SYSTEM_NAME=k3s-${INSTALL_K3S_NAME} else if [ "${CMD_K3S}" = server ]; then SYSTEM_NAME=k3s else SYSTEM_NAME=k3s-${CMD_K3S} fi fi # --- check for invalid characters in system name --- valid_chars=$(printf '%s' "${SYSTEM_NAME}" | sed -e 's/[][!#$%&()*;<=>?\_`{|}/[:space:]]/^/g;' ) if [ "${SYSTEM_NAME}" != "${valid_chars}" ]; then invalid_chars=$(printf '%s' "${valid_chars}" | sed -e 's/[^^]/ /g') fatal "Invalid characters for system name: ${SYSTEM_NAME} ${invalid_chars}" fi # --- use sudo if we are not already root --- SUDO=sudo if [ $(id -u) -eq 0 ]; then SUDO= fi # --- use systemd type if defined or create default --- if [ -n "${INSTALL_K3S_TYPE}" ]; then SYSTEMD_TYPE=${INSTALL_K3S_TYPE} else SYSTEMD_TYPE=notify fi # --- use binary install directory if defined or create default --- if [ -n "${INSTALL_K3S_BIN_DIR}" ]; then BIN_DIR=${INSTALL_K3S_BIN_DIR} else # --- use /usr/local/bin if root can write to it, otherwise use /opt/bin if it exists BIN_DIR=/usr/local/bin if ! $SUDO sh -c "touch ${BIN_DIR}/k3s-ro-test && rm -rf ${BIN_DIR}/k3s-ro-test"; then if [ -d /opt/bin ]; then BIN_DIR=/opt/bin fi fi fi # --- use systemd directory if defined or create default --- if [ -n "${INSTALL_K3S_SYSTEMD_DIR}" ]; then SYSTEMD_DIR="${INSTALL_K3S_SYSTEMD_DIR}" else SYSTEMD_DIR=/etc/systemd/system fi # --- set related files from system name --- SERVICE_K3S=${SYSTEM_NAME}.service UNINSTALL_K3S_SH=${UNINSTALL_K3S_SH:-${BIN_DIR}/${SYSTEM_NAME}-uninstall.sh} KILLALL_K3S_SH=${KILLALL_K3S_SH:-${BIN_DIR}/k3s-killall.sh} # --- use service or environment location depending on systemd/openrc --- if [ "${HAS_SYSTEMD}" = true ]; then FILE_K3S_SERVICE=${SYSTEMD_DIR}/${SERVICE_K3S} FILE_K3S_ENV=${SYSTEMD_DIR}/${SERVICE_K3S}.env elif [ "${HAS_OPENRC}" = true ]; then $SUDO mkdir -p /etc/rancher/k3s FILE_K3S_SERVICE=/etc/init.d/${SYSTEM_NAME} FILE_K3S_ENV=/etc/rancher/k3s/${SYSTEM_NAME}.env fi # --- get hash of config & exec for currently installed k3s --- PRE_INSTALL_HASHES=$(get_installed_hashes) # --- if bin directory is read only skip download --- if [ "${INSTALL_K3S_BIN_DIR_READ_ONLY}" = true ]; then INSTALL_K3S_SKIP_DOWNLOAD=true fi # --- setup channel values INSTALL_K3S_CHANNEL_URL=${INSTALL_K3S_CHANNEL_URL:-'https://update.k3s.io/v1-release/channels'} INSTALL_K3S_CHANNEL=${INSTALL_K3S_CHANNEL:-'stable'} } # --- check if skip download environment variable set --- can_skip_download_binary() { if [ "${INSTALL_K3S_SKIP_DOWNLOAD}" != true ] && [ "${INSTALL_K3S_SKIP_DOWNLOAD}" != binary ]; then return 1 fi } can_skip_download_selinux() { if [ "${INSTALL_K3S_SKIP_DOWNLOAD}" != true ] && [ "${INSTALL_K3S_SKIP_DOWNLOAD}" != selinux ]; then return 1 fi } # --- verify an executable k3s binary is installed --- verify_k3s_is_executable() { if [ ! -x ${BIN_DIR}/k3s ]; then fatal "Executable k3s binary not found at ${BIN_DIR}/k3s" fi } # --- set arch and suffix, fatal if architecture not supported --- setup_verify_arch() { if [ -z "$ARCH" ]; then ARCH=$(uname -m) fi case $ARCH in amd64) ARCH=amd64 SUFFIX= ;; x86_64) ARCH=amd64 SUFFIX= ;; arm64) ARCH=arm64 SUFFIX=-${ARCH} ;; s390x) ARCH=s390x SUFFIX=-${ARCH} ;; aarch64) ARCH=arm64 SUFFIX=-${ARCH} ;; arm*) ARCH=arm SUFFIX=-${ARCH}hf ;; *) fatal "Unsupported architecture $ARCH" esac } # --- verify existence of network downloader executable --- verify_downloader() { # Return failure if it doesn't exist or is no executable [ -x "$(command -v $1)" ] || return 1 # Set verified executable as our downloader program and return success DOWNLOADER=$1 return 0 } # --- create temporary directory and cleanup when done --- setup_tmp() { TMP_DIR=$(mktemp -d -t k3s-install.XXXXXXXXXX) TMP_HASH=${TMP_DIR}/k3s.hash TMP_BIN=${TMP_DIR}/k3s.bin cleanup() { code=$? set +e trap - EXIT rm -rf ${TMP_DIR} exit $code } trap cleanup INT EXIT } # --- use desired k3s version if defined or find version from channel --- get_release_version() { if [ -n "${INSTALL_K3S_COMMIT}" ]; then VERSION_K3S="commit ${INSTALL_K3S_COMMIT}" elif [ -n "${INSTALL_K3S_VERSION}" ]; then VERSION_K3S=${INSTALL_K3S_VERSION} else info "Finding release for channel ${INSTALL_K3S_CHANNEL}" version_url="${INSTALL_K3S_CHANNEL_URL}/${INSTALL_K3S_CHANNEL}" case $DOWNLOADER in curl) VERSION_K3S=$(curl -w '%{url_effective}' -L -s -S ${version_url} -o /dev/null | sed -e 's|.*/||') ;; wget) VERSION_K3S=$(wget -SqO /dev/null ${version_url} 2>&1 | grep -i Location | sed -e 's|.*/||') ;; *) fatal "Incorrect downloader executable '$DOWNLOADER'" ;; esac fi info "Using ${VERSION_K3S} as release" } # --- get k3s-selinux version --- get_k3s_selinux_version() { available_version="k3s-selinux-1.2-2.${rpm_target}.noarch.rpm" info "Finding available k3s-selinux versions" # run verify_downloader in case it binary installation was skipped verify_downloader curl || verify_downloader wget || fatal 'Can not find curl or wget for downloading files' case $DOWNLOADER in curl) DOWNLOADER_OPTS="-s" ;; wget) DOWNLOADER_OPTS="-q -O -" ;; *) fatal "Incorrect downloader executable '$DOWNLOADER'" ;; esac for i in {1..3}; do set +e if [ "${rpm_channel}" = "testing" ]; then version=$(timeout 5 ${DOWNLOADER} ${DOWNLOADER_OPTS} https://api.github.com/repos/k3s-io/k3s-selinux/releases | grep browser_download_url | awk '{ print $2 }' | grep -oE "[^\/]+${rpm_target}\.noarch\.rpm" | head -n 1) else version=$(timeout 5 ${DOWNLOADER} ${DOWNLOADER_OPTS} https://api.github.com/repos/k3s-io/k3s-selinux/releases/latest | grep browser_download_url | awk '{ print $2 }' | grep -oE "[^\/]+${rpm_target}\.noarch\.rpm") fi set -e if [ "${version}" != "" ]; then break fi sleep 1 done if [ "${version}" == "" ]; then warn "Failed to get available versions of k3s-selinux..defaulting to ${available_version}" return fi available_version=${version} } # --- download from github url --- download() { [ $# -eq 2 ] || fatal 'download needs exactly 2 arguments' case $DOWNLOADER in curl) curl -o $1 -skfL $2 ;; wget) wget -qO $1 $2 ;; *) fatal "Incorrect executable '$DOWNLOADER'" ;; esac # Abort if download command failed [ $? -eq 0 ] || fatal 'Download failed' } # --- download hash from github url --- download_hash() { if [ -n "${INSTALL_K3S_COMMIT}" ]; then HASH_URL=${STORAGE_URL}/k3s${SUFFIX}-${INSTALL_K3S_COMMIT}.sha256sum else HASH_URL=${GITHUB_URL}/download/${VERSION_K3S}/sha256sum-${ARCH}.txt fi info "Downloading hash ${HASH_URL}" download ${TMP_HASH} ${HASH_URL} HASH_EXPECTED=$(grep " k3s${SUFFIX}$" ${TMP_HASH}) HASH_EXPECTED=${HASH_EXPECTED%%[[:blank:]]*} } # --- check hash against installed version --- installed_hash_matches() { if [ -x ${BIN_DIR}/k3s ]; then HASH_INSTALLED=$(sha256sum ${BIN_DIR}/k3s) HASH_INSTALLED=${HASH_INSTALLED%%[[:blank:]]*} if [ "${HASH_EXPECTED}" = "${HASH_INSTALLED}" ]; then return fi fi return 1 } # --- download binary from github url --- download_binary() { if [ -n "${INSTALL_K3S_COMMIT}" ]; then BIN_URL=${STORAGE_URL}/k3s${SUFFIX}-${INSTALL_K3S_COMMIT} else BIN_URL=${GITHUB_URL}/download/${VERSION_K3S}/k3s${SUFFIX} fi info "Downloading binary ${BIN_URL}" download ${TMP_BIN} ${BIN_URL} } # --- verify downloaded binary hash --- verify_binary() { info "Verifying binary download" HASH_BIN=$(sha256sum ${TMP_BIN}) HASH_BIN=${HASH_BIN%%[[:blank:]]*} if [ "${HASH_EXPECTED}" != "${HASH_BIN}" ]; then fatal "Download sha256 does not match ${HASH_EXPECTED}, got ${HASH_BIN}" fi } # --- setup permissions and move binary to system directory --- setup_binary() { chmod 755 ${TMP_BIN} info "Installing k3s to ${BIN_DIR}/k3s" $SUDO chown root:root ${TMP_BIN} $SUDO mv -f ${TMP_BIN} ${BIN_DIR}/k3s } # --- setup selinux policy --- setup_selinux() { case ${INSTALL_K3S_CHANNEL} in *testing) rpm_channel=testing ;; *latest) rpm_channel=latest ;; *) rpm_channel=stable ;; esac rpm_site="rpm.rancher.io" if [ "${rpm_channel}" = "testing" ]; then rpm_site="rpm-testing.rancher.io" fi [ -r /etc/os-release ] && . /etc/os-release if [ `expr "${ID_LIKE}" : ".*suse.*"` != 0 ]; then rpm_target=sle rpm_site_infix=microos package_installer=zypper if [ "${ID_LIKE:-}" = suse ] && [ "${VARIANT_ID:-}" = sle-micro ]; then rpm_target=sle rpm_site_infix=slemicro package_installer=zypper fi elif [ "${ID_LIKE:-}" = coreos ] || [ "${VARIANT_ID:-}" = coreos ]; then rpm_target=coreos rpm_site_infix=coreos package_installer=rpm-ostree elif [ "${VERSION_ID%%.*}" = "7" ]; then rpm_target=el7 rpm_site_infix=centos/7 package_installer=yum elif [ "${VERSION_ID%%.*}" = "8" ] || [ "${VERSION_ID%%.*}" -gt "36" ]; then rpm_target=el8 rpm_site_infix=centos/8 package_installer=yum else rpm_target=el9 rpm_site_infix=centos/9 package_installer=yum fi if [ "${package_installer}" = "rpm-ostree" ] && [ -x /bin/yum ]; then package_installer=yum fi if [ "${package_installer}" = "yum" ] && [ -x /usr/bin/dnf ]; then package_installer=dnf fi policy_hint="please install: ${package_installer} install -y container-selinux ${package_installer} install -y https://${rpm_site}/k3s/${rpm_channel}/common/${rpm_site_infix}/noarch/${available_version} " if [ "$INSTALL_K3S_SKIP_SELINUX_RPM" = true ] || can_skip_download_selinux || [ ! -d /usr/share/selinux ]; then info "Skipping installation of SELinux RPM" else get_k3s_selinux_version install_selinux_rpm ${rpm_site} ${rpm_channel} ${rpm_target} ${rpm_site_infix} fi policy_error=fatal if [ "$INSTALL_K3S_SELINUX_WARN" = true ] || [ "${ID_LIKE:-}" = coreos ] || [ "${VARIANT_ID:-}" = coreos ]; then policy_error=warn fi if ! $SUDO chcon -u system_u -r object_r -t container_runtime_exec_t ${BIN_DIR}/k3s >/dev/null 2>&1; then if $SUDO grep '^\s*SELINUX=enforcing' /etc/selinux/config >/dev/null 2>&1; then $policy_error "Failed to apply container_runtime_exec_t to ${BIN_DIR}/k3s, ${policy_hint}" fi elif [ ! -f /usr/share/selinux/packages/k3s.pp ]; then if [ -x /usr/sbin/transactional-update ] || [ "${ID_LIKE:-}" = coreos ] || [ "${VARIANT_ID:-}" = coreos ]; then warn "Please reboot your machine to activate the changes and avoid data loss." else $policy_error "Failed to find the k3s-selinux policy, ${policy_hint}" fi fi } install_selinux_rpm() { if [ -r /etc/redhat-release ] || [ -r /etc/centos-release ] || [ -r /etc/oracle-release ] || [ -r /etc/fedora-release ] || [ "${ID_LIKE%%[ ]*}" = "suse" ]; then repodir=/etc/yum.repos.d if [ -d /etc/zypp/repos.d ]; then repodir=/etc/zypp/repos.d fi set +o noglob $SUDO rm -f ${repodir}/rancher-k3s-common*.repo set -o noglob if [ -r /etc/redhat-release ] && [ "${3}" = "el7" ]; then $SUDO yum install -y yum-utils $SUDO yum-config-manager --enable rhel-7-server-extras-rpms fi $SUDO tee ${repodir}/rancher-k3s-common.repo >/dev/null << EOF [rancher-k3s-common-${2}] name=Rancher K3s Common (${2}) baseurl=https://${1}/k3s/${2}/common/${4}/noarch enabled=1 gpgcheck=1 repo_gpgcheck=0 gpgkey=https://${1}/public.key EOF case ${3} in sle) rpm_installer="zypper --gpg-auto-import-keys" if [ "${TRANSACTIONAL_UPDATE=false}" != "true" ] && [ -x /usr/sbin/transactional-update ]; then transactional_update_run="transactional-update --no-selfupdate -d run" rpm_installer="transactional-update --no-selfupdate -d run ${rpm_installer}" : "${INSTALL_K3S_SKIP_START:=true}" fi # create the /var/lib/rpm-state in SLE systems to fix the prein selinux macro ${transactional_update_run} mkdir -p /var/lib/rpm-state ;; coreos) rpm_installer="rpm-ostree" # rpm_install_extra_args="--apply-live" : "${INSTALL_K3S_SKIP_START:=true}" ;; *) rpm_installer="yum" ;; esac if [ "${rpm_installer}" = "yum" ] && [ -x /usr/bin/dnf ]; then rpm_installer=dnf fi if rpm -q --quiet k3s-selinux; then # remove k3s-selinux module before upgrade to allow container-selinux to upgrade safely if check_available_upgrades container-selinux ${3} && check_available_upgrades k3s-selinux ${3}; then MODULE_PRIORITY=$($SUDO semodule --list=full | grep k3s | cut -f1 -d" ") if [ -n "${MODULE_PRIORITY}" ]; then $SUDO semodule -X $MODULE_PRIORITY -r k3s || true fi fi fi # shellcheck disable=SC2086 $SUDO ${rpm_installer} install -y "k3s-selinux" fi return } check_available_upgrades() { set +e case ${2} in sle) available_upgrades=$($SUDO zypper -q -t -s 11 se -s -u --type package $1 | tail -n 1 | grep -v "No matching" | awk '{print $3}') ;; coreos) # currently rpm-ostree does not support search functionality https://github.com/coreos/rpm-ostree/issues/1877 ;; *) available_upgrades=$($SUDO yum -q --refresh list $1 --upgrades | tail -n 1 | awk '{print $2}') ;; esac set -e if [ -n "${available_upgrades}" ]; then return 0 fi return 1 } # --- download and verify k3s --- download_and_verify() { if can_skip_download_binary; then info 'Skipping k3s download and verify' verify_k3s_is_executable return fi setup_verify_arch verify_downloader curl || verify_downloader wget || fatal 'Can not find curl or wget for downloading files' setup_tmp get_release_version download_hash if installed_hash_matches; then info 'Skipping binary downloaded, installed k3s matches hash' return fi download_binary verify_binary setup_binary } # --- add additional utility links --- create_symlinks() { [ "${INSTALL_K3S_BIN_DIR_READ_ONLY}" = true ] && return [ "${INSTALL_K3S_SYMLINK}" = skip ] && return for cmd in kubectl crictl ctr; do if [ ! -e ${BIN_DIR}/${cmd} ] || [ "${INSTALL_K3S_SYMLINK}" = force ]; then which_cmd=$(command -v ${cmd} 2>/dev/null || true) if [ -z "${which_cmd}" ] || [ "${INSTALL_K3S_SYMLINK}" = force ]; then info "Creating ${BIN_DIR}/${cmd} symlink to k3s" $SUDO ln -sf k3s ${BIN_DIR}/${cmd} else info "Skipping ${BIN_DIR}/${cmd} symlink to k3s, command exists in PATH at ${which_cmd}" fi else info "Skipping ${BIN_DIR}/${cmd} symlink to k3s, already exists" fi done } # --- create killall script --- create_killall() { [ "${INSTALL_K3S_BIN_DIR_READ_ONLY}" = true ] && return info "Creating killall script ${KILLALL_K3S_SH}" $SUDO tee ${KILLALL_K3S_SH} >/dev/null << \EOF #!/bin/sh [ $(id -u) -eq 0 ] || exec sudo $0 $@ for bin in /var/lib/rancher/k3s/data/**/bin/; do [ -d $bin ] && export PATH=$PATH:$bin:$bin/aux done set -x for service in /etc/systemd/system/k3s*.service; do [ -s $service ] && systemctl stop $(basename $service) done for service in /etc/init.d/k3s*; do [ -x $service ] && $service stop done pschildren() { ps -e -o ppid= -o pid= | \ sed -e 's/^\s*//g; s/\s\s*/\t/g;' | \ grep -w "^$1" | \ cut -f2 } pstree() { for pid in $@; do echo $pid for child in $(pschildren $pid); do pstree $child done done } killtree() { kill -9 $( { set +x; } 2>/dev/null; pstree $@; set -x; ) 2>/dev/null } remove_interfaces() { # Delete network interface(s) that match 'master cni0' ip link show 2>/dev/null | grep 'master cni0' | while read ignore iface ignore; do iface=${iface%%@*} [ -z "$iface" ] || ip link delete $iface done # Delete cni related interfaces ip link delete cni0 ip link delete flannel.1 ip link delete flannel-v6.1 ip link delete kube-ipvs0 ip link delete flannel-wg ip link delete flannel-wg-v6 # Restart tailscale if [ -n "$(command -v tailscale)" ]; then tailscale set --advertise-routes= fi } getshims() { ps -e -o pid= -o args= | sed -e 's/^ *//; s/\s\s*/\t/;' | grep -w 'k3s/data/[^/]*/bin/containerd-shim' | cut -f1 } killtree $({ set +x; } 2>/dev/null; getshims; set -x) do_unmount_and_remove() { set +x while read -r _ path _; do case "$path" in $1*) echo "$path" ;; esac done < /proc/self/mounts | sort -r | xargs -r -t -n 1 sh -c 'umount "$0" && rm -rf "$0"' set -x } do_unmount_and_remove '/run/k3s' do_unmount_and_remove '/var/lib/rancher/k3s' do_unmount_and_remove '/var/lib/kubelet/pods' do_unmount_and_remove '/var/lib/kubelet/plugins' do_unmount_and_remove '/run/netns/cni-' # Remove CNI namespaces ip netns show 2>/dev/null | grep cni- | xargs -r -t -n 1 ip netns delete remove_interfaces rm -rf /var/lib/cni/ iptables-save | grep -v KUBE- | grep -v CNI- | grep -iv flannel | iptables-restore ip6tables-save | grep -v KUBE- | grep -v CNI- | grep -iv flannel | ip6tables-restore EOF $SUDO chmod 755 ${KILLALL_K3S_SH} $SUDO chown root:root ${KILLALL_K3S_SH} } # --- create uninstall script --- create_uninstall() { [ "${INSTALL_K3S_BIN_DIR_READ_ONLY}" = true ] && return info "Creating uninstall script ${UNINSTALL_K3S_SH}" $SUDO tee ${UNINSTALL_K3S_SH} >/dev/null << EOF #!/bin/sh set -x [ \$(id -u) -eq 0 ] || exec sudo \$0 \$@ ${KILLALL_K3S_SH} if command -v systemctl; then systemctl disable ${SYSTEM_NAME} systemctl reset-failed ${SYSTEM_NAME} systemctl daemon-reload fi if command -v rc-update; then rc-update delete ${SYSTEM_NAME} default fi rm -f ${FILE_K3S_SERVICE} rm -f ${FILE_K3S_ENV} remove_uninstall() { rm -f ${UNINSTALL_K3S_SH} } trap remove_uninstall EXIT if (ls ${SYSTEMD_DIR}/k3s*.service || ls /etc/init.d/k3s*) >/dev/null 2>&1; then set +x; echo 'Additional k3s services installed, skipping uninstall of k3s'; set -x exit fi for cmd in kubectl crictl ctr; do if [ -L ${BIN_DIR}/\$cmd ]; then rm -f ${BIN_DIR}/\$cmd fi done rm -rf /etc/rancher/k3s rm -rf /run/k3s rm -rf /run/flannel rm -rf /var/lib/rancher/k3s rm -rf /var/lib/kubelet rm -f ${BIN_DIR}/k3s rm -f ${KILLALL_K3S_SH} if type yum >/dev/null 2>&1; then yum remove -y k3s-selinux rm -f /etc/yum.repos.d/rancher-k3s-common*.repo elif type rpm-ostree >/dev/null 2>&1; then rpm-ostree uninstall k3s-selinux rm -f /etc/yum.repos.d/rancher-k3s-common*.repo elif type zypper >/dev/null 2>&1; then uninstall_cmd="zypper remove -y k3s-selinux" if [ "\${TRANSACTIONAL_UPDATE=false}" != "true" ] && [ -x /usr/sbin/transactional-update ]; then uninstall_cmd="transactional-update --no-selfupdate -d run \$uninstall_cmd" fi \$uninstall_cmd rm -f /etc/zypp/repos.d/rancher-k3s-common*.repo fi EOF $SUDO chmod 755 ${UNINSTALL_K3S_SH} $SUDO chown root:root ${UNINSTALL_K3S_SH} } # --- disable current service if loaded -- systemd_disable() { $SUDO systemctl disable ${SYSTEM_NAME} >/dev/null 2>&1 || true $SUDO rm -f /etc/systemd/system/${SERVICE_K3S} || true $SUDO rm -f /etc/systemd/system/${SERVICE_K3S}.env || true } # --- capture current env and create file containing k3s_ variables --- create_env_file() { info "env: Creating environment file ${FILE_K3S_ENV}" $SUDO touch ${FILE_K3S_ENV} $SUDO chmod 0600 ${FILE_K3S_ENV} sh -c export | while read x v; do echo $v; done | grep -E '^(K3S|CONTAINERD)_' | $SUDO tee ${FILE_K3S_ENV} >/dev/null sh -c export | while read x v; do echo $v; done | grep -Ei '^(NO|HTTP|HTTPS)_PROXY' | $SUDO tee -a ${FILE_K3S_ENV} >/dev/null } # --- write systemd service file --- create_systemd_service_file() { info "systemd: Creating service file ${FILE_K3S_SERVICE}" $SUDO tee ${FILE_K3S_SERVICE} >/dev/null << EOF [Unit] Description=Lightweight Kubernetes Documentation=https://k3s.io Wants=network-online.target After=network-online.target [Install] WantedBy=multi-user.target [Service] Type=${SYSTEMD_TYPE} EnvironmentFile=-/etc/default/%N EnvironmentFile=-/etc/sysconfig/%N EnvironmentFile=-${FILE_K3S_ENV} KillMode=process Delegate=yes # Having non-zero Limit*s causes performance problems due to accounting overhead # in the kernel. We recommend using cgroups to do container-local accounting. LimitNOFILE=1048576 LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity TimeoutStartSec=0 Restart=always RestartSec=5s ExecStartPre=/bin/sh -xc '! /usr/bin/systemctl is-enabled --quiet nm-cloud-setup.service' ExecStartPre=-/sbin/modprobe br_netfilter ExecStartPre=-/sbin/modprobe overlay ExecStart=${BIN_DIR}/k3s \\ ${CMD_K3S_EXEC} EOF } # --- write openrc service file --- create_openrc_service_file() { LOG_FILE=/var/log/${SYSTEM_NAME}.log info "openrc: Creating service file ${FILE_K3S_SERVICE}" $SUDO tee ${FILE_K3S_SERVICE} >/dev/null << EOF #!/sbin/openrc-run depend() { after network-online want cgroups } start_pre() { rm -f /tmp/k3s.* } supervisor=supervise-daemon name=${SYSTEM_NAME} command="${BIN_DIR}/k3s" command_args="$(escape_dq "${CMD_K3S_EXEC}") >>${LOG_FILE} 2>&1" output_log=${LOG_FILE} error_log=${LOG_FILE} pidfile="/var/run/${SYSTEM_NAME}.pid" respawn_delay=5 respawn_max=0 set -o allexport if [ -f /etc/environment ]; then . /etc/environment; fi if [ -f ${FILE_K3S_ENV} ]; then . ${FILE_K3S_ENV}; fi set +o allexport EOF $SUDO chmod 0755 ${FILE_K3S_SERVICE} $SUDO tee /etc/logrotate.d/${SYSTEM_NAME} >/dev/null << EOF ${LOG_FILE} { missingok notifempty copytruncate } EOF } # --- write systemd or openrc service file --- create_service_file() { [ "${HAS_SYSTEMD}" = true ] && create_systemd_service_file [ "${HAS_OPENRC}" = true ] && create_openrc_service_file return 0 } # --- get hashes of the current k3s bin and service files get_installed_hashes() { $SUDO sha256sum ${BIN_DIR}/k3s ${FILE_K3S_SERVICE} ${FILE_K3S_ENV} 2>&1 || true } # --- enable and start systemd service --- systemd_enable() { info "systemd: Enabling ${SYSTEM_NAME} unit" $SUDO systemctl enable ${FILE_K3S_SERVICE} >/dev/null $SUDO systemctl daemon-reload >/dev/null } systemd_start() { info "systemd: Starting ${SYSTEM_NAME}" $SUDO systemctl restart ${SYSTEM_NAME} } # --- enable and start openrc service --- openrc_enable() { info "openrc: Enabling ${SYSTEM_NAME} service for default runlevel" $SUDO rc-update add ${SYSTEM_NAME} default >/dev/null } openrc_start() { info "openrc: Starting ${SYSTEM_NAME}" $SUDO ${FILE_K3S_SERVICE} restart } # --- startup systemd or openrc service --- service_enable_and_start() { if [ -f "/proc/cgroups" ] && [ "$(grep memory /proc/cgroups | while read -r n n n enabled; do echo $enabled; done)" -eq 0 ]; then info 'Failed to find memory cgroup, you may need to add "cgroup_memory=1 cgroup_enable=memory" to your linux cmdline (/boot/cmdline.txt on a Raspberry Pi)' fi [ "${INSTALL_K3S_SKIP_ENABLE}" = true ] && return [ "${HAS_SYSTEMD}" = true ] && systemd_enable [ "${HAS_OPENRC}" = true ] && openrc_enable [ "${INSTALL_K3S_SKIP_START}" = true ] && return POST_INSTALL_HASHES=$(get_installed_hashes) if [ "${PRE_INSTALL_HASHES}" = "${POST_INSTALL_HASHES}" ] && [ "${INSTALL_K3S_FORCE_RESTART}" != true ]; then info 'No change detected so skipping service start' return fi if command -v iptables-save 1> /dev/null && command -v iptables-restore 1> /dev/null then $SUDO iptables-save | grep -v KUBE- | grep -iv flannel | $SUDO iptables-restore fi if command -v ip6tables-save 1> /dev/null && command -v ip6tables-restore 1> /dev/null then $SUDO ip6tables-save | grep -v KUBE- | grep -iv flannel | $SUDO ip6tables-restore fi [ "${HAS_SYSTEMD}" = true ] && systemd_start [ "${HAS_OPENRC}" = true ] && openrc_start return 0 } # --- re-evaluate args to include env command --- eval set -- $(escape "${INSTALL_K3S_EXEC}") $(quote "$@") # --- run the install process -- { verify_system setup_env "$@" download_and_verify setup_selinux create_symlinks create_killall create_uninstall systemd_disable create_env_file create_service_file service_enable_and_start } ================================================ FILE: mdz/pkg/server/k3s_destroy.go ================================================ package server import ( "fmt" "os/exec" "syscall" ) // k3sDestroyAllStep installs k3s and related tools. type k3sDestroyAllStep struct { options Options } func (s *k3sDestroyAllStep) Run() error { fmt.Fprintf(s.options.OutputStream, "🚧 Destroy the OpenModelz Cluster...\n") // TODO(gaocegege): Embed the script into the binary. cmd := exec.Command("/bin/sh", "-c", "/usr/local/bin/k3s-uninstall.sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } err := cmd.Run() if err != nil { return err } return nil } func (s *k3sDestroyAllStep) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/k3s_install.go ================================================ package server import ( _ "embed" "fmt" "io" "os/exec" "syscall" ) //go:embed k3s-install.sh var bashContent string // k3sInstallStep installs k3s and related tools. type k3sInstallStep struct { options Options } func (s *k3sInstallStep) Run() error { checkCmd := exec.Command("/bin/sh", "-c", "sudo k3s kubectl get nodes") checkCmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } checkCmd.Stdout = nil checkCmd.Stderr = nil err := checkCmd.Run() if err == nil { fmt.Fprintf(s.options.OutputStream, "🚧 The server is already created, skip...\n") return nil } fmt.Fprintf(s.options.OutputStream, "🚧 Creating the server...\n") // TODO(gaocegege): Embed the script into the binary. // Always run start, do not check the hash to decide. cmd := exec.Command("/bin/sh", "-c", "INSTALL_K3S_VERSION=v1.27.3+k3s1 INSTALL_K3S_EXEC='--disable=traefik' INSTALL_K3S_FORCE_RESTART=true K3S_KUBECONFIG_MODE=644 K3S_TOKEN=openmodelz sh -") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } stdin, err := cmd.StdinPipe() if err != nil { return err } defer stdin.Close() // the doc says subProcess.Wait will close it, but I'm not sure, so I kept this line if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } if err := cmd.Start(); err != nil { return err } if _, err := io.WriteString(stdin, bashContent); err != nil { return err } // Close the input stream to finish the pipe. Then the command will use the // input from the pipe to start the next process. stdin.Close() fmt.Fprintf(s.options.OutputStream, "🚧 Waiting for the server to be created...\n") if err := cmd.Wait(); err != nil { return err } return nil } func (s *k3sInstallStep) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/k3s_join.go ================================================ package server import ( "fmt" "io" "os/exec" "syscall" ) // k3sJoinStep installs k3s and related tools. type k3sJoinStep struct { options Options } func (s *k3sJoinStep) Run() error { fmt.Fprintf(s.options.OutputStream, "🚧 Joining the cluster...\n") // TODO(gaocegege): Embed the script into the binary. cmdStr := fmt.Sprintf("INSTALL_K3S_FORCE_RESTART=true K3S_KUBECONFIG_MODE=644 K3S_TOKEN=openmodelz K3S_URL=https://%s:6443 sh -", s.options.ServerIP) cmd := exec.Command("/bin/sh", "-c", cmdStr) cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } stdin, err := cmd.StdinPipe() if err != nil { return err } defer stdin.Close() // the doc says subProcess.Wait will close it, but I'm not sure, so I kept this line if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } if err := cmd.Start(); err != nil { return err } if _, err := io.WriteString(stdin, bashContent); err != nil { return err } // Close the input stream to finish the pipe. Then the command will use the // input from the pipe to start the next process. stdin.Close() fmt.Fprintf(s.options.OutputStream, "🚧 Waiting for the server to be ready...\n") if err := cmd.Wait(); err != nil { return err } return nil } func (s *k3sJoinStep) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/k3s_killall.go ================================================ package server import ( "fmt" "os/exec" "syscall" ) // k3sKillAllStep installs k3s and related tools. type k3sKillAllStep struct { options Options } func (s *k3sKillAllStep) Run() error { fmt.Fprintf(s.options.OutputStream, "🚧 Stopping the OpenModelz Cluster...\n") // TODO(gaocegege): Embed the script into the binary. cmd := exec.Command("/bin/sh", "-c", "/usr/local/bin/k3s-killall.sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } err := cmd.Run() if err != nil { return err } return nil } func (s *k3sKillAllStep) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/k3s_prepare.go ================================================ package server import ( _ "embed" "fmt" "os/exec" "path/filepath" "strings" "syscall" "text/template" ) //go:embed registries.yaml var registriesContent string const mirrorPath = "/etc/rancher/k3s" const mirrorFile = "registries.yaml" // k3sPrepare install everything required by k3s. type k3sPrepare struct { options Options } func (s *k3sPrepare) Run() error { if !s.options.Mirror.Configured() { return nil } fmt.Fprintf(s.options.OutputStream, "🚧 Configure the mirror...\n") tmpl, err := template.New("registries").Parse(registriesContent) if err != nil { panic(err) } buf := strings.Builder{} err = tmpl.Execute(&buf, s.options.Mirror) if err != nil { panic(err) } cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf( "sudo mkdir -p %s && sudo tee %s > /dev/null << EOF\n%s\nEOF", mirrorPath, filepath.Join(mirrorPath, mirrorFile), buf.String(), )) cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } err = cmd.Run() if err != nil { return err } return nil } func (s *k3sPrepare) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/nginx-dep.yaml ================================================ kind: Namespace apiVersion: v1 metadata: name: ingress-nginx --- apiVersion: helm.cattle.io/v1 kind: HelmChart metadata: name: ingress-nginx namespace: kube-system spec: chart: ingress-nginx repo: https://kubernetes.github.io/ingress-nginx targetNamespace: ingress-nginx version: v4.7.0 set: valuesContent: |- fullnameOverride: ingress-nginx controller: kind: DaemonSet hostNetwork: true hostPort: enabled: true service: enabled: true ports: http: 9000 https: 9001 publishService: enabled: false metrics: enabled: false serviceMonitor: enabled: false config: use-forwarded-headers: "true" ================================================ FILE: mdz/pkg/server/nginx_install.go ================================================ package server import ( _ "embed" "fmt" "io" "os/exec" "syscall" ) //go:embed nginx-dep.yaml var nginxYamlContent string // nginxInstallStep installs the nginx deployment. type nginxInstallStep struct { options Options } func (s *nginxInstallStep) Run() error { fmt.Fprintf(s.options.OutputStream, "🚧 Initializing the load balancer...\n") cmd := exec.Command("/bin/sh", "-c", "sudo k3s kubectl apply -f -") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } stdin, err := cmd.StdinPipe() if err != nil { return err } defer stdin.Close() // the doc says subProcess.Wait will close it, but I'm not sure, so I kept this line if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } if err := cmd.Start(); err != nil { return err } if _, err := io.WriteString(stdin, nginxYamlContent); err != nil { return err } // Close the input stream to finish the pipe. Then the command will use the // input from the pipe to start the next process. stdin.Close() if err := cmd.Wait(); err != nil { return err } return nil } func (s *nginxInstallStep) Verify() error { return nil } ================================================ FILE: mdz/pkg/server/openmodelz.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: openmodelz labels: name: openmodelz --- apiVersion: helm.cattle.io/v1 kind: HelmChart metadata: name: openmodelz namespace: kube-system spec: chart: openmodelz repo: https://tensorchord.github.io/openmodelz-charts targetNamespace: openmodelz version: {{.Version}} set: valuesContent: |- fullnameOverride: openmodelz agent: ingress: enabled: true ipToDomain: {{.IpToDomain}} domain: "{{.Domain}}" modelzCloud: enabled: {{.EnableModelZCloud}} url: {{.ModelZCloudUrl}} token: {{.ModelZCloudAgentToken}} region: {{.ModelZCloudRegion}} ================================================ FILE: mdz/pkg/server/openmodelz_install.go ================================================ package server import ( _ "embed" "fmt" "html/template" "io" "os/exec" "regexp" "strings" "syscall" "github.com/sirupsen/logrus" ) //go:embed openmodelz.yaml var yamlContent string var resultDomain string // openModelZInstallStep installs the OpenModelZ deployments. type openModelZInstallStep struct { options Options } func (s *openModelZInstallStep) Run() error { fmt.Fprintf(s.options.OutputStream, "🚧 Initializing the server...\n") cmd := exec.Command("/bin/sh", "-c", "sudo k3s kubectl apply -f -") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } stdin, err := cmd.StdinPipe() if err != nil { return err } defer stdin.Close() // the doc says subProcess.Wait will close it, but I'm not sure, so I kept this line if s.options.Verbose { cmd.Stderr = s.options.OutputStream cmd.Stdout = s.options.OutputStream } else { cmd.Stdout = nil cmd.Stderr = nil } if err := cmd.Start(); err != nil { return err } variables := struct { Domain string IpToDomain bool Version string EnableModelZCloud bool ModelZCloudUrl string ModelZCloudAgentToken string ModelZCloudRegion string }{ Version: s.options.Version, } if s.options.Domain != nil { variables.Domain = *s.options.Domain variables.IpToDomain = false } else { fmt.Fprintf(s.options.OutputStream, "🚧 No domain provided, using the server IP...\n") variables.Domain = "" variables.IpToDomain = true } if s.options.ModelZCloud.Enabled { variables.EnableModelZCloud = true variables.ModelZCloudUrl = s.options.ModelZCloud.URL variables.ModelZCloudAgentToken = s.options.ModelZCloud.Token variables.ModelZCloudRegion = s.options.ModelZCloud.Region } else { variables.EnableModelZCloud = false variables.ModelZCloudUrl = "" variables.ModelZCloudAgentToken = "" variables.ModelZCloudRegion = "" } tmpl, err := template.New("openmodelz").Parse(yamlContent) if err != nil { panic(err) } buf := strings.Builder{} err = tmpl.Execute(&buf, variables) if err != nil { panic(err) } logrus.WithField("variables", variables). Debugf("Deploying OpenModelZ with the following variables") if _, err := io.WriteString(stdin, buf.String()); err != nil { return err } // Close the input stream to finish the pipe. Then the command will use the // input from the pipe to start the next process. stdin.Close() fmt.Fprintf(s.options.OutputStream, "🚧 Waiting for the server to be ready...\n") if err := cmd.Wait(); err != nil { return err } return nil } func (s *openModelZInstallStep) Verify() error { fmt.Fprintf(s.options.OutputStream, "🚧 Verifying the load balancer...\n") cmd := exec.Command("/bin/sh", "-c", "sudo k3s kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath={@.status.loadBalancer.ingress}") cmd.SysProcAttr = &syscall.SysProcAttr{ Pdeathsig: syscall.SIGKILL, } output, err := cmd.CombinedOutput() if err != nil { logrus.Debugf("failed to get the ingress ip: %v", err) return err } logrus.Debugf("kubectl get cmd output: %s\n", output) if len(output) <= 4 { return fmt.Errorf("cannot get the ingress ip: output is empty") } // Get the IP from the output lie this: `[{"ip":"192.168.71.93"}]` re := regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`) found := re.MatchString(string(output)) if !found { return fmt.Errorf("cannot get the ingress ip") } resultDomain = re.FindString(string(output)) return nil } ================================================ FILE: mdz/pkg/server/registries.yaml ================================================ mirrors: {{ .Name }}: endpoint: {{ range $endpoint := .Endpoints }}- "{{ $endpoint }}"{{ end }} ================================================ FILE: mdz/pkg/telemetry/telemetry.go ================================================ package telemetry import ( "io" "os" "path/filepath" "runtime" "sync" "time" "github.com/cockroachdb/errors" "github.com/google/uuid" segmentio "github.com/segmentio/analytics-go/v3" "github.com/sirupsen/logrus" "github.com/tensorchord/openmodelz/mdz/pkg/version" ) type TelemetryField func(*segmentio.Properties) type Telemetry interface { Record(command string, args ...TelemetryField) } type defaultTelemetry struct { client segmentio.Client uid string enabled bool } const telemetryToken = "65WHA9bxCNX74K3HjgplMOmsio9LkYSI" var ( once sync.Once telemetry *defaultTelemetry telemetryConfigFile string ) func init() { home, err := os.UserHomeDir() if err != nil { panic(err) } telemetryConfigFile = filepath.Join(home, ".config", "openmodelz", "telemetry") } func GetTelemetry() Telemetry { return telemetry } func Initialize(enabled bool) error { once.Do(func() { client, err := segmentio.NewWithConfig(telemetryToken, segmentio.Config{ BatchSize: 1, }) if err != nil { panic(err) } telemetry = &defaultTelemetry{ client: client, enabled: enabled, } }) return telemetry.init() } func (t *defaultTelemetry) init() error { if !t.enabled { return nil } // detect if the config file already exists _, err := os.Stat(telemetryConfigFile) if err != nil { if !os.IsNotExist(err) { return errors.Wrap(err, "failed to stat telemetry config file") } t.uid = uuid.New().String() return t.dumpConfig() } if err = t.loadConfig(); err != nil { return errors.Wrap(err, "failed to load telemetry config") } t.Idnetify() return nil } func (t *defaultTelemetry) dumpConfig() error { if err := os.MkdirAll(filepath.Dir(telemetryConfigFile), os.ModeDir|0700); err != nil { return errors.Wrap(err, "failed to create telemetry config directory") } file, err := os.Create(telemetryConfigFile) if err != nil { return errors.Wrap(err, "failed to create telemetry config file") } defer file.Close() _, err = file.WriteString(t.uid) if err != nil { return errors.Wrap(err, "failed to write telemetry config file") } return nil } func (t *defaultTelemetry) loadConfig() error { file, err := os.Open(telemetryConfigFile) if err != nil { return errors.Wrap(err, "failed to open telemetry config file") } defer file.Close() uid, err := io.ReadAll(file) if err != nil { return errors.Wrap(err, "failed to read telemetry config file") } t.uid = string(uid) return nil } func (t *defaultTelemetry) Idnetify() { if !t.enabled { return } v := version.GetOpenModelzVersion() if err := t.client.Enqueue(segmentio.Identify{ AnonymousId: t.uid, Context: &segmentio.Context{ OS: segmentio.OSInfo{ Name: runtime.GOOS, Version: runtime.GOARCH, }, App: segmentio.AppInfo{ Name: "openmodelz", Version: v, }, }, Timestamp: time.Now(), Traits: segmentio.NewTraits(), }); err != nil { logrus.WithError(err).Debug("failed to identify user") return } } func AddField(name string, value interface{}) TelemetryField { return func(p *segmentio.Properties) { p.Set(name, value) } } func (t *defaultTelemetry) Record(command string, fields ...TelemetryField) { if !t.enabled { return } logrus.WithField("UID", t.uid).WithField("command", command).Debug("send telemetry") track := segmentio.Track{ AnonymousId: t.uid, Event: command, Properties: segmentio.NewProperties(), } for _, field := range fields { field(&track.Properties) } if err := t.client.Enqueue(track); err != nil { logrus.WithError(err).Debug("failed to send telemetry") } // make sure the msg can be sent out t.client.Close() } ================================================ FILE: mdz/pkg/term/interrupt.go ================================================ /* Copyright 2016 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 term import ( "os" "os/signal" "sync" "syscall" ) // terminationSignals are signals that cause the program to exit in the // supported platforms (linux, darwin, windows). var terminationSignals = []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT} // Handler guarantees execution of notifications after a critical section (the function passed // to a Run method), even in the presence of process termination. It guarantees exactly once // invocation of the provided notify functions. type Handler struct { notify []func() final func(os.Signal) once sync.Once } // Chain creates a new handler that invokes all notify functions when the critical section exits // and then invokes the optional handler's notifications. This allows critical sections to be // nested without losing exactly once invocations. Notify functions can invoke any cleanup needed // but should not exit (which is the responsibility of the parent handler). func Chain(handler *Handler, notify ...func()) *Handler { if handler == nil { return New(nil, notify...) } return New(handler.Signal, append(notify, handler.Close)...) } // New creates a new handler that guarantees all notify functions are run after the critical // section exits (or is interrupted by the OS), then invokes the final handler. If no final // handler is specified, the default final is `os.Exit(1)`. A handler can only be used for // one critical section. func New(final func(os.Signal), notify ...func()) *Handler { return &Handler{ final: final, notify: notify, } } // Close executes all the notification handlers if they have not yet been executed. func (h *Handler) Close() { h.once.Do(func() { for _, fn := range h.notify { fn() } }) } // Signal is called when an os.Signal is received, and guarantees that all notifications // are executed, then the final handler is executed. This function should only be called once // per Handler instance. func (h *Handler) Signal(s os.Signal) { h.once.Do(func() { for _, fn := range h.notify { fn() } if h.final == nil { os.Exit(1) } h.final(s) }) } // Run ensures that any notifications are invoked after the provided fn exits (even if the // process is interrupted by an OS termination signal). Notifications are only invoked once // per Handler instance, so calling Run more than once will not behave as the user expects. func (h *Handler) Run(fn func() error) error { ch := make(chan os.Signal, 1) signal.Notify(ch, terminationSignals...) defer func() { signal.Stop(ch) close(ch) }() go func() { sig, ok := <-ch if !ok { return } h.Signal(sig) }() defer h.Close() return fn() } ================================================ FILE: mdz/pkg/term/term.go ================================================ /* Copyright 2016 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 term import ( "io" "os" "runtime" "github.com/moby/term" ) // SafeFunc is a function to be invoked by TTY. type SafeFunc func() error // TTY helps invoke a function and preserve the state of the terminal, even if the process is // terminated during execution. It also provides support for terminal resizing for remote command // execution/attachment. type TTY struct { // In is a reader representing stdin. It is a required field. In io.Reader // Out is a writer representing stdout. It must be set to support terminal resizing. It is an // optional field. Out io.Writer // Raw is true if the terminal should be set raw. Raw bool // TryDev indicates the TTY should try to open /dev/tty if the provided input // is not a file descriptor. TryDev bool // Parent is an optional interrupt handler provided to this function - if provided // it will be invoked after the terminal state is restored. If it is not provided, // a signal received during the TTY will result in os.Exit(0) being invoked. Parent *Handler // sizeQueue is set after a call to MonitorSize() and is used to monitor SIGWINCH signals when the // user's terminal resizes. // sizeQueue *sizeQueue } func NewTTY() *TTY { tty := &TTY{} stdin, stdout, _ := term.StdStreams() tty.In = stdin tty.Out = stdout return tty } // IsTerminalIn returns true if t.In is a terminal. Does not check /dev/tty // even if TryDev is set. func (t TTY) IsTerminalIn() bool { return IsTerminal(t.In) } // IsTerminalOut returns true if t.Out is a terminal. Does not check /dev/tty // even if TryDev is set. func (t TTY) IsTerminalOut() bool { return IsTerminal(t.Out) } // IsTerminal returns whether the passed object is a terminal or not func IsTerminal(i interface{}) bool { _, terminal := term.GetFdInfo(i) return terminal } // AllowsColorOutput returns true if the specified writer is a terminal and // the process environment indicates color output is supported and desired. func AllowsColorOutput(w io.Writer) bool { if !IsTerminal(w) { return false } // https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals if os.Getenv("TERM") == "dumb" { return false } // https://no-color.org/ if _, nocolor := os.LookupEnv("NO_COLOR"); nocolor { return false } // On Windows WT_SESSION is set by the modern terminal component. // Older terminals have poor support for UTF-8, VT escape codes, etc. if runtime.GOOS == "windows" && os.Getenv("WT_SESSION") == "" { return false } return true } // Safe invokes the provided function and will attempt to ensure that when the // function returns (or a termination signal is sent) that the terminal state // is reset to the condition it was in prior to the function being invoked. If // t.Raw is true the terminal will be put into raw mode prior to calling the function. // If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file // will be opened (if available). func (t TTY) Safe(fn SafeFunc) error { inFd, isTerminal := term.GetFdInfo(t.In) if !isTerminal && t.TryDev { if f, err := os.Open("/dev/tty"); err == nil { defer f.Close() inFd = f.Fd() isTerminal = term.IsTerminal(inFd) } } if !isTerminal { return fn() } var state *term.State var err error if t.Raw { state, err = term.MakeRaw(inFd) } else { state, err = term.SaveState(inFd) } if err != nil { return err } return Chain(t.Parent, func() { term.RestoreTerminal(inFd, state) }).Run(fn) } ================================================ FILE: mdz/pkg/version/version.go ================================================ /* Copyright The TensorChord Inc. Copyright The BuildKit Authors. Copyright The containerd 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 version import ( "fmt" "regexp" "runtime" "strings" "sync" ) var ( // Package is filled at linking time Package = "github.com/tensorchord/openmodelz/agent" HelmChartVersion = "0.0.15" // Revision is filled with the VCS (e.g. git) revision being used to build // the program at linking time. Revision = "" version = "0.0.0+unknown" buildDate = "1970-01-01T00:00:00Z" // output from `date -u +'%Y-%m-%dT%H:%M:%SZ'` gitCommit = "" // output from `git rev-parse HEAD` gitTag = "" // output from `git describe --exact-match --tags HEAD` (if clean tree state) gitTreeState = "" // determined from `git status --porcelain`. either 'clean' or 'dirty' developmentFlag = "false" ) // Version contains OpenModelz version information type Version struct { Version string BuildDate string GitCommit string GitTag string GitTreeState string GoVersion string Compiler string Platform string } func (v Version) String() string { return v.Version } // SetGitTagForE2ETest sets the gitTag for test purpose. func SetGitTagForE2ETest(tag string) { gitTag = tag } // GetOpenModelzVersion gets OpenModelz version information func GetOpenModelzVersion() string { var versionStr string if gitCommit != "" && gitTag != "" && gitTreeState == "clean" && developmentFlag == "false" { // if we have a clean tree state and the current commit is tagged, // this is an official release. versionStr = gitTag } else { // otherwise formulate a version string based on as much metadata // information we have available. if strings.HasPrefix(version, "v") { versionStr = version } else { versionStr = "v" + version } if len(gitCommit) >= 7 { versionStr += "+" + gitCommit[0:7] if gitTreeState != "clean" { versionStr += ".dirty" } } else { versionStr += "+unknown" } } return versionStr } // GetVersion returns the version information func GetVersion() Version { return Version{ Version: GetOpenModelzVersion(), BuildDate: buildDate, GitCommit: gitCommit, GitTag: gitTag, GitTreeState: gitTreeState, GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } } var ( reRelease *regexp.Regexp reDev *regexp.Regexp reOnce sync.Once ) func UserAgent() string { version := GetVersion().String() reOnce.Do(func() { reRelease = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+$`) reDev = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+`) }) if matches := reRelease.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] } else if matches := reDev.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] + "-dev" } return "modelz/" + version } ================================================ FILE: modelzetes/.dockerignore ================================================ ./faas-netes /yaml /yaml_armhf /yaml_arm64 /chart /contrib /artifacts /hack /docs /.git ================================================ FILE: modelzetes/.gitattributes ================================================ yaml/* linguist-generated=true ================================================ FILE: modelzetes/.gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ .idea bin/ **/password.txt **/gateway-password.txt .vscode of_kind_portforward.pid /kind* /kubectl /yaml_armhf /yaml_arm64 /broker-* /chart/pro-builder/out /chart/pro-builder/payload.txt /pgconnector.yaml jwt_key jwt_key.pub /*.pid .tools/ ================================================ FILE: modelzetes/Dockerfile ================================================ FROM ubuntu:22.04 LABEL maintainer="modelz-support@tensorchord.ai" COPY modelzetes /usr/bin/modelzetes ENTRYPOINT ["/usr/bin/modelzetes"] ================================================ FILE: modelzetes/LICENSE ================================================ MIT License Copyright (c) 2023 TensorChord Inc. Copyright (c) 2020 OpenFaaS Author(s) Copyright (c) 2017 Alex Ellis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: modelzetes/Makefile ================================================ # Copyright 2022 TensorChord Inc. # # The old school Makefile, following are required targets. The Makefile is written # to allow building multiple binaries. You are free to add more targets or change # existing implementations, as long as the semantics are preserved. # # make - default to 'build' target # make lint - code analysis # make test - run unit test (or plus integration test) # make build - alias to build-local target # make build-local - build local binary targets # make build-linux - build linux binary targets # make container - build containers # $ docker login registry -u username -p xxxxx # make push - push containers # make clean - clean up targets # # Not included but recommended targets: # make e2e-test # # The makefile is also responsible to populate project version information. # # # Tweak the variables based on your project. # # This repo's root import path (under GOPATH). ROOT := github.com/tensorchord/openmodelz/modelzetes # Target binaries. You can build multiple binaries for a single project. TARGETS := modelzetes # Container image prefix and suffix added to targets. # The final built images are: # $[REGISTRY]/$[IMAGE_PREFIX]$[TARGET]$[IMAGE_SUFFIX]:$[VERSION] # $[REGISTRY] is an item from $[REGISTRIES], $[TARGET] is an item from $[TARGETS]. IMAGE_PREFIX ?= $(strip ) IMAGE_SUFFIX ?= $(strip ) # Container registries. REGISTRY ?= ghcr.io/tensorchord # Container registry for base images. BASE_REGISTRY ?= docker.io BASE_REGISTRY_USER ?= modelzai # Disable CGO by default. CGO_ENABLED ?= 0 # # These variables should not need tweaking. # # It's necessary to set this because some environments don't link sh -> bash. export SHELL := bash # It's necessary to set the errexit flags for the bash shell. export SHELLOPTS := errexit PACKAGE_NAME := github.com/tensorchord/openmodelz/modelzetes GOLANG_CROSS_VERSION ?= v1.17.6 # Project main package location (can be multiple ones). CMD_DIR := ./cmd # Project output directory. OUTPUT_DIR := ./bin DEBUG_DIR := ./debug-bin # Build directory. BUILD_DIR := ./build # Current version of the project. VERSION ?= $(shell git describe --match 'v[0-9]*' --always --tags --abbrev=0) BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') GIT_COMMIT=$(shell git rev-parse HEAD) GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi) GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi) GITSHA ?= $(shell git rev-parse --short HEAD) # Track code version with Docker Label. DOCKER_LABELS ?= git-describe="$(shell date -u +v%Y%m%d)-$(shell git describe --tags --always --dirty)" # Golang standard bin directory. GOPATH ?= $(shell go env GOPATH) GOROOT ?= $(shell go env GOROOT) BIN_DIR := $(GOPATH)/bin GOLANGCI_LINT := $(BIN_DIR)/golangci-lint # check if we need embed the dashboard DASHBOARD_BUILD ?= debug # Default golang flags used in build and test # -mod=vendor: force go to use the vendor files instead of using the `$GOPATH/pkg/mod` # -p: the number of programs that can be run in parallel # -count: run each test and benchmark 1 times. Set this flag to disable test cache export GOFLAGS ?= -count=1 # # Define all targets. At least the following commands are required: # # All targets. .PHONY: help lint test build container push addlicense debug debug-local build-local generate clean test-local addlicense-install release build-image .DEFAULT_GOAL:=build build: build-local ## Build the release version of envd help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) debug: debug-local ## Build the debug version of envd # more info about `GOGC` env: https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint lint: $(GOLANGCI_LINT) ## Lint GO code @$(GOLANGCI_LINT) run $(GOLANGCI_LINT): curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin mockgen-install: go install github.com/golang/mock/mockgen@v1.6.0 addlicense-install: go install github.com/google/addlicense@latest sqlc-install: go install github.com/kyleconroy/sqlc/cmd/sqlc@latest # https://github.com/swaggo/swag/pull/1322, we should use master instead of latest for now. swag-install: go install github.com/swaggo/swag/cmd/swag@v1.8.7 build-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags $(DASHBOARD_BUILD) -trimpath -v -o $(OUTPUT_DIR)/$${target} \ -ldflags "-s -w -X $(ROOT)/pkg/version.version=$(VERSION) -X $(ROOT)/pkg/version.buildDate=$(BUILD_DATE) -X $(ROOT)/pkg/version.gitCommit=$(GIT_COMMIT) -X $(ROOT)/pkg/version.gitTreeState=$(GIT_TREE_STATE)" \ $(CMD_DIR)/$${target}; \ done # It is used by vscode to attach into the process. debug-local: @for target in $(TARGETS); do \ CGO_ENABLED=$(CGO_ENABLED) go build -tags $(DASHBOARD_BUILD) -trimpath \ -v -o $(DEBUG_DIR)/$${target} \ -gcflags='all=-N -l' \ $(CMD_DIR)/$${target}; \ done addlicense: addlicense-install ## Add license to GO code files addlicense -l mpl -c "TensorChord Inc." $$(find . -type f -name '*.go' | grep -v pkg/docs/docs.go) test-local: @go test -tags=$(DASHBOARD_BUILD) -v -race -coverprofile=coverage.out ./... test: ## Run the tests @go test -tags=$(DASHBOARD_BUILD) -race -coverpkg=./pkg/... -coverprofile=coverage.out ./... @go tool cover -func coverage.out | tail -n 1 | awk '{ print "Total coverage: " $$3 }' clean: ## Clean the outputs and artifacts @-rm -vrf ${OUTPUT_DIR} @-rm -vrf ${DEBUG_DIR} @-rm -vrf build dist .eggs *.egg-info fmt: swag-install ## Run go fmt against code. go fmt ./... swag fmt vet: ## Run go vet against code. go vet ./... swag: swag-install swag init -g ./cmd/modelzetes/main.go --parseDependency --output ./pkg/docs build-image: build-local docker build -t ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/modelzetes:dev -f Dockerfile ./bin docker push ${BASE_REGISTRY}/${BASE_REGISTRY_USER}/modelzetes:dev release: @if [ ! -f ".release-env" ]; then \ echo "\033[91m.release-env is required for release\033[0m";\ exit 1;\ fi docker run \ --rm \ --privileged \ -e CGO_ENABLED=1 \ --env-file .release-env \ -v /var/run/docker.sock:/var/run/docker.sock \ -v `pwd`:/go/src/$(PACKAGE_NAME) \ -v `pwd`/sysroot:/sysroot \ -w /go/src/$(PACKAGE_NAME) \ goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ release --rm-dist ================================================ FILE: modelzetes/artifacts/crds/tensorchord.ai_inferences.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.5.0 creationTimestamp: null name: inferences.tensorchord.ai spec: group: tensorchord.ai names: kind: Inference listKind: InferenceList plural: inferences singular: inference scope: Namespaced versions: - additionalPrinterColumns: - jsonPath: .spec.image name: Image type: string name: v2alpha1 schema: openAPIV3Schema: description: Inference describes an Inference type: object required: - spec properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: InferenceSpec defines the desired state of Inference type: object required: - image - name properties: annotations: description: Annotations are metadata for inferences which may be used by the faas-provider or the gateway type: object additionalProperties: type: string command: description: Command to run when starting the type: string constraints: description: Constraints are specific to the operator. type: array items: type: string envVars: description: EnvVars can be provided to set environment variables for the inference runtime. type: object additionalProperties: type: string framework: description: Framework is the inference framework. type: string http_probe_path: description: HTTPProbePath is the path of the http probe. type: string image: type: string labels: description: Labels are metadata for inferences which may be used by the faas-provider or the gateway type: object additionalProperties: type: string name: type: string port: description: Port is the port exposed by the inference. type: integer format: int32 resources: description: Limits for inference type: object properties: claims: description: "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. \n This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. \n This field is immutable. It can only be set for containers." type: array items: description: ResourceClaim references one entry in PodSpec.ResourceClaims. type: object required: - name properties: name: description: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container. type: string x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object additionalProperties: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ anyOf: - type: integer - type: string x-kubernetes-int-or-string: true requests: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object additionalProperties: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scaling: description: Scaling is the scaling configuration for the inference. type: object properties: max_replicas: description: MaxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. It cannot be less that minReplicas. It defaults to 1. type: integer format: int32 min_replicas: description: MinReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. It defaults to 0. type: integer format: int32 startup_duration: description: StartupDuration is the duration of startup time. type: integer format: int32 target_load: description: TargetLoad is the target load. In capacity mode, it is the expected number of the inflight requests per replica. type: integer format: int32 type: description: Type is the scaling type. It can be either "capacity" or "rps". Default is "capacity". type: string zero_duration: description: ZeroDuration is the duration of zero load before scaling down to zero. Default is 5 minutes. type: integer format: int32 secrets: description: Secrets list of secrets to be made available to inference type: array items: type: string served: true storage: true subresources: {} status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: [] ================================================ FILE: modelzetes/artifacts/samples/v2alpha1.yaml ================================================ apiVersion: tensorchord.ai/v2alpha1 kind: Inference metadata: name: demo namespace: default spec: name: demo framework: mosec image: modelzai/llm-bloomz-560m:23.06.13 scaling: min_replicas: 0 max_replicas: 1 target_load: 100 type: capacity zero_duration: 60 startup_duration: 600 resources: requests: cpu: "3" memory: 12Gi ================================================ FILE: modelzetes/buf.yaml ================================================ apiVersion: v1 items: - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-02T07:37:53Z" generation: 1 labels: inference: 895f81b5-7e60-49b0-a2c1-145fc884cac7 name: 895f81b5-7e60-49b0-a2c1-145fc884cac7 namespace: modelz-a39caa1c-0b03-4054-b75f-1f1cf8424b01 resourceVersion: "76627989" uid: 8f0b842a-be19-4dae-9fa0-986e830b0670 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-imagebind:23.05.2 ai.tensorchord.domain: https://imagebind-ogeboq7ciyo3sq0t.modelz.tech ai.tensorchord.source: docker constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-imagebind:23.05.2 labels: ai.tensorchord.framework: mosec ai.tensorchord.name: imagebind ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 1m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 895f81b5-7e60-49b0-a2c1-145fc884cac7 resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-05-06T09:28:13Z" generation: 1 labels: inference: c5e72dcd-bee6-4b82-b08b-47cc9f4a2c1c name: c5e72dcd-bee6-4b82-b08b-47cc9f4a2c1c namespace: modelz-a39caa1c-0b03-4054-b75f-1f1cf8424b01 resourceVersion: "50967692" uid: 1f201ac0-ed45-4cf3-b084-47752fabfb5c spec: annotations: ai.tensorchord.docker.image: docker.io/starkind/moss:v1.11 ai.tensorchord.domain: https://moss-jmclvinro4plugtp.modelz.tech ai.tensorchord.source: docker constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: docker.io/starkind/moss:v1.11 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: moss ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: c5e72dcd-bee6-4b82-b08b-47cc9f4a2c1c resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-05-15T03:41:02Z" generation: 3 labels: inference: 0b1e753e-9703-41c8-9311-b6bb943bcbda name: 0b1e753e-9703-41c8-9311-b6bb943bcbda namespace: modelz-a9d660cf-2537-4e48-bcaf-adf8470a83c0 resourceVersion: "60441705" uid: ee8818d1-1e46-4d0b-bab3-3b1893ba4703 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4 ai.tensorchord.domain: https://sd-uif34dg3x17kb21j.modelz.tech ai.tensorchord.inference.spec: '{"name":"0b1e753e-9703-41c8-9311-b6bb943bcbda","image":"us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4","envVars":{"GRADIO_SERVER_NAME":"0.0.0.0","GRADIO_SERVER_PORT":"7860","HF_ENDPOINT":"http://hfserver.default:8080"},"constraints":["ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g"],"labels":{"ai.tensorchord.framework":"gradio","ai.tensorchord.name":"sd","ai.tensorchord.port":"7860","ai.tensorchord.region":"us-central1","ai.tensorchord.scale.max":"1","ai.tensorchord.scale.min":"0","ai.tensorchord.scale.target":"10","ai.tensorchord.scale.type":"capacity","ai.tensorchord.scale.zero-duration":"5m0s","ai.tensorchord.server-resource":"nvidia-ada-l4-8c-32g","ai.tensorchord.startup-duration":"10m0s","ai.tensorchord.vendor":"gcp","app":"0b1e753e-9703-41c8-9311-b6bb943bcbda","controller":"0b1e753e-9703-41c8-9311-b6bb943bcbda","inference":"0b1e753e-9703-41c8-9311-b6bb943bcbda"},"annotations":{"ai.tensorchord.docker.image":"us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4","ai.tensorchord.domain":"https://sd-uif34dg3x17kb21j.modelz.tech","ai.tensorchord.inference.spec":"{\"name\":\"0b1e753e-9703-41c8-9311-b6bb943bcbda\",\"image\":\"us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4\",\"constraints\":[\"ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g\"],\"labels\":{\"ai.tensorchord.framework\":\"gradio\",\"ai.tensorchord.name\":\"sd\",\"ai.tensorchord.port\":\"7860\",\"ai.tensorchord.region\":\"us-central1\",\"ai.tensorchord.scale.max\":\"1\",\"ai.tensorchord.scale.min\":\"0\",\"ai.tensorchord.scale.target\":\"10\",\"ai.tensorchord.scale.type\":\"capacity\",\"ai.tensorchord.scale.zero-duration\":\"5m0s\",\"ai.tensorchord.server-resource\":\"nvidia-ada-l4-8c-32g\",\"ai.tensorchord.startup-duration\":\"10m0s\",\"ai.tensorchord.vendor\":\"gcp\"},\"annotations\":{\"ai.tensorchord.docker.image\":\"us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4\",\"ai.tensorchord.domain\":\"https://sd-uif34dg3x17kb21j.modelz.tech\",\"ai.tensorchord.source\":\"docker\"},\"resources\":{\"limits\":{\"nvidia.com/gpu\":\"1\"},\"requests\":{\"cpu\":\"6500m\",\"memory\":\"24Gi\",\"nvidia.com/gpu\":\"1\"}}}","ai.tensorchord.source":"docker","prometheus.io.scrape":"false"},"resources":{"limits":{"nvidia.com/gpu":"1"},"requests":{"cpu":"6500m","memory":"24Gi","nvidia.com/gpu":"1"}}}' ai.tensorchord.source: docker prometheus.io.scrape: "false" constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g envVars: GRADIO_SERVER_NAME: 0.0.0.0 GRADIO_SERVER_PORT: "7860" HF_ENDPOINT: http://hfserver.default:8080 image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: sd ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp app: 0b1e753e-9703-41c8-9311-b6bb943bcbda controller: 0b1e753e-9703-41c8-9311-b6bb943bcbda inference: 0b1e753e-9703-41c8-9311-b6bb943bcbda name: 0b1e753e-9703-41c8-9311-b6bb943bcbda resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-05-18T11:50:26Z" generation: 1 labels: inference: 2fb46857-d0ea-4237-8165-4defbe037bbd name: 2fb46857-d0ea-4237-8165-4defbe037bbd namespace: modelz-a9d660cf-2537-4e48-bcaf-adf8470a83c0 resourceVersion: "62499285" uid: 7edbbfe2-809f-4196-9ac1-fc47f731e817 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1 ai.tensorchord.domain: https://ws-c2om23pvkpaeirf9.modelz.tech ai.tensorchord.source: docker constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g envVars: HF_HUB_OFFLINE: "true" image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1 labels: ai.tensorchord.framework: mosec ai.tensorchord.name: ws ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 2fb46857-d0ea-4237-8165-4defbe037bbd resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-02T12:55:10Z" generation: 1 labels: inference: 41d0d82e-31e4-4704-9e0d-49eec10a455b name: 41d0d82e-31e4-4704-9e0d-49eec10a455b namespace: modelz-a9d660cf-2537-4e48-bcaf-adf8470a83c0 resourceVersion: "76837944" uid: b562acde-3849-4ecb-ac8e-30ba5252c7a5 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-stable-diffusion:23.04.1 ai.tensorchord.domain: https://mosec-sd-3n7j7i3oh28sp2jw.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 0d603dd5-6d74-4e94-bc70-e5f223dd9d81 constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g envVars: HF_HUB_OFFLINE: "true" image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-stable-diffusion:23.04.1 labels: ai.tensorchord.framework: mosec ai.tensorchord.name: mosec-sd ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 41d0d82e-31e4-4704-9e0d-49eec10a455b resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-02T12:53:20Z" generation: 1 labels: inference: 431e5913-f5f3-45dd-a1b3-139917a07cf9 name: 431e5913-f5f3-45dd-a1b3-139917a07cf9 namespace: modelz-a9d660cf-2537-4e48-bcaf-adf8470a83c0 resourceVersion: "76836748" uid: 682dec57-3eb6-4925-af3b-c56a81073009 spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 ai.tensorchord.domain: https://ddddd2-wqkeotxfansceqp2.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 074852a9-b324-4c02-95b1-da06ec98a964 constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: ddddd2 ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 431e5913-f5f3-45dd-a1b3-139917a07cf9 resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-02T07:38:54Z" generation: 1 labels: inference: 660a2a0a-d9b1-4b86-823b-4dc86f89cd02 name: 660a2a0a-d9b1-4b86-823b-4dc86f89cd02 namespace: modelz-a9d660cf-2537-4e48-bcaf-adf8470a83c0 resourceVersion: "76628665" uid: f89d820c-7c7e-43d3-a73b-cafe57627006 spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 ai.tensorchord.domain: https://ddd-oky85wto4kxp0uyz.modelz.tech ai.tensorchord.source: docker constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: ddd ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 660a2a0a-d9b1-4b86-823b-4dc86f89cd02 resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-02T07:29:34Z" generation: 1 labels: inference: 744ef0a4-51ea-438c-b013-f5c3b1c1548f name: 744ef0a4-51ea-438c-b013-f5c3b1c1548f namespace: modelz-a9d660cf-2537-4e48-bcaf-adf8470a83c0 resourceVersion: "76622421" uid: a7ae0743-e166-4591-aec0-33a23101506d spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 ai.tensorchord.domain: https://dollyv2-9fhuv221e3py7j8t.modelz.tech ai.tensorchord.source: docker constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: dollyv2 ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 744ef0a4-51ea-438c-b013-f5c3b1c1548f resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-02T07:30:30Z" generation: 1 labels: inference: aeaf50f9-1b7e-4a37-87a1-6947a65fc07c name: aeaf50f9-1b7e-4a37-87a1-6947a65fc07c namespace: modelz-a9d660cf-2537-4e48-bcaf-adf8470a83c0 resourceVersion: "76623043" uid: 8ce0ace2-a710-4ba0-96ad-be2f100ad8fb spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 ai.tensorchord.domain: https://dollyv2-l3idk5o2yhzpgqmc.modelz.tech ai.tensorchord.source: docker constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: dollyv2 ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: aeaf50f9-1b7e-4a37-87a1-6947a65fc07c resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-27T04:20:35Z" generation: 1 labels: inference: 8ed9973e-8bf0-45d3-8f9a-24a228e1a1f9 name: 8ed9973e-8bf0-45d3-8f9a-24a228e1a1f9 namespace: modelz-ba5ce029-180a-445d-89a8-bb111b29c0fe resourceVersion: "98647435" uid: 92b09491-086f-4be0-baf1-c4a6d82cb2c5 spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.domain: https://test-y5qie3qkgqba99fw.modelz.tech ai.tensorchord.huggingface.space: https://huggingface.co/spaces/abidlabs/en2fr ai.tensorchord.source: huggingface constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g image: registry.hf.space/abidlabs-en2fr:latest labels: ai.tensorchord.framework: gradio ai.tensorchord.name: test ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "1" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 8ed9973e-8bf0-45d3-8f9a-24a228e1a1f9 resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-15T06:59:52Z" generation: 1 labels: inference: b33863ba-9c0f-4edb-8f95-00a522a0c335 name: b33863ba-9c0f-4edb-8f95-00a522a0c335 namespace: modelz-ba5ce029-180a-445d-89a8-bb111b29c0fe resourceVersion: "88434776" uid: 48cf0cdc-defa-4986-ad55-13a4769252e3 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4 ai.tensorchord.domain: https://test-gradio-yf9ws7k1yimrgrcd.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 9c0137e7-5732-49f9-b6bd-e920f1c6e4d4 constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: test-gradio ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: b33863ba-9c0f-4edb-8f95-00a522a0c335 resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-05T03:27:22Z" generation: 1 labels: inference: 0741b1e5-f556-439d-adc7-9cba932d2d73 name: 0741b1e5-f556-439d-adc7-9cba932d2d73 namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "79293297" uid: e2183d12-afa4-443c-ab59-01af0f581aba spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1 ai.tensorchord.domain: https://whisper-9yx10apmsgdtb0di.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: c2974e48-7fc3-4690-8910-a2e98abc82d1 constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g envVars: HF_HUB_OFFLINE: "true" image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1 labels: ai.tensorchord.framework: mosec ai.tensorchord.name: whisper ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 0741b1e5-f556-439d-adc7-9cba932d2d73 resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-05T03:25:48Z" generation: 1 labels: inference: 3c2328a1-4c61-41b8-b670-6df5813d8c51 name: 3c2328a1-4c61-41b8-b670-6df5813d8c51 namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "79292168" uid: f0da8e43-2235-4910-98d2-76621fdd2e4a spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 ai.tensorchord.domain: https://dolly-p52q4f5qhpnayw00.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 10a47537-d1cc-4aa5-a9bd-173060cf812e constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-ramananth1-dolly-v2:23.05.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: dolly ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 3c2328a1-4c61-41b8-b670-6df5813d8c51 resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-29T03:27:54Z" generation: 1 labels: inference: 480f37dc-57b5-4677-a2e7-9e34621dc4e3 name: 480f37dc-57b5-4677-a2e7-9e34621dc4e3 namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "100326009" uid: fc090985-3971-41f3-8777-6785ea328fa5 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-tts-vits:23.06.7 ai.tensorchord.domain: https://vits-bench-f5sn326og2gb20lv.modelz.tech ai.tensorchord.source: docker constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-tts-vits:23.06.7 labels: ai.tensorchord.framework: mosec ai.tensorchord.name: vits-bench ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "3" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "5" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 1m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 480f37dc-57b5-4677-a2e7-9e34621dc4e3 resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-05T03:26:49Z" generation: 1 labels: inference: 5f10d4e3-e9b0-46e1-9265-6d2ecb98930e name: 5f10d4e3-e9b0-46e1-9265-6d2ecb98930e namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "79292878" uid: d77bb280-b397-4e41-a2f5-9bf180f61596 spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stabilityai-stablelm-tuned-alpha-chat:23.05.1 ai.tensorchord.domain: https://stablelm-7p1b95fcxhyeiriy.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 64462bf4-2373-4273-ae76-433707410bcd constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stabilityai-stablelm-tuned-alpha-chat:23.05.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: stablelm ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 5f10d4e3-e9b0-46e1-9265-6d2ecb98930e resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-05T03:27:12Z" generation: 1 labels: inference: 81343170-1688-491c-a139-87037f3b8dcd name: 81343170-1688-491c-a139-87037f3b8dcd namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "79293174" uid: 8d63f8de-7bf6-40a8-8997-fa69fce037d0 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-visual-chatgpt:23.04.1 ai.tensorchord.domain: https://visual-chatgpt-0e0iez8r17xzxksf.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 78100725-18ca-41e9-b7a8-7ac573f6280b constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g envVars: OPENAI_API_KEY: a image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-visual-chatgpt:23.04.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: visual-chatgpt ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 81343170-1688-491c-a139-87037f3b8dcd resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-09T10:50:48Z" generation: 1 labels: inference: 8e365318-53ed-449e-ac15-f7ab8750d79f name: 8e365318-53ed-449e-ac15-f7ab8750d79f namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "83183128" uid: 5fca43a9-a9e4-4bd0-8b9b-5ffa5b730ab8 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/llm-bloomz-560m:23.06.12 ai.tensorchord.domain: https://a-k7l6t2w8osvf4trq.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 92721d83-2dba-460f-9bde-b7eee4ea4950 constraints: - ai.tensorchord.server-resource=cpu-4c-16g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/llm-bloomz-560m:23.06.12 labels: ai.tensorchord.framework: other ai.tensorchord.name: a ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: cpu-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: 8e365318-53ed-449e-ac15-f7ab8750d79f resources: requests: cpu: "3" memory: 8Gi - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-19T09:15:48Z" generation: 1 labels: inference: c6637a2e-0812-4600-b9d5-d1fbc01a68f8 name: c6637a2e-0812-4600-b9d5-d1fbc01a68f8 namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "92057903" uid: b59cdece-e083-40bc-8a39-25d30ad9a753 spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.domain: https://hf-9oln0cnymw5eyzzf.modelz.tech ai.tensorchord.huggingface.space: https://huggingface.co/spaces/HuggingFaceH4/falcon-chat ai.tensorchord.source: huggingface constraints: - ai.tensorchord.server-resource=cpu-4c-16g image: registry.hf.space/huggingfaceh4-falcon-chat:latest labels: ai.tensorchord.framework: gradio ai.tensorchord.name: hf ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: cpu-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: c6637a2e-0812-4600-b9d5-d1fbc01a68f8 resources: requests: cpu: "3" memory: 8Gi - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-05T03:26:36Z" generation: 1 labels: inference: c7f18c10-4c99-471c-b9d3-dd906e02cc7f name: c7f18c10-4c99-471c-b9d3-dd906e02cc7f namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "79292725" uid: 36d9a032-2cda-4abb-ad8e-aad182e6850f spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4 ai.tensorchord.domain: https://sd-web-co4ubmbo9k6c751q.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 755b5ba6-ce50-4ef9-bbe0-e00c3cecb380 constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-stable-diffusion:23.04.4 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: sd-web ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: c7f18c10-4c99-471c-b9d3-dd906e02cc7f resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-05T03:26:16Z" generation: 1 labels: inference: d0c06992-94b5-4a99-90b4-a9900335acfe name: d0c06992-94b5-4a99-90b4-a9900335acfe namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "79292483" uid: 42b1485b-9ed6-49ed-81b7-fb9742931300 spec: annotations: ai.tensorchord.command: python app.py ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-microsoft-hugginggpt:23.05.1 ai.tensorchord.domain: https://huggingpt-5vi5th3l7llhgq4h.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 8dbec890-ff7b-467e-ada2-e37f055f5c4f constraints: - ai.tensorchord.server-resource=nvidia-ada-l4-8c-32g image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/gradio-microsoft-hugginggpt:23.05.1 labels: ai.tensorchord.framework: gradio ai.tensorchord.name: huggingpt ai.tensorchord.port: "7860" ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-ada-l4-8c-32g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: d0c06992-94b5-4a99-90b4-a9900335acfe resources: limits: nvidia.com/gpu: "1" requests: cpu: 6500m memory: 24Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-06-05T03:26:26Z" generation: 1 labels: inference: f3d6bedf-447d-4f9b-b27f-1f03f43a1f97 name: f3d6bedf-447d-4f9b-b27f-1f03f43a1f97 namespace: modelz-cd4a928c-7c66-4934-beb2-98dd82d672ce resourceVersion: "79292608" uid: 68ee40dd-6366-4229-941a-16a5c019956d spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-stable-diffusion:23.04.1 ai.tensorchord.domain: https://sd-rl41fip7qafvrjzl.modelz.tech ai.tensorchord.source: docker ai.tensorchord.template-id: 6fe4cfb5-4e8b-4d34-ba03-de534343361e constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g envVars: HF_HUB_OFFLINE: "true" image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-stable-diffusion:23.04.1 labels: ai.tensorchord.framework: mosec ai.tensorchord.name: sd ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "1" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp name: f3d6bedf-447d-4f9b-b27f-1f03f43a1f97 resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" - apiVersion: tensorchord.ai/v1 kind: Inference metadata: creationTimestamp: "2023-05-26T08:34:40Z" generation: 2 labels: inference: 94e62a6d-adde-4ff2-9053-7b15b6f18727 name: 94e62a6d-adde-4ff2-9053-7b15b6f18727 namespace: modelz-d3524a71-c17c-4c92-8faf-8603f02f4713 resourceVersion: "99387184" uid: 5b11d701-b474-4cad-8fff-bf107c5b3992 spec: annotations: ai.tensorchord.docker.image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1 ai.tensorchord.domain: https://demo-e9ijwrk2il0kvzl7.modelz.tech ai.tensorchord.inference.spec: '{"name":"94e62a6d-adde-4ff2-9053-7b15b6f18727","image":"us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1","envVars":{"HF_HUB_OFFLINE":"true"},"constraints":["ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g"],"labels":{"ai.tensorchord.framework":"mosec","ai.tensorchord.name":"demo","ai.tensorchord.region":"us-central1","ai.tensorchord.scale.max":"1","ai.tensorchord.scale.min":"0","ai.tensorchord.scale.target":"10","ai.tensorchord.scale.type":"capacity","ai.tensorchord.scale.zero-duration":"5m0s","ai.tensorchord.server-resource":"nvidia-tesla-t4-4c-16g","ai.tensorchord.startup-duration":"10m0s","ai.tensorchord.vendor":"gcp"},"annotations":{"ai.tensorchord.docker.image":"us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1","ai.tensorchord.domain":"https://demo-e9ijwrk2il0kvzl7.modelz.tech","ai.tensorchord.source":"docker"},"resources":{"limits":{"nvidia.com/gpu":"1"},"requests":{"cpu":"3","memory":"12Gi","nvidia.com/gpu":"1"}}}' ai.tensorchord.source: docker prometheus.io.scrape: "false" constraints: - ai.tensorchord.server-resource=nvidia-tesla-t4-4c-16g envVars: HF_ENDPOINT: http://hfserver.default:8080 HF_HUB_OFFLINE: "true" MOSEC_PORT: "8080" image: us-central1-docker.pkg.dev/nth-guide-378813/modelzai/mosec-whisper:23.04.1 labels: ai.tensorchord.framework: mosec ai.tensorchord.name: demo ai.tensorchord.region: us-central1 ai.tensorchord.scale.max: "0" ai.tensorchord.scale.min: "0" ai.tensorchord.scale.target: "10" ai.tensorchord.scale.type: capacity ai.tensorchord.scale.zero-duration: 5m0s ai.tensorchord.server-resource: nvidia-tesla-t4-4c-16g ai.tensorchord.startup-duration: 10m0s ai.tensorchord.vendor: gcp app: 94e62a6d-adde-4ff2-9053-7b15b6f18727 controller: 94e62a6d-adde-4ff2-9053-7b15b6f18727 inference: 94e62a6d-adde-4ff2-9053-7b15b6f18727 name: 94e62a6d-adde-4ff2-9053-7b15b6f18727 resources: limits: nvidia.com/gpu: "1" requests: cpu: "3" memory: 12Gi nvidia.com/gpu: "1" kind: List metadata: resourceVersion: "" selfLink: "" ================================================ FILE: modelzetes/cmd/modelzetes/main.go ================================================ package main import ( "fmt" "os" cli "github.com/urfave/cli/v2" "k8s.io/klog" "github.com/tensorchord/openmodelz/modelzetes/pkg/app" "github.com/tensorchord/openmodelz/modelzetes/pkg/version" ) func run(args []string) error { cli.VersionPrinter = func(c *cli.Context) { fmt.Println(c.App.Name, version.Package, c.App.Version, version.Revision) } klog.InitFlags(nil) a := app.New() return a.Run(args) } func handleErr(err error) { if err == nil { return } klog.Error(err) os.Exit(1) } func main() { err := run(os.Args) handleErr(err) } ================================================ FILE: modelzetes/hack/boilerplate.go.txt ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ ================================================ FILE: modelzetes/hack/print-codegen-version.sh ================================================ #!/bin/bash # This scripts exists primarily so that it can be used in the Makefile. # It is needed because the `($shell ...)` command was having issues with the pipe. # Extracting it to a script was the simplest solution. grep 'k8s.io/code-generator' go.mod | awk '{print $2}' ================================================ FILE: modelzetes/hack/update-codegen.sh ================================================ #!/usr/bin/env bash # copied from: https://github.com/weaveworks/flagger/tree/master/hack set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(git rev-parse --show-toplevel)/modelzetes echo ">> SCRIPT_ROOT ${SCRIPT_ROOT}" GET_PKG_LOCATION() { pkg_name="${1:-}" pkg_location="$(go list -m -f '{{.Dir}}' "${pkg_name}" 2>/dev/null)" if [ "${pkg_location}" = "" ]; then echo "${pkg_name} is missing. Running 'go mod download'." go mod download pkg_location=$(go list -m -f '{{.Dir}}' "${pkg_name}") fi echo "${pkg_location}" } # Grab code-generator version from go.sum CODEGEN_PKG="$(GET_PKG_LOCATION "k8s.io/code-generator")" echo ">> Using ${CODEGEN_PKG}" # Grab openapi-gen version from go.mod OPENAPI_PKG="$(GET_PKG_LOCATION 'k8s.io/kube-openapi')" echo ">> Using ${OPENAPI_PKG}" echo ">> Using ${CODEGEN_PKG}" # code-generator does work with go.mod but makes assumptions about # the project living in `$GOPATH/src`. To work around this and support # any location; create a temporary directory, use this as an output # base, and copy everything back once generated. TEMP_DIR=$(mktemp -d) cleanup() { echo ">> Removing ${TEMP_DIR}" rm -rf ${TEMP_DIR} } trap "cleanup" EXIT SIGINT echo ">> Temporary output directory ${TEMP_DIR}" # Ensure we can execute. chmod +x ${CODEGEN_PKG}/generate-groups.sh ${CODEGEN_PKG}/generate-groups.sh all \ github.com/tensorchord/openmodelz/modelzetes/pkg/client github.com/tensorchord/openmodelz/modelzetes/pkg/apis \ modelzetes:v2alpha1 \ --output-base "${TEMP_DIR}" \ --go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt # Copy everything back. cp -r "${TEMP_DIR}/github.com/tensorchord/openmodelz/modelzetes/." "${SCRIPT_ROOT}/" ================================================ FILE: modelzetes/hack/update-crds.sh ================================================ #!/bin/bash export controllergen="$GOPATH/bin/controller-gen" export PKG=sigs.k8s.io/controller-tools/cmd/controller-gen@v0.7.0 if [ ! -e "$controllergen" ]; then echo "Getting $PKG" go install $PKG fi "$controllergen" \ crd \ schemapatch:manifests=./artifacts/crds \ paths=./pkg/apis/modelzetes/v2alpha1 \ output:dir=./artifacts/crds ================================================ FILE: modelzetes/hack/verify-codegen.sh ================================================ #!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(git rev-parse --show-toplevel)/modelzetes DIFFROOT="${SCRIPT_ROOT}/pkg" TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/pkg" _tmp="${SCRIPT_ROOT}/_tmp" cleanup() { rm -rf "${_tmp}" } trap "cleanup" EXIT SIGINT cleanup mkdir -p "${TMP_DIFFROOT}" cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" "${SCRIPT_ROOT}/hack/update-codegen.sh" echo "diffing ${DIFFROOT} against freshly generated codegen" ret=0 diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" if [[ $ret -eq 0 ]] then echo "${DIFFROOT} up to date." else echo "${DIFFROOT} is out of date. Please run hack/update-codegen.sh" exit 1 fi ================================================ FILE: modelzetes/pkg/apis/modelzetes/register.go ================================================ package modelzetes const ( GroupName = "tensorchord.ai" ) ================================================ FILE: modelzetes/pkg/apis/modelzetes/v2alpha1/doc.go ================================================ // +k8s:deepcopy-gen=package,register // Package v2alpha1 is the modelzetes API. // +groupName=tensorchord.ai package v2alpha1 ================================================ FILE: modelzetes/pkg/apis/modelzetes/v2alpha1/register.go ================================================ package v2alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" controller "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes" ) // SchemeGroupVersion is group version used to register these objects var SchemeGroupVersion = schema.GroupVersion{Group: controller.GroupName, Version: "v2alpha1"} // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder AddToScheme = localSchemeBuilder.AddToScheme Kind = "Inference" ) func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. localSchemeBuilder.Register(addKnownTypes) } // Adds the list of known types to api.Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Inference{}, &InferenceList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ================================================ FILE: modelzetes/pkg/apis/modelzetes/v2alpha1/types.go ================================================ package v2alpha1 import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +genclient:noStatus // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:printcolumn:name="Image",type=string,JSONPath=`.spec.image` // Inference describes an Inference type Inference struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec InferenceSpec `json:"spec"` } // InferenceSpec defines the desired state of Inference type InferenceSpec struct { Name string `json:"name"` Image string `json:"image"` // Scaling is the scaling configuration for the inference. Scaling *ScalingConfig `json:"scaling,omitempty"` // Framework is the inference framework. Framework Framework `json:"framework,omitempty"` // Port is the port exposed by the inference. Port *int32 `json:"port,omitempty"` // HTTPProbePath is the path of the http probe. HTTPProbePath *string `json:"http_probe_path,omitempty"` // Command to run when starting the Command *string `json:"command,omitempty"` // EnvVars can be provided to set environment variables for the inference runtime. EnvVars map[string]string `json:"envVars,omitempty"` // Constraints are specific to the operator. Constraints []string `json:"constraints,omitempty"` // Secrets list of secrets to be made available to inference Secrets []string `json:"secrets,omitempty"` // Labels are metadata for inferences which may be used by the // faas-provider or the gateway Labels map[string]string `json:"labels,omitempty"` // Annotations are metadata for inferences which may be used by the // faas-provider or the gateway Annotations map[string]string `json:"annotations,omitempty"` // Limits for inference Resources *v1.ResourceRequirements `json:"resources,omitempty"` } // Framework is the inference framework. It is only used to set the default port // and command. For example, if the framework is "gradio", the default port is // 7860 and the default command is "python app.py". You could override these // defaults by setting the port and command fields and framework to `other`. type Framework string const ( FrameworkGradio Framework = "gradio" FrameworkStreamlit Framework = "streamlit" FrameworkMosec Framework = "mosec" FrameworkOther Framework = "other" ) type ScalingConfig struct { // MinReplicas is the lower limit for the number of replicas to which the // autoscaler can scale down. It defaults to 0. MinReplicas *int32 `json:"min_replicas,omitempty"` // MaxReplicas is the upper limit for the number of replicas to which the // autoscaler can scale up. It cannot be less that minReplicas. It defaults // to 1. MaxReplicas *int32 `json:"max_replicas,omitempty"` // TargetLoad is the target load. In capacity mode, it is the expected number of the inflight requests per replica. TargetLoad *int32 `json:"target_load,omitempty"` // Type is the scaling type. It can be either "capacity" or "rps". Default is "capacity". Type *ScalingType `json:"type,omitempty"` // ZeroDuration is the duration of zero load before scaling down to zero. Default is 5 minutes. ZeroDuration *int32 `json:"zero_duration,omitempty"` // StartupDuration is the duration of startup time. StartupDuration *int32 `json:"startup_duration,omitempty"` } type ScalingType string const ( ScalingTypeCapacity ScalingType = "capacity" ScalingTypeRPS ScalingType = "rps" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // InferenceList is a list of inference resources type InferenceList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` Items []Inference `json:"items"` } ================================================ FILE: modelzetes/pkg/apis/modelzetes/v2alpha1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by deepcopy-gen. DO NOT EDIT. package v2alpha1 import ( v1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Inference) DeepCopyInto(out *Inference) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Inference. func (in *Inference) DeepCopy() *Inference { if in == nil { return nil } out := new(Inference) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Inference) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InferenceList) DeepCopyInto(out *InferenceList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Inference, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceList. func (in *InferenceList) DeepCopy() *InferenceList { if in == nil { return nil } out := new(InferenceList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *InferenceList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InferenceSpec) DeepCopyInto(out *InferenceSpec) { *out = *in if in.Scaling != nil { in, out := &in.Scaling, &out.Scaling *out = new(ScalingConfig) (*in).DeepCopyInto(*out) } if in.Port != nil { in, out := &in.Port, &out.Port *out = new(int32) **out = **in } if in.HTTPProbePath != nil { in, out := &in.HTTPProbePath, &out.HTTPProbePath *out = new(string) **out = **in } if in.Command != nil { in, out := &in.Command, &out.Command *out = new(string) **out = **in } if in.EnvVars != nil { in, out := &in.EnvVars, &out.EnvVars *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.Constraints != nil { in, out := &in.Constraints, &out.Constraints *out = make([]string, len(*in)) copy(*out, *in) } if in.Secrets != nil { in, out := &in.Secrets, &out.Secrets *out = make([]string, len(*in)) copy(*out, *in) } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.Resources != nil { in, out := &in.Resources, &out.Resources *out = new(v1.ResourceRequirements) (*in).DeepCopyInto(*out) } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceSpec. func (in *InferenceSpec) DeepCopy() *InferenceSpec { if in == nil { return nil } out := new(InferenceSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScalingConfig) DeepCopyInto(out *ScalingConfig) { *out = *in if in.MinReplicas != nil { in, out := &in.MinReplicas, &out.MinReplicas *out = new(int32) **out = **in } if in.MaxReplicas != nil { in, out := &in.MaxReplicas, &out.MaxReplicas *out = new(int32) **out = **in } if in.TargetLoad != nil { in, out := &in.TargetLoad, &out.TargetLoad *out = new(int32) **out = **in } if in.Type != nil { in, out := &in.Type, &out.Type *out = new(ScalingType) **out = **in } if in.ZeroDuration != nil { in, out := &in.ZeroDuration, &out.ZeroDuration *out = new(int32) **out = **in } if in.StartupDuration != nil { in, out := &in.StartupDuration, &out.StartupDuration *out = new(int32) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalingConfig. func (in *ScalingConfig) DeepCopy() *ScalingConfig { if in == nil { return nil } out := new(ScalingConfig) in.DeepCopyInto(out) return out } ================================================ FILE: modelzetes/pkg/app/config.go ================================================ package app import ( cli "github.com/urfave/cli/v2" "github.com/tensorchord/openmodelz/modelzetes/pkg/config" ) func configFromCLI(c *cli.Context) config.Config { cfg := config.Config{} // kubernetes cfg.KubeConfig.Kubeconfig = c.String(flagKubeConfig) cfg.KubeConfig.MasterURL = c.String(flagMasterURL) cfg.KubeConfig.QPS = c.Int(flagQPS) cfg.KubeConfig.Burst = c.Int(flagBurst) cfg.KubeConfig.ResyncPeriod = c.Duration(flagResyncPeriod) // controller cfg.Controller.ThreadCount = c.Int(flagControllerThreads) // metrics cfg.Metrics.ServerPort = c.Int(flagMetricsServerPort) // huggingface cfg.HuggingfaceProxy.Endpoint = c.String(flagHuggingfaceEndpoint) // probes cfg.Probes.Readiness.InitialDelaySeconds = c.Int(flagProbeReadinessInitialDelaySeconds) cfg.Probes.Readiness.PeriodSeconds = c.Int(flagProbeReadinessPeriodSeconds) cfg.Probes.Readiness.TimeoutSeconds = c.Int(flagProbeReadinessTimeoutSeconds) cfg.Probes.Liveness.InitialDelaySeconds = c.Int(flagProbeLivenessInitialDelaySeconds) cfg.Probes.Liveness.PeriodSeconds = c.Int(flagProbeLivenessPeriodSeconds) cfg.Probes.Liveness.TimeoutSeconds = c.Int(flagProbeLivenessTimeoutSeconds) cfg.Probes.Startup.InitialDelaySeconds = c.Int(flagProbeStartupInitialDelaySeconds) cfg.Probes.Startup.PeriodSeconds = c.Int(flagProbeStartupPeriodSeconds) cfg.Probes.Startup.TimeoutSeconds = c.Int(flagProbeStartupTimeoutSeconds) // inference cfg.Inference.ImagePullPolicy = c.String(flagInferenceImagePullPolicy) cfg.Inference.SetUpRuntimeClassNvidia = c.Bool(flagInferenceSetUpRuntimeClassNvidia) return cfg } ================================================ FILE: modelzetes/pkg/app/root.go ================================================ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. package app import ( "flag" "time" "github.com/cockroachdb/errors" cli "github.com/urfave/cli/v2" "k8s.io/klog" "github.com/tensorchord/openmodelz/modelzetes/pkg/controller" "github.com/tensorchord/openmodelz/modelzetes/pkg/signals" "github.com/tensorchord/openmodelz/modelzetes/pkg/version" ) const ( flagDebug = "debug" // metrics flagMetricsServerPort = "metrics-server-port" // kubernetes flagMasterURL = "master-url" flagKubeConfig = "kube-config" flagQPS = "kube-qps" flagBurst = "kube-burst" flagResyncPeriod = "kube-resync-period" // controller flagControllerThreads = "controller-thread-count" // huggingface flagHuggingfaceEndpoint = "huggingface-endpoint" // probes flagProbeReadinessInitialDelaySeconds = "probe-readiness-initial-delay-seconds" flagProbeReadinessPeriodSeconds = "probe-readiness-period-seconds" flagProbeReadinessTimeoutSeconds = "probe-readiness-timeout-seconds" flagProbeLivenessInitialDelaySeconds = "probe-liveness-initial-delay-seconds" flagProbeLivenessPeriodSeconds = "probe-liveness-period-seconds" flagProbeLivenessTimeoutSeconds = "probe-liveness-timeout-seconds" flagProbeStartupInitialDelaySeconds = "probe-startup-initial-delay-seconds" flagProbeStartupPeriodSeconds = "probe-startup-period-seconds" flagProbeStartupTimeoutSeconds = "probe-startup-timeout-seconds" // inference flagInferenceImagePullPolicy = "inference-image-pull-policy" flagInferenceSetUpRuntimeClassNvidia = "inference-set-up-runtime-class-nvidia" ) type App struct { *cli.App } func New() App { internalApp := cli.NewApp() internalApp.EnableBashCompletion = true internalApp.Name = "modelzetes" internalApp.Usage = "kubernetes operator for modelz" internalApp.HideHelpCommand = true internalApp.HideVersion = false internalApp.Version = version.GetVersion().String() internalApp.Flags = []cli.Flag{ &cli.BoolFlag{ Name: flagDebug, Usage: "enable debug output in logs", EnvVars: []string{"DEBUG"}, }, &cli.IntFlag{ Name: flagMetricsServerPort, Value: 8081, Usage: "port to listen on", EnvVars: []string{"MODELZETES_SERVER_PORT"}, Aliases: []string{"p"}, }, &cli.StringFlag{ Name: flagMasterURL, Usage: "URL to master for kubernetes cluster", EnvVars: []string{"MODELZETES_MASTER_URL"}, Aliases: []string{"mu"}, }, &cli.StringFlag{ Name: flagKubeConfig, Usage: "Path to kubeconfig file. If not provided, will use in-cluster config", EnvVars: []string{"MODELZETES_KUBE_CONFIG"}, Aliases: []string{"kc"}, }, &cli.IntFlag{ Name: flagQPS, Usage: "QPS for kubernetes client", Value: 100, EnvVars: []string{"MODELZETES_KUBE_QPS"}, Aliases: []string{"kq"}, }, &cli.IntFlag{ Name: flagBurst, Value: 250, Usage: "Burst for kubernetes client", EnvVars: []string{"MODELZETES_KUBE_BURST"}, Aliases: []string{"kb"}, }, &cli.DurationFlag{ Name: flagResyncPeriod, Value: time.Minute * 5, Usage: "Resync period for kubernetes client", EnvVars: []string{"MODELZETES_KUBE_RESYNC_PERIOD"}, Aliases: []string{"kr"}, }, &cli.IntFlag{ Name: flagControllerThreads, Value: 1, Usage: "Number of threads to use for controller", EnvVars: []string{"MODELZETES_CONTROLLER_THREAD_COUNT"}, Aliases: []string{"ct"}, }, &cli.StringFlag{ Name: flagHuggingfaceEndpoint, Usage: "Endpoint for huggingface modelz API. If not provided, will use " + "https://huggingface.co by default", EnvVars: []string{"MODELZETES_HUGGINGFACE_ENDPOINT"}, Aliases: []string{"he"}, }, &cli.IntFlag{ Name: flagProbeReadinessInitialDelaySeconds, Value: 2, Usage: "Initial delay for readiness probe", EnvVars: []string{"MODELZETES_PROBE_READINESS_INITIAL_DELAY_SECONDS"}, Aliases: []string{"prids"}, }, &cli.IntFlag{ Name: flagProbeReadinessPeriodSeconds, Value: 1, Usage: "Period for readiness probe", EnvVars: []string{"MODELZETES_PROBE_READINESS_PERIOD_SECONDS"}, Aliases: []string{"prps"}, }, &cli.IntFlag{ Name: flagProbeReadinessTimeoutSeconds, Value: 1, Usage: "Timeout for readiness probe", EnvVars: []string{"MODELZETES_PROBE_READINESS_TIMEOUT_SECONDS"}, Aliases: []string{"prts"}, }, &cli.IntFlag{ Name: flagProbeLivenessInitialDelaySeconds, Value: 2, Usage: "Initial delay for liveness probe", EnvVars: []string{"MODELZETES_PROBE_LIVENESS_INITIAL_DELAY_SECONDS"}, Aliases: []string{"plids"}, }, &cli.IntFlag{ Name: flagProbeLivenessPeriodSeconds, Value: 1, Usage: "Period for liveness probe", EnvVars: []string{"MODELZETES_PROBE_LIVENESS_PERIOD_SECONDS"}, Aliases: []string{"plps"}, }, &cli.IntFlag{ Name: flagProbeLivenessTimeoutSeconds, Value: 1, Usage: "Timeout for liveness probe", EnvVars: []string{"MODELZETES_PROBE_LIVENESS_TIMEOUT_SECONDS"}, Aliases: []string{"plts"}, }, &cli.IntFlag{ Name: flagProbeStartupInitialDelaySeconds, Value: 0, Usage: "Initial delay for startup probe", EnvVars: []string{"MODELZETES_PROBE_STARTUP_INITIAL_DELAY_SECONDS"}, Aliases: []string{"psids"}, }, &cli.IntFlag{ Name: flagProbeStartupPeriodSeconds, Value: 2, Usage: "Period for startup probe", EnvVars: []string{"MODELZETES_PROBE_STARTUP_PERIOD_SECONDS"}, Aliases: []string{"psps"}, }, &cli.IntFlag{ Name: flagProbeStartupTimeoutSeconds, Value: 1, Usage: "Timeout for startup probe", EnvVars: []string{"MODELZETES_PROBE_STARTUP_TIMEOUT_SECONDS"}, Aliases: []string{"psts"}, }, &cli.StringFlag{ Name: flagInferenceImagePullPolicy, Usage: "Image pull policy for inference service.", Value: "IfNotPresent", EnvVars: []string{"MODELZETES_INFERENCE_IMAGE_PULL_POLICY"}, Aliases: []string{"iipp"}, }, &cli.BoolFlag{ Name: flagInferenceSetUpRuntimeClassNvidia, Usage: "If true, will set up the Nvidia RuntimeClassName to the inference deployment.", EnvVars: []string{"MODELZETES_INFERENCE_SET_UP_RUNTIME_CLASS_NVIDIA"}, }, } internalApp.Action = runServer // Deal with debug flag. var debugEnabled bool internalApp.Before = func(context *cli.Context) error { debugEnabled = context.Bool(flagDebug) fs := flag.NewFlagSet("", flag.PanicOnError) klog.InitFlags(fs) if debugEnabled { fs.Set("v", "10") } else { fs.Set("v", "0") } return nil } return App{ App: internalApp, } } func runServer(clicontext *cli.Context) error { c := configFromCLI(clicontext) cfgString, _ := c.GetString() klog.V(0).Info("config: ", cfgString) if err := c.Validate(); err != nil { if clicontext.Bool(flagDebug) { return errors.Wrap(err, "invalid config: "+cfgString) } else { return errors.Wrap(err, "invalid config") } } // set up signals so we handle the first shutdown signal gracefully stopCh := signals.SetupSignalHandler() s, err := controller.New(c, stopCh) if err != nil { return errors.Wrap(err, "failed to create server") } return s.Run(c.Controller.ThreadCount, stopCh) } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/clientset.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package versioned import ( "fmt" "net/http" tensorchordv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" ) type Interface interface { Discovery() discovery.DiscoveryInterface TensorchordV2alpha1() tensorchordv2alpha1.TensorchordV2alpha1Interface } // Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient tensorchordV2alpha1 *tensorchordv2alpha1.TensorchordV2alpha1Client } // TensorchordV2alpha1 retrieves the TensorchordV2alpha1Client func (c *Clientset) TensorchordV2alpha1() tensorchordv2alpha1.TensorchordV2alpha1Interface { return c.tensorchordV2alpha1 } // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { return nil } return c.DiscoveryClient } // NewForConfig creates a new Clientset for the given config. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfig will generate a rate-limiter in configShallowCopy. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.UserAgent == "" { configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() } // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { return nil, err } return NewForConfigAndClient(&configShallowCopy, httpClient) } // NewForConfigAndClient creates a new Clientset for the given config and http client. // Note the http client provided takes precedence over the configured transport values. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { if configShallowCopy.Burst <= 0 { return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") } configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) } var cs Clientset var err error cs.tensorchordV2alpha1, err = tensorchordv2alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } return &cs, nil } // NewForConfigOrDie creates a new Clientset for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *Clientset { cs, err := NewForConfig(c) if err != nil { panic(err) } return cs } // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset cs.tensorchordV2alpha1 = tensorchordv2alpha1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/doc.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated clientset. package versioned ================================================ FILE: modelzetes/pkg/client/clientset/versioned/fake/clientset_generated.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( clientset "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" tensorchordv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1" faketensorchordv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/testing" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) } } cs := &Clientset{tracker: o} cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { gvr := action.GetResource() ns := action.GetNamespace() watch, err := o.Watch(gvr, ns) if err != nil { return false, nil, err } return true, watch, nil }) return cs } // Clientset implements clientset.Interface. Meant to be embedded into a // struct to get a default implementation. This makes faking out just the method // you want to test easier. type Clientset struct { testing.Fake discovery *fakediscovery.FakeDiscovery tracker testing.ObjectTracker } func (c *Clientset) Discovery() discovery.DiscoveryInterface { return c.discovery } func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} ) // TensorchordV2alpha1 retrieves the TensorchordV2alpha1Client func (c *Clientset) TensorchordV2alpha1() tensorchordv2alpha1.TensorchordV2alpha1Interface { return &faketensorchordv2alpha1.FakeTensorchordV2alpha1{Fake: &c.Fake} } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/fake/doc.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated fake clientset. package fake ================================================ FILE: modelzetes/pkg/client/clientset/versioned/fake/register.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( tensorchordv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ tensorchordv2alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(scheme)) } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/scheme/doc.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package contains the scheme of the automatically generated clientset. package scheme ================================================ FILE: modelzetes/pkg/client/clientset/versioned/scheme/register.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package scheme import ( tensorchordv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ tensorchordv2alpha1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(Scheme)) } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/doc.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated typed clients. package v2alpha1 ================================================ FILE: modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/fake/doc.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. // Package fake has the automatically generated clients. package fake ================================================ FILE: modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/fake/fake_inference.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( "context" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" labels "k8s.io/apimachinery/pkg/labels" schema "k8s.io/apimachinery/pkg/runtime/schema" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" testing "k8s.io/client-go/testing" ) // FakeInferences implements InferenceInterface type FakeInferences struct { Fake *FakeTensorchordV2alpha1 ns string } var inferencesResource = schema.GroupVersionResource{Group: "tensorchord.ai", Version: "v2alpha1", Resource: "inferences"} var inferencesKind = schema.GroupVersionKind{Group: "tensorchord.ai", Version: "v2alpha1", Kind: "Inference"} // Get takes name of the inference, and returns the corresponding inference object, and an error if there is any. func (c *FakeInferences) Get(ctx context.Context, name string, options v1.GetOptions) (result *v2alpha1.Inference, err error) { obj, err := c.Fake. Invokes(testing.NewGetAction(inferencesResource, c.ns, name), &v2alpha1.Inference{}) if obj == nil { return nil, err } return obj.(*v2alpha1.Inference), err } // List takes label and field selectors, and returns the list of Inferences that match those selectors. func (c *FakeInferences) List(ctx context.Context, opts v1.ListOptions) (result *v2alpha1.InferenceList, err error) { obj, err := c.Fake. Invokes(testing.NewListAction(inferencesResource, inferencesKind, c.ns, opts), &v2alpha1.InferenceList{}) if obj == nil { return nil, err } label, _, _ := testing.ExtractFromListOptions(opts) if label == nil { label = labels.Everything() } list := &v2alpha1.InferenceList{ListMeta: obj.(*v2alpha1.InferenceList).ListMeta} for _, item := range obj.(*v2alpha1.InferenceList).Items { if label.Matches(labels.Set(item.Labels)) { list.Items = append(list.Items, item) } } return list, err } // Watch returns a watch.Interface that watches the requested inferences. func (c *FakeInferences) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { return c.Fake. InvokesWatch(testing.NewWatchAction(inferencesResource, c.ns, opts)) } // Create takes the representation of a inference and creates it. Returns the server's representation of the inference, and an error, if there is any. func (c *FakeInferences) Create(ctx context.Context, inference *v2alpha1.Inference, opts v1.CreateOptions) (result *v2alpha1.Inference, err error) { obj, err := c.Fake. Invokes(testing.NewCreateAction(inferencesResource, c.ns, inference), &v2alpha1.Inference{}) if obj == nil { return nil, err } return obj.(*v2alpha1.Inference), err } // Update takes the representation of a inference and updates it. Returns the server's representation of the inference, and an error, if there is any. func (c *FakeInferences) Update(ctx context.Context, inference *v2alpha1.Inference, opts v1.UpdateOptions) (result *v2alpha1.Inference, err error) { obj, err := c.Fake. Invokes(testing.NewUpdateAction(inferencesResource, c.ns, inference), &v2alpha1.Inference{}) if obj == nil { return nil, err } return obj.(*v2alpha1.Inference), err } // Delete takes name of the inference and deletes it. Returns an error if one occurs. func (c *FakeInferences) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { _, err := c.Fake. Invokes(testing.NewDeleteActionWithOptions(inferencesResource, c.ns, name, opts), &v2alpha1.Inference{}) return err } // DeleteCollection deletes a collection of objects. func (c *FakeInferences) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { action := testing.NewDeleteCollectionAction(inferencesResource, c.ns, listOpts) _, err := c.Fake.Invokes(action, &v2alpha1.InferenceList{}) return err } // Patch applies the patch and returns the patched inference. func (c *FakeInferences) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v2alpha1.Inference, err error) { obj, err := c.Fake. Invokes(testing.NewPatchSubresourceAction(inferencesResource, c.ns, name, pt, data, subresources...), &v2alpha1.Inference{}) if obj == nil { return nil, err } return obj.(*v2alpha1.Inference), err } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/fake/fake_modelzetes_client.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" ) type FakeTensorchordV2alpha1 struct { *testing.Fake } func (c *FakeTensorchordV2alpha1) Inferences(namespace string) v2alpha1.InferenceInterface { return &FakeInferences{c, namespace} } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeTensorchordV2alpha1) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/generated_expansion.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package v2alpha1 type InferenceExpansion interface{} ================================================ FILE: modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/inference.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package v2alpha1 import ( "context" "time" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" scheme "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" rest "k8s.io/client-go/rest" ) // InferencesGetter has a method to return a InferenceInterface. // A group's client should implement this interface. type InferencesGetter interface { Inferences(namespace string) InferenceInterface } // InferenceInterface has methods to work with Inference resources. type InferenceInterface interface { Create(ctx context.Context, inference *v2alpha1.Inference, opts v1.CreateOptions) (*v2alpha1.Inference, error) Update(ctx context.Context, inference *v2alpha1.Inference, opts v1.UpdateOptions) (*v2alpha1.Inference, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*v2alpha1.Inference, error) List(ctx context.Context, opts v1.ListOptions) (*v2alpha1.InferenceList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v2alpha1.Inference, err error) InferenceExpansion } // inferences implements InferenceInterface type inferences struct { client rest.Interface ns string } // newInferences returns a Inferences func newInferences(c *TensorchordV2alpha1Client, namespace string) *inferences { return &inferences{ client: c.RESTClient(), ns: namespace, } } // Get takes name of the inference, and returns the corresponding inference object, and an error if there is any. func (c *inferences) Get(ctx context.Context, name string, options v1.GetOptions) (result *v2alpha1.Inference, err error) { result = &v2alpha1.Inference{} err = c.client.Get(). Namespace(c.ns). Resource("inferences"). Name(name). VersionedParams(&options, scheme.ParameterCodec). Do(ctx). Into(result) return } // List takes label and field selectors, and returns the list of Inferences that match those selectors. func (c *inferences) List(ctx context.Context, opts v1.ListOptions) (result *v2alpha1.InferenceList, err error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } result = &v2alpha1.InferenceList{} err = c.client.Get(). Namespace(c.ns). Resource("inferences"). VersionedParams(&opts, scheme.ParameterCodec). Timeout(timeout). Do(ctx). Into(result) return } // Watch returns a watch.Interface that watches the requested inferences. func (c *inferences) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { var timeout time.Duration if opts.TimeoutSeconds != nil { timeout = time.Duration(*opts.TimeoutSeconds) * time.Second } opts.Watch = true return c.client.Get(). Namespace(c.ns). Resource("inferences"). VersionedParams(&opts, scheme.ParameterCodec). Timeout(timeout). Watch(ctx) } // Create takes the representation of a inference and creates it. Returns the server's representation of the inference, and an error, if there is any. func (c *inferences) Create(ctx context.Context, inference *v2alpha1.Inference, opts v1.CreateOptions) (result *v2alpha1.Inference, err error) { result = &v2alpha1.Inference{} err = c.client.Post(). Namespace(c.ns). Resource("inferences"). VersionedParams(&opts, scheme.ParameterCodec). Body(inference). Do(ctx). Into(result) return } // Update takes the representation of a inference and updates it. Returns the server's representation of the inference, and an error, if there is any. func (c *inferences) Update(ctx context.Context, inference *v2alpha1.Inference, opts v1.UpdateOptions) (result *v2alpha1.Inference, err error) { result = &v2alpha1.Inference{} err = c.client.Put(). Namespace(c.ns). Resource("inferences"). Name(inference.Name). VersionedParams(&opts, scheme.ParameterCodec). Body(inference). Do(ctx). Into(result) return } // Delete takes name of the inference and deletes it. Returns an error if one occurs. func (c *inferences) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { return c.client.Delete(). Namespace(c.ns). Resource("inferences"). Name(name). Body(&opts). Do(ctx). Error() } // DeleteCollection deletes a collection of objects. func (c *inferences) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { var timeout time.Duration if listOpts.TimeoutSeconds != nil { timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second } return c.client.Delete(). Namespace(c.ns). Resource("inferences"). VersionedParams(&listOpts, scheme.ParameterCodec). Timeout(timeout). Body(&opts). Do(ctx). Error() } // Patch applies the patch and returns the patched inference. func (c *inferences) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v2alpha1.Inference, err error) { result = &v2alpha1.Inference{} err = c.client.Patch(pt). Namespace(c.ns). Resource("inferences"). Name(name). SubResource(subresources...). VersionedParams(&opts, scheme.ParameterCodec). Body(data). Do(ctx). Into(result) return } ================================================ FILE: modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1/modelzetes_client.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by client-gen. DO NOT EDIT. package v2alpha1 import ( "net/http" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/scheme" rest "k8s.io/client-go/rest" ) type TensorchordV2alpha1Interface interface { RESTClient() rest.Interface InferencesGetter } // TensorchordV2alpha1Client is used to interact with features provided by the tensorchord.ai group. type TensorchordV2alpha1Client struct { restClient rest.Interface } func (c *TensorchordV2alpha1Client) Inferences(namespace string) InferenceInterface { return newInferences(c, namespace) } // NewForConfig creates a new TensorchordV2alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*TensorchordV2alpha1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err } return NewForConfigAndClient(&config, httpClient) } // NewForConfigAndClient creates a new TensorchordV2alpha1Client for the given config and http client. // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*TensorchordV2alpha1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err } return &TensorchordV2alpha1Client{client}, nil } // NewForConfigOrDie creates a new TensorchordV2alpha1Client for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *TensorchordV2alpha1Client { client, err := NewForConfig(c) if err != nil { panic(err) } return client } // New creates a new TensorchordV2alpha1Client for the given RESTClient. func New(c rest.Interface) *TensorchordV2alpha1Client { return &TensorchordV2alpha1Client{c} } func setConfigDefaults(config *rest.Config) error { gv := v2alpha1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } return nil } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *TensorchordV2alpha1Client) RESTClient() rest.Interface { if c == nil { return nil } return c.restClient } ================================================ FILE: modelzetes/pkg/client/informers/externalversions/factory.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( reflect "reflect" sync "sync" time "time" versioned "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" internalinterfaces "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions/internalinterfaces" modelzetes "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions/modelzetes" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory type sharedInformerFactory struct { client versioned.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool // wg tracks how many goroutines were started. wg sync.WaitGroup // shuttingDown is true when Shutdown has been called. It may still be running // because it needs to wait for goroutines. shuttingDown bool } // WithCustomResyncConfig sets a custom resync period for the specified informer types. func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { for k, v := range resyncConfig { factory.customResync[reflect.TypeOf(k)] = v } return factory } } // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.tweakListOptions = tweakListOptions return factory } } // WithNamespace limits the SharedInformerFactory to the specified namespace. func WithNamespace(namespace string) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.namespace = namespace return factory } } // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync) } // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. // Listers obtained via this SharedInformerFactory will be subject to the same filters // as specified here. // Deprecated: Please use NewSharedInformerFactoryWithOptions instead func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) } // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { factory := &sharedInformerFactory{ client: client, namespace: v1.NamespaceAll, defaultResync: defaultResync, informers: make(map[reflect.Type]cache.SharedIndexInformer), startedInformers: make(map[reflect.Type]bool), customResync: make(map[reflect.Type]time.Duration), } // Apply all options for _, opt := range options { factory = opt(factory) } return factory } func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() if f.shuttingDown { return } for informerType, informer := range f.informers { if !f.startedInformers[informerType] { f.wg.Add(1) // We need a new variable in each loop iteration, // otherwise the goroutine would use the loop variable // and that keeps changing. informer := informer go func() { defer f.wg.Done() informer.Run(stopCh) }() f.startedInformers[informerType] = true } } } func (f *sharedInformerFactory) Shutdown() { f.lock.Lock() f.shuttingDown = true f.lock.Unlock() // Will return immediately if there is nothing to wait for. f.wg.Wait() } func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { informers := func() map[reflect.Type]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informers := map[reflect.Type]cache.SharedIndexInformer{} for informerType, informer := range f.informers { if f.startedInformers[informerType] { informers[informerType] = informer } } return informers }() res := map[reflect.Type]bool{} for informType, informer := range informers { res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) } return res } // InternalInformerFor returns the SharedIndexInformer for obj using an internal // client. func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) f.informers[informerType] = informer return informer } // SharedInformerFactory provides shared informers for resources in all known // API group versions. // // It is typically used like this: // // ctx, cancel := context.Background() // defer cancel() // factory := NewSharedInformerFactory(client, resyncPeriod) // defer factory.WaitForStop() // Returns immediately if nothing was started. // genericInformer := factory.ForResource(resource) // typedInformer := factory.SomeAPIGroup().V1().SomeType() // factory.Start(ctx.Done()) // Start processing these informers. // synced := factory.WaitForCacheSync(ctx.Done()) // for v, ok := range synced { // if !ok { // fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) // return // } // } // // // Creating informers can also be created after Start, but then // // Start must be called again: // anotherGenericInformer := factory.ForResource(resource) // factory.Start(ctx.Done()) type SharedInformerFactory interface { internalinterfaces.SharedInformerFactory // Start initializes all requested informers. They are handled in goroutines // which run until the stop channel gets closed. Start(stopCh <-chan struct{}) // Shutdown marks a factory as shutting down. At that point no new // informers can be started anymore and Start will return without // doing anything. // // In addition, Shutdown blocks until all goroutines have terminated. For that // to happen, the close channel(s) that they were started with must be closed, // either before Shutdown gets called or while it is waiting. // // Shutdown may be called multiple times, even concurrently. All such calls will // block until all goroutines have terminated. Shutdown() // WaitForCacheSync blocks until all started informers' caches were synced // or the stop channel gets closed. WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool // ForResource gives generic access to a shared informer of the matching type. ForResource(resource schema.GroupVersionResource) (GenericInformer, error) // InternalInformerFor returns the SharedIndexInformer for obj using an internal // client. InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer Tensorchord() modelzetes.Interface } func (f *sharedInformerFactory) Tensorchord() modelzetes.Interface { return modelzetes.New(f, f.namespace, f.tweakListOptions) } ================================================ FILE: modelzetes/pkg/client/informers/externalversions/generic.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( "fmt" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) // GenericInformer is type of SharedIndexInformer which will locate and delegate to other // sharedInformers based on type type GenericInformer interface { Informer() cache.SharedIndexInformer Lister() cache.GenericLister } type genericInformer struct { informer cache.SharedIndexInformer resource schema.GroupResource } // Informer returns the SharedIndexInformer. func (f *genericInformer) Informer() cache.SharedIndexInformer { return f.informer } // Lister returns the GenericLister. func (f *genericInformer) Lister() cache.GenericLister { return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) } // ForResource gives generic access to a shared informer of the matching type // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=tensorchord.ai, Version=v2alpha1 case v2alpha1.SchemeGroupVersion.WithResource("inferences"): return &genericInformer{resource: resource.GroupResource(), informer: f.Tensorchord().V2alpha1().Inferences().Informer()}, nil } return nil, fmt.Errorf("no informer found for %v", resource) } ================================================ FILE: modelzetes/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package internalinterfaces import ( time "time" versioned "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" cache "k8s.io/client-go/tools/cache" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer // SharedInformerFactory a small interface to allow for adding an informer without an import cycle type SharedInformerFactory interface { Start(stopCh <-chan struct{}) InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer } // TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) ================================================ FILE: modelzetes/pkg/client/informers/externalversions/modelzetes/interface.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package modelzetes import ( internalinterfaces "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions/internalinterfaces" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions/modelzetes/v2alpha1" ) // Interface provides access to each of this group's versions. type Interface interface { // V2alpha1 provides access to shared informers for resources in V2alpha1. V2alpha1() v2alpha1.Interface } type group struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // V2alpha1 returns a new v2alpha1.Interface. func (g *group) V2alpha1() v2alpha1.Interface { return v2alpha1.New(g.factory, g.namespace, g.tweakListOptions) } ================================================ FILE: modelzetes/pkg/client/informers/externalversions/modelzetes/v2alpha1/inference.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package v2alpha1 import ( "context" time "time" modelzetesv2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" versioned "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" internalinterfaces "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions/internalinterfaces" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/client/listers/modelzetes/v2alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) // InferenceInformer provides access to a shared informer and lister for // Inferences. type InferenceInformer interface { Informer() cache.SharedIndexInformer Lister() v2alpha1.InferenceLister } type inferenceInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewInferenceInformer constructs a new informer for Inference type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewInferenceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredInferenceInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredInferenceInformer constructs a new informer for Inference type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredInferenceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.TensorchordV2alpha1().Inferences(namespace).List(context.TODO(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.TensorchordV2alpha1().Inferences(namespace).Watch(context.TODO(), options) }, }, &modelzetesv2alpha1.Inference{}, resyncPeriod, indexers, ) } func (f *inferenceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredInferenceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *inferenceInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&modelzetesv2alpha1.Inference{}, f.defaultInformer) } func (f *inferenceInformer) Lister() v2alpha1.InferenceLister { return v2alpha1.NewInferenceLister(f.Informer().GetIndexer()) } ================================================ FILE: modelzetes/pkg/client/informers/externalversions/modelzetes/v2alpha1/interface.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by informer-gen. DO NOT EDIT. package v2alpha1 import ( internalinterfaces "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. type Interface interface { // Inferences returns a InferenceInformer. Inferences() InferenceInformer } type version struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // Inferences returns a InferenceInformer. func (v *version) Inferences() InferenceInformer { return &inferenceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } ================================================ FILE: modelzetes/pkg/client/listers/modelzetes/v2alpha1/expansion_generated.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by lister-gen. DO NOT EDIT. package v2alpha1 // InferenceListerExpansion allows custom methods to be added to // InferenceLister. type InferenceListerExpansion interface{} // InferenceNamespaceListerExpansion allows custom methods to be added to // InferenceNamespaceLister. type InferenceNamespaceListerExpansion interface{} ================================================ FILE: modelzetes/pkg/client/listers/modelzetes/v2alpha1/inference.go ================================================ /* Copyright 2019-2023 TensorChord Inc. Licensed under the MIT license. See LICENSE file in the project root for full license information. */ // Code generated by lister-gen. DO NOT EDIT. package v2alpha1 import ( v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/tools/cache" ) // InferenceLister helps list Inferences. // All objects returned here must be treated as read-only. type InferenceLister interface { // List lists all Inferences in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*v2alpha1.Inference, err error) // Inferences returns an object that can list and get Inferences. Inferences(namespace string) InferenceNamespaceLister InferenceListerExpansion } // inferenceLister implements the InferenceLister interface. type inferenceLister struct { indexer cache.Indexer } // NewInferenceLister returns a new InferenceLister. func NewInferenceLister(indexer cache.Indexer) InferenceLister { return &inferenceLister{indexer: indexer} } // List lists all Inferences in the indexer. func (s *inferenceLister) List(selector labels.Selector) (ret []*v2alpha1.Inference, err error) { err = cache.ListAll(s.indexer, selector, func(m interface{}) { ret = append(ret, m.(*v2alpha1.Inference)) }) return ret, err } // Inferences returns an object that can list and get Inferences. func (s *inferenceLister) Inferences(namespace string) InferenceNamespaceLister { return inferenceNamespaceLister{indexer: s.indexer, namespace: namespace} } // InferenceNamespaceLister helps list and get Inferences. // All objects returned here must be treated as read-only. type InferenceNamespaceLister interface { // List lists all Inferences in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*v2alpha1.Inference, err error) // Get retrieves the Inference from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*v2alpha1.Inference, error) InferenceNamespaceListerExpansion } // inferenceNamespaceLister implements the InferenceNamespaceLister // interface. type inferenceNamespaceLister struct { indexer cache.Indexer namespace string } // List lists all Inferences in the indexer for a given namespace. func (s inferenceNamespaceLister) List(selector labels.Selector) (ret []*v2alpha1.Inference, err error) { err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { ret = append(ret, m.(*v2alpha1.Inference)) }) return ret, err } // Get retrieves the Inference from the indexer for a given namespace and name. func (s inferenceNamespaceLister) Get(name string) (*v2alpha1.Inference, error) { obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) if err != nil { return nil, err } if !exists { return nil, errors.NewNotFound(v2alpha1.Resource("inference"), name) } return obj.(*v2alpha1.Inference), nil } ================================================ FILE: modelzetes/pkg/config/config.go ================================================ package config import ( "encoding/json" "errors" "time" ) type Config struct { Metrics MetricsConfig `json:"metrics,omitempty"` KubeConfig KubeConfig `json:"kube_config,omitempty"` Controller ControllerConfig `json:"controller,omitempty"` HuggingfaceProxy HuggingfaceProxyConfig `json:"huggingface_proxy,omitempty"` Probes ProbesConfig `json:"probes,omitempty"` Inference InferenceConfig `json:"inference,omitempty"` } type InferenceConfig struct { ImagePullPolicy string `json:"image_pull_policy,omitempty"` SetUpRuntimeClassNvidia bool `json:"set_up_runtime_class_nvidia,omitempty"` } type ProbesConfig struct { Startup ProbeConfig `json:"startup,omitempty"` Readiness ProbeConfig `json:"readiness,omitempty"` Liveness ProbeConfig `json:"liveness,omitempty"` } type ProbeConfig struct { InitialDelaySeconds int `json:"initial_delay_seconds,omitempty"` PeriodSeconds int `json:"period_seconds,omitempty"` TimeoutSeconds int `json:"timeout_seconds,omitempty"` } type HuggingfaceProxyConfig struct { Endpoint string `json:"endpoint,omitempty"` } type ControllerConfig struct { ThreadCount int `json:"thread_count,omitempty"` } type MetricsConfig struct { ServerPort int `json:"server_port,omitempty"` } type KubeConfig struct { Kubeconfig string `json:"kubeconfig,omitempty"` MasterURL string `json:"master_url,omitempty"` QPS int `json:"qps,omitempty"` Burst int `json:"burst,omitempty"` ResyncPeriod time.Duration `json:"resync_period,omitempty"` } func New() Config { return Config{} } func (c Config) GetString() (string, error) { bytes, err := json.Marshal(c) return string(bytes), err } func (c Config) Validate() error { if c.KubeConfig.QPS == 0 || c.KubeConfig.Burst == 0 || c.KubeConfig.ResyncPeriod == 0 { return errors.New("invalid kubeconfig") } if c.Metrics.ServerPort <= 0 { return errors.New("invalid metrics config") } if c.Controller.ThreadCount == 0 { return errors.New("invalid controller config") } if c.Inference.ImagePullPolicy == "" { return errors.New("invalid inference config") } return nil } ================================================ FILE: modelzetes/pkg/consts/consts.go ================================================ package consts const ( ResourceNvidiaGPU = "nvidia.com/gpu" LabelInferenceName = "inference" LabelInferenceNamespace = "inference-namespace" LabelBuildName = "ai.tensorchord.build" LabelName = "ai.tensorchord.name" LabelNamespace = "modelz.tensorchord.ai/namespace" LabelServerResource = "ai.tensorchord.server-resource" AnnotationBuilding = "ai.tensorchord.building" AnnotationDockerImage = "ai.tensorchord.docker.image" AnnotationControlPlaneKey = "ai.tensorchord.control-plane" ModelzAnnotationValue = "modelz" TolerationGPU = "ai.tensorchord.gpu" TolerationNvidiaGPUPresent = "nvidia.com/gpu" //OrchestrationIdentifier identifier string for provider orchestration OrchestrationIdentifier = "kubernetes" //ProviderName name of the provider ProviderName = "modelzetes" DefaultServicePrefix = "mdz-" DefaultHTTPProbePath = "/" // MaxReplicas is the maximum number of replicas that can be set for a inference. MaxReplicas = 5 ) ================================================ FILE: modelzetes/pkg/controller/annotations_test.go ================================================ package controller import ( "testing" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" ) func Test_makeAnnotations_NoKeys(t *testing.T) { annotationVal := `{"name":"","image":""}` spec := v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{}, } annotations := makeAnnotations(&spec) if _, ok := annotations["prometheus.io.scrape"]; !ok { t.Errorf("wanted annotation " + "prometheus.io.scrape" + " to be added") t.Fail() } if val, _ := annotations["prometheus.io.scrape"]; val != "false" { t.Errorf("wanted annotation " + "prometheus.io.scrape" + ` to equal "false"`) t.Fail() } if _, ok := annotations[annotationInferenceSpec]; !ok { t.Errorf("wanted annotation " + annotationInferenceSpec) t.Fail() } if val, _ := annotations[annotationInferenceSpec]; val != annotationVal { t.Errorf("Annotation " + annotationInferenceSpec + "\nwant: '" + annotationVal + "'\ngot: '" + val + "'") t.Fail() } } func Test_makeAnnotations_WithKeyAndValue(t *testing.T) { annotationVal := `{"name":"","image":"","annotations":{"key":"value","key2":"value2"}}` spec := v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Annotations: map[string]string{ "key": "value", "key2": "value2", }, }, } annotations := makeAnnotations(&spec) if _, ok := annotations["prometheus.io.scrape"]; !ok { t.Errorf("wanted annotation " + "prometheus.io.scrape" + " to be added") t.Fail() } if val := annotations["prometheus.io.scrape"]; val != "false" { t.Errorf("wanted annotation " + "prometheus.io.scrape" + ` to equal "false"`) t.Fail() } if _, ok := annotations[annotationInferenceSpec]; !ok { t.Errorf("wanted annotation " + annotationInferenceSpec) t.Fail() } if val := annotations[annotationInferenceSpec]; val != annotationVal { t.Errorf("Annotation " + annotationInferenceSpec + "\nwant: '" + annotationVal + "'\ngot: '" + val + "'") t.Fail() } } func Test_makeAnnotationsDoesNotModifyOriginalSpec(t *testing.T) { specAnnotations := map[string]string{ "test.foo": "bar", } function := &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Annotations: specAnnotations, }, } expectedAnnotations := map[string]string{ "prometheus.io.scrape": "false", "test.foo": "bar", annotationInferenceSpec: `{"name":"testfunc","image":"","annotations":{"test.foo":"bar"}}`, } makeAnnotations(function) annotations := makeAnnotations(function) if len(specAnnotations) != 1 { t.Errorf("length of original spec annotations has changed, expected 1, got %d", len(specAnnotations)) } if specAnnotations["test.foo"] != "bar" { t.Errorf("original spec annotation has changed") } for name, expectedValue := range expectedAnnotations { actualValue := annotations[name] if actualValue != expectedValue { t.Fatalf("incorrect annotation for '%s': \nwant %q,\ngot %q", name, expectedValue, actualValue) } } } ================================================ FILE: modelzetes/pkg/controller/controller.go ================================================ package controller import ( "context" "fmt" "strings" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" appslisters "k8s.io/client-go/listers/apps/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" glog "k8s.io/klog" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" clientset "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" faasscheme "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/scheme" informers "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions" listers "github.com/tensorchord/openmodelz/modelzetes/pkg/client/listers/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) const ( controllerAgentName = "modelz-operator" functionPort = 8080 // SuccessSynced is used as part of the Event 'reason' when a Function is synced SuccessSynced = "Synced" // ErrResourceExists is used as part of the Event 'reason' when a Function fails // to sync due to a Deployment of the same name already existing. ErrResourceExists = "ErrResourceExists" // MessageResourceExists is the message used for Events when a resource // fails to sync due to a Deployment already existing MessageResourceExists = "Resource %q already exists and is not managed by OpenFaaS" // MessageResourceSynced is the message used for an Event fired when a Function // is synced successfully MessageResourceSynced = "Function synced successfully" ) // Controller is the controller implementation for Function resources type Controller struct { BaseDomain string // kubeclientset is a standard kubernetes clientset kubeclientset kubernetes.Interface // faasclientset is a clientset for our own API group faasclientset clientset.Interface deploymentsLister appslisters.DeploymentLister deploymentsSynced cache.InformerSynced inferenceLister listers.InferenceLister inferencesSynced cache.InformerSynced // workqueue is a rate limited work queue. This is used to queue work to be // processed instead of performing it as soon as a change happens. This // means we can ensure we only process a fixed amount of resources at a // time, and makes it easy to ensure we are never processing the same item // simultaneously in two different workers. workqueue workqueue.RateLimitingInterface // recorder is an event recorder for recording Event resources to the // Kubernetes API. recorder record.EventRecorder // OpenFaaS function factory factory FunctionFactory } // NewController returns a new OpenFaaS controller func NewController( kubeclientset kubernetes.Interface, inferenceclientset clientset.Interface, kubeInformerFactory kubeinformers.SharedInformerFactory, inferenceInformerFactory informers.SharedInformerFactory, factory FunctionFactory) *Controller { // obtain references to shared index informers for the Deployment and Function types deploymentInformer := kubeInformerFactory.Apps().V1().Deployments() inferenceInformer := inferenceInformerFactory.Tensorchord().V2alpha1().Inferences() // Create event broadcaster // Add o6s types to the default Kubernetes Scheme so Events can be // logged for faas-controller types. faasscheme.AddToScheme(scheme.Scheme) glog.V(4).Info("Creating event broadcaster") eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(glog.V(4).Infof) eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")}) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName}) controller := &Controller{ kubeclientset: kubeclientset, faasclientset: inferenceclientset, deploymentsLister: deploymentInformer.Lister(), deploymentsSynced: deploymentInformer.Informer().HasSynced, inferenceLister: inferenceInformer.Lister(), inferencesSynced: inferenceInformer.Informer().HasSynced, workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Functions"), recorder: recorder, factory: factory, } glog.Info("Setting up event handlers") // Add Function (OpenFaaS CRD-entry) Informer // // Set up an event handler for when Function resources change inferenceInformer.Informer(). AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.enqueueFunction, UpdateFunc: func(old, new interface{}) { controller.enqueueFunction(new) }, }) // Set up an event handler for when functions related resources like pods, deployments, replica sets // can't be materialized. This logs abnormal events like ImagePullBackOff, back-off restarting failed container, // failed to start container, oci runtime errors, etc // Enable this with -v=3 kubeInformerFactory.Core().V1().Events().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { key, err := cache.MetaNamespaceKeyFunc(obj) if err == nil { event := obj.(*corev1.Event) since := time.Since(event.LastTimestamp.Time) // log abnormal events occurred in the last minute if since.Seconds() < 61 && strings.Contains(event.Type, "Warning") { glog.V(3).Infof("Abnormal event detected on %s %s: %s", event.LastTimestamp, key, event.Message) } } }, }) return controller } // Run will set up the event handlers for types we are interested in, as well // as syncing informer caches and starting workers. It will block until stopCh // is closed, at which point it will shutdown the workqueue and wait for // workers to finish processing their current work items. func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error { defer runtime.HandleCrash() defer c.workqueue.ShutDown() // Start the informer factories to begin populating the informer caches // Wait for the caches to be synced before starting workers glog.Info("Waiting for informer caches to sync") if ok := cache.WaitForCacheSync(stopCh, c.deploymentsSynced, c.inferencesSynced); !ok { return fmt.Errorf("failed to wait for caches to sync") } glog.Info("Starting workers") // Launch two workers to process Function resources for i := 0; i < threadiness; i++ { go wait.Until(c.runWorker, time.Second, stopCh) } glog.Info("Started workers") <-stopCh glog.Info("Shutting down workers") return nil } // runWorker is a long-running function that will continually call the // processNextWorkItem function in order to read and process a message on the workqueue. func (c *Controller) runWorker() { for c.processNextWorkItem() { } } // processNextWorkItem will read a single work item off the workqueue and // attempt to process it, by calling the syncHandler. func (c *Controller) processNextWorkItem() bool { obj, shutdown := c.workqueue.Get() if shutdown { return false } err := func(obj interface{}) error { defer c.workqueue.Done(obj) var key string var ok bool if key, ok = obj.(string); !ok { c.workqueue.Forget(obj) runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) return nil } if err := c.syncHandler(key); err != nil { return fmt.Errorf("error syncing '%s': %s", key, err.Error()) } c.workqueue.Forget(obj) return nil }(obj) if err != nil { runtime.HandleError(err) return true } return true } // syncHandler compares the actual state with the desired, and attempts to // converge the two. func (c *Controller) syncHandler(key string) error { // Convert the namespace/name string into a distinct namespace and name namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { runtime.HandleError(fmt.Errorf("invalid resource key: %s", key)) return nil } // Get the Function resource with this namespace/name function, err := c.inferenceLister.Inferences(namespace).Get(name) if err != nil { // The Function resource may no longer exist, in which case we stop processing. if errors.IsNotFound(err) { runtime.HandleError(fmt.Errorf("function '%s' in work queue no longer exists", key)) return nil } return err } deploymentName := function.Spec.Name if deploymentName == "" { // We choose to absorb the error here as the worker would requeue the // resource otherwise. Instead, the next time the resource is updated // the resource will be queued again. runtime.HandleError(fmt.Errorf("%s: deployment name must be specified", key)) return nil } if function.Spec.Annotations != nil { if _, ok := function.Spec.Annotations[consts.AnnotationBuilding]; ok { glog.Infof("Function '%s' is still building", function.Spec.Name) return nil } } // Get the deployment with the name specified in Function.spec deployment, err := c.deploymentsLister. Deployments(function.Namespace).Get(deploymentName) // If the resource doesn't exist, we'll create it if errors.IsNotFound(err) { err = nil existingSecrets, err := c.getSecrets(function.Namespace, function.Spec.Secrets) if err != nil { return err } glog.Infof("Creating deployment for '%s'", function.Spec.Name) deployment, err = c.kubeclientset.AppsV1().Deployments(function.Namespace).Create( context.TODO(), newDeployment(function, deployment, existingSecrets, c.factory), metav1.CreateOptions{}, ) if err != nil { return err } } svcGetOptions := metav1.GetOptions{} svcName := consts.DefaultServicePrefix + deploymentName _, getSvcErr := c.kubeclientset.CoreV1().Services(function.Namespace).Get(context.TODO(), deploymentName, svcGetOptions) if errors.IsNotFound(getSvcErr) { glog.Infof("Creating ClusterIP service for '%s'", function.Spec.Name) if _, err := c.kubeclientset.CoreV1().Services(function.Namespace).Create(context.TODO(), newService(function), metav1.CreateOptions{}); err != nil { // If an error occurs during Service Create, we'll requeue the item if errors.IsAlreadyExists(err) { err = nil glog.V(2).Infof("ClusterIP service '%s' already exists. Skipping creation.", function.Spec.Name) } else { return err } } } // If an error occurs during Get/Create, we'll requeue the item so we can // attempt processing again later. This could have been caused by a // temporary network failure, or any other transient reason. if err != nil { return fmt.Errorf("transient error: %v", err) } // If the Deployment is not controlled by this Function resource, we should log // a warning to the event recorder and ret if !metav1.IsControlledBy(deployment, function) { msg := fmt.Sprintf(MessageResourceExists, deployment.Name) c.recorder.Event(function, corev1.EventTypeWarning, ErrResourceExists, msg) return fmt.Errorf(msg) } // Update the Deployment resource if the Function definition differs if deploymentNeedsUpdate(function, deployment) { glog.Infof("Updating deployment for '%s'", function.Spec.Name) existingSecrets, err := c.getSecrets(function.Namespace, function.Spec.Secrets) if err != nil { return err } deployment, err = c.kubeclientset.AppsV1().Deployments(function.Namespace).Update( context.TODO(), newDeployment(function, deployment, existingSecrets, c.factory), metav1.UpdateOptions{}, ) if err != nil { glog.Errorf("Updating deployment for '%s' failed: %v", function.Spec.Name, err) } existingService, err := c.kubeclientset.CoreV1().Services(function.Namespace).Get(context.TODO(), svcName, metav1.GetOptions{}) if err != nil { return err } existingService.Annotations = makeAnnotations(function) _, err = c.kubeclientset.CoreV1().Services(function.Namespace).Update(context.TODO(), existingService, metav1.UpdateOptions{}) if err != nil { glog.Errorf("Updating service for '%s' failed: %v", function.Spec.Name, err) } } // If an error occurs during Update, we'll requeue the item so we can // attempt processing again later. THis could have been caused by a // temporary network failure, or any other transient reason. if err != nil { return err } c.recorder.Event(function, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced) return nil } // enqueueFunction takes a Function resource and converts it into a namespace/name // string which is then put onto the work queue. This method should *not* be // passed resources of any type other than Function. func (c *Controller) enqueueFunction(obj interface{}) { var key string var err error if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { runtime.HandleError(err) return } c.workqueue.AddRateLimited(key) } // handleObject will take any resource implementing metav1.Object and attempt // to find the Function resource that 'owns' it. It does this by looking at the // objects metadata.ownerReferences field for an appropriate OwnerReference. // It then enqueues that Function resource to be processed. If the object does not // have an appropriate OwnerReference, it will simply be skipped. func (c *Controller) handleObject(obj interface{}) { var object metav1.Object var ok bool if object, ok = obj.(metav1.Object); !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { runtime.HandleError(fmt.Errorf("error decoding object, invalid type")) return } object, ok = tombstone.Obj.(metav1.Object) if !ok { runtime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) return } glog.V(4).Infof("Recovered deleted object '%s' from tombstone", object.GetName()) } glog.V(4).Infof("Processing object: %s", object.GetName()) if ownerRef := metav1.GetControllerOf(object); ownerRef != nil { // If this object is not owned by a function, we should not do anything more // with it. if ownerRef.Kind != v2alpha1.Kind { return } function, err := c.inferenceLister.Inferences( object.GetNamespace()).Get(ownerRef.Name) if err != nil { glog.Infof("Function '%s' deleted. Ignoring orphaned object '%s'", ownerRef.Name, object.GetSelfLink()) return } c.enqueueFunction(function) return } } // getSecrets queries Kubernetes for a list of secrets by name in the given k8s namespace. func (c *Controller) getSecrets(namespace string, secretNames []string) (map[string]*corev1.Secret, error) { secrets := map[string]*corev1.Secret{} for _, secretName := range secretNames { secret, err := c.kubeclientset.CoreV1(). Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{}) if err != nil { return secrets, err } secrets[secretName] = secret } return secrets, nil } // getReplicas returns the desired number of replicas for a function taking into account // the min replicas label, HPA, the autoscaler and scaled to zero deployments func getReplicas(inference *v2alpha1.Inference, deployment *appsv1.Deployment) *int32 { var minReplicas, maxReplicas *int32 if inference.Spec.Scaling != nil { minReplicas = inference.Spec.Scaling.MinReplicas maxReplicas = inference.Spec.Scaling.MaxReplicas } // extract current deployment replicas if specified var deploymentReplicas *int32 if deployment != nil { deploymentReplicas = deployment.Spec.Replicas } // do not set replicas if min replicas is not set // and current deployment has no replicas count if minReplicas == nil && deploymentReplicas == nil { return nil } // set replicas to min if deployment has no replicas and min replicas exists if minReplicas != nil && deploymentReplicas == nil { return minReplicas } // do not override replicas when min is not specified if minReplicas == nil && deploymentReplicas != nil { return deploymentReplicas } if minReplicas != nil && deploymentReplicas != nil { if maxReplicas == nil { // do not override HPA or OF autoscaler replicas if the value is greater than min if *deploymentReplicas >= *minReplicas { return deploymentReplicas } } else { // do not override HPA or OF autoscaler replicas if the value is between min and max if *deploymentReplicas >= *minReplicas && *deploymentReplicas <= *maxReplicas { return deploymentReplicas } else if *deploymentReplicas > *maxReplicas { return maxReplicas } } } return minReplicas } ================================================ FILE: modelzetes/pkg/controller/deployment.go ================================================ package controller import ( "encoding/json" "strconv" "strings" "github.com/google/go-cmp/cmp" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" glog "k8s.io/klog" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" "github.com/tensorchord/openmodelz/modelzetes/pkg/k8s" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" ) const ( annotationInferenceSpec = "ai.tensorchord.inference.spec" defaultPort = 8080 ) var runtimeClassNvidia = "nvidia" // newDeployment creates a new Deployment for a Function resource. It also sets // the appropriate OwnerReferences on the resource so handleObject can discover // the Function resource that 'owns' it. func newDeployment( inference *v2alpha1.Inference, existingDeployment *appsv1.Deployment, existingSecrets map[string]*corev1.Secret, factory FunctionFactory) *appsv1.Deployment { // Set replicas to 0 if the expected number of replicas is 0 replicas := getReplicas(inference, existingDeployment) envVars := makeEnvVars(inference) labels := makeLabels(inference) nodeSelector := makeNodeSelector(inference.Spec.Constraints) port := makePort(inference) probes, err := factory.MakeProbes(inference, port) if err != nil { glog.Warningf("Function %s probes parsing failed: %v", inference.Spec.Name, err) } labelMap := k8s.MakeLabelSelector(inference.Spec.Name) // Add a new env var HF_ENDPOINT if enabled. hfEnvs := factory.MakeHuggingfacePullThroughCacheEnvVar() if hfEnvs != nil { envVars = addEnvVarIfNotExists(envVars, hfEnvs.Name, hfEnvs.Value) } annotations := makeAnnotations(inference) command := makeCommand(inference) allowPrivilegeEscalation := false deploymentSpec := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: inference.Spec.Name, Annotations: annotations, Namespace: inference.Namespace, Labels: labels, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(inference, schema.GroupVersionKind{ Group: v2alpha1.SchemeGroupVersion.Group, Version: v2alpha1.SchemeGroupVersion.Version, Kind: v2alpha1.Kind, }), }, }, Spec: appsv1.DeploymentSpec{ Replicas: replicas, Strategy: appsv1.DeploymentStrategy{ Type: appsv1.RollingUpdateDeploymentStrategyType, RollingUpdate: &appsv1.RollingUpdateDeployment{ MaxUnavailable: &intstr.IntOrString{ Type: intstr.String, StrVal: "10%", }, MaxSurge: &intstr.IntOrString{ Type: intstr.String, StrVal: "10%", }, }, }, Selector: &metav1.LabelSelector{ MatchLabels: labelMap, }, RevisionHistoryLimit: Ptr(int32(5)), Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, Annotations: annotations, }, Spec: corev1.PodSpec{ NodeSelector: nodeSelector, Containers: []corev1.Container{ { Name: inference.Spec.Name, Image: inference.Spec.Image, Ports: []corev1.ContainerPort{ {ContainerPort: int32(port), Protocol: corev1.ProtocolTCP}, }, Command: command, ImagePullPolicy: corev1.PullPolicy(factory.Factory.Config.ImagePullPolicy), Env: envVars, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: &allowPrivilegeEscalation, }, // TODO(xieydd): Add a function to set shm size VolumeMounts: []corev1.VolumeMount{ { Name: "dshm", MountPath: "/dev/shm", }, }, }, }, Volumes: []corev1.Volume{ { Name: "dshm", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{ Medium: corev1.StorageMediumMemory, }, }, }, }, }, }, }, } if probes != nil { if probes.Liveness != nil { deploymentSpec.Spec.Template.Spec.Containers[0].LivenessProbe = probes.Liveness } if probes.Readiness != nil { deploymentSpec.Spec.Template.Spec.Containers[0].ReadinessProbe = probes.Readiness } if probes.Startup != nil { deploymentSpec.Spec.Template.Spec.Containers[0].StartupProbe = probes.Startup if inference.Spec.Scaling != nil && inference.Spec.Scaling.StartupDuration != nil { // Set the failure threshold to the number of seconds in the duration. deploymentSpec.Spec.Template.Spec.Containers[0]. StartupProbe.FailureThreshold = int32( *inference.Spec.Scaling.StartupDuration / probes.Startup.PeriodSeconds) } } } if inference.Spec.Resources != nil { deploymentSpec.Spec.Template.Spec.Containers[0].Resources = *inference.Spec.Resources if q, ok := inference.Spec.Resources.Limits[consts.ResourceNvidiaGPU]; ok { if q.Value() > 0 { // If GPU is requested, add the GPU toleration. deploymentSpec.Spec.Template.Spec.Tolerations = makeTolerationGPU() if factory.Factory.Config.RuntimeClassNvidia { deploymentSpec.Spec.Template.Spec.RuntimeClassName = &runtimeClassNvidia } } else { // If GPU is not requested, set CUDA_VISIBLE_DEVICES to empty string. deploymentSpec.Spec.Template.Spec.Containers[0].Env = append( deploymentSpec.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ Name: "CUDA_VISIBLE_DEVICES", Value: "", }, ) } } } factory.ConfigureReadOnlyRootFilesystem(inference, deploymentSpec) factory.ConfigureContainerUserID(deploymentSpec) return deploymentSpec } func makeTolerationGPU() []corev1.Toleration { res := []corev1.Toleration{ { Key: consts.TolerationGPU, Operator: corev1.TolerationOpEqual, Value: "true", }, { Key: consts.TolerationNvidiaGPUPresent, Operator: corev1.TolerationOpEqual, Value: "present", }, } return res } func makeCommand(inference *v2alpha1.Inference) []string { if inference.Spec.Command != nil { res := strings.Split(*inference.Spec.Command, " ") return res } return nil } func makeEnvVars(inference *v2alpha1.Inference) []corev1.EnvVar { envVars := []corev1.EnvVar{} if inference.Spec.EnvVars != nil { for k, v := range inference.Spec.EnvVars { envVars = append(envVars, corev1.EnvVar{ Name: k, Value: v, }) } } // Set environment variables for different frameworks. switch inference.Spec.Framework { case v2alpha1.FrameworkGradio: envVars = addEnvVarIfNotExists(envVars, "GRADIO_SERVER_NAME", "0.0.0.0") envVars = addEnvVarIfNotExists(envVars, "GRADIO_SERVER_PORT", "7860") case v2alpha1.FrameworkMosec: envVars = addEnvVarIfNotExists(envVars, "MOSEC_PORT", strconv.Itoa(defaultPort)) case v2alpha1.FrameworkStreamlit: envVars = addEnvVarIfNotExists(envVars, "STREAMLIT_SERVER_ENABLE_CORS", "false") envVars = addEnvVarIfNotExists(envVars, "STREAMLIT_SERVER_ADDRESS", "0.0.0.0") envVars = addEnvVarIfNotExists(envVars, "STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION", "false") } return envVars } func addEnvVarIfNotExists(envVars []corev1.EnvVar, name, value string) []corev1.EnvVar { for _, envVar := range envVars { if envVar.Name == name { return envVars } } return append(envVars, corev1.EnvVar{ Name: name, Value: value, }) } func makeLabels(inference *v2alpha1.Inference) map[string]string { labels := map[string]string{ consts.LabelInferenceName: inference.Spec.Name, "app": inference.Spec.Name, "controller": inference.Name, } if inference.Spec.Labels != nil { for k, v := range inference.Spec.Labels { labels[k] = v } } return labels } func makePort(inference *v2alpha1.Inference) int { if inference.Spec.Port != nil { return int(*inference.Spec.Port) } return defaultPort } func makeAnnotations(inference *v2alpha1.Inference) map[string]string { annotations := make(map[string]string) // disable scraping since the watchdog doesn't expose a metrics endpoint annotations["prometheus.io.scrape"] = "false" // copy inference annotations if inference.Spec.Annotations != nil { for k, v := range inference.Spec.Annotations { annotations[k] = v } } // save inference spec in deployment annotations // used to detect changes in inference spec specJSON, err := json.Marshal(inference.Spec) if err != nil { glog.Errorf("Failed to marshal inference spec: %s", err.Error()) return annotations } annotations[annotationInferenceSpec] = string(specJSON) return annotations } func makeNodeSelector(constraints []string) map[string]string { selector := make(map[string]string) if len(constraints) > 0 { for _, constraint := range constraints { parts := strings.Split(constraint, "=") if len(parts) == 2 { selector[parts[0]] = parts[1] } } } return selector } // deploymentNeedsUpdate determines if the inference spec is different from the deployment spec func deploymentNeedsUpdate( inference *v2alpha1.Inference, deployment *appsv1.Deployment) bool { prevFnSpecJson := deployment.ObjectMeta.Annotations[annotationInferenceSpec] if prevFnSpecJson == "" { // is a new deployment or is an old deployment that is missing the annotation return true } prevFnSpec := &v2alpha1.InferenceSpec{} err := json.Unmarshal([]byte(prevFnSpecJson), prevFnSpec) if err != nil { glog.Errorf("Failed to parse previous inference spec: %s", err.Error()) return true } prevFn := v2alpha1.Inference{ Spec: *prevFnSpec, } if diff := cmp.Diff(prevFn.Spec, inference.Spec); diff != "" { glog.V(2).Infof("Change detected for %s diff\n%s", inference.Name, diff) return true } else { glog.V(3).Infof("No changes detected for %s", inference.Name) } return false } func int32p(i int32) *int32 { return &i } ================================================ FILE: modelzetes/pkg/controller/deployment_test.go ================================================ package controller import ( "strings" "testing" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" "github.com/tensorchord/openmodelz/modelzetes/pkg/k8s" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" ) var defaultK8sConfig = k8s.DeploymentConfig{ HTTPProbe: true, SetNonRootUser: true, LivenessProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, ReadinessProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, StartupProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, } func assertEnv(t *testing.T, expect map[string]string, real []v1.EnvVar) { for _, env := range real { value, exist := expect[env.Name] if exist == false || value != env.Value { t.Errorf("Environment variables contains unexpected %s:%s", env.Name, env.Value) t.Fail() } delete(expect, env.Name) } if len(expect) != 0 { t.Errorf("Environment variables should contain %v", expect) t.Fail() } } func Test_newDeployment(t *testing.T) { inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), defaultK8sConfig) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) if deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.HTTPGet.Path != "/" { t.Errorf("Readiness probe should have HTTPGet handler set to %s", "/") t.Fail() } if deployment.Spec.Template.Spec.Containers[0].StartupProbe.InitialDelaySeconds != 0 { t.Errorf("Startup probe should have initial delay seconds set to %s", "0") t.Fail() } if deployment.Spec.Template.Spec.Containers[0].LivenessProbe.InitialDelaySeconds != 0 { t.Errorf("Liveness probe should have initial delay seconds set to %s", "0") t.Fail() } if *(deployment.Spec.Template.Spec.Containers[0].SecurityContext.RunAsUser) != k8s.SecurityContextUserID { t.Errorf("RunAsUser should be %v", k8s.SecurityContextUserID) t.Fail() } } func TestNewDeploymentWithStartupDurationLabel(t *testing.T) { inf := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{ "prometheus.io.scrape": "true", }, Scaling: &v2alpha1.ScalingConfig{ StartupDuration: Ptr(int32(600)), }, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), k8s.DeploymentConfig{ HTTPProbe: true, SetNonRootUser: true, LivenessProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, ReadinessProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, StartupProbe: &k8s.ProbeConfig{ PeriodSeconds: 10, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, }) secrets := map[string]*corev1.Secret{} expectedPeriodSeconds := int32(10) expectedFailureThreshold := int32(60) deployment := newDeployment(inf, nil, secrets, factory) if len(deployment.Spec.Template.Spec.Containers) == 0 { t.Errorf("Deployment should have at least one container") t.Fail() } if deployment.Spec.Template.Spec.Containers[0].StartupProbe == nil { t.Errorf("Deployment should have a startup probe") t.Fail() } if deployment.Spec.Template.Spec.Containers[0].StartupProbe.PeriodSeconds != expectedPeriodSeconds { t.Errorf("Startup probe should have timeout seconds set to %d", expectedPeriodSeconds) t.Fail() } if deployment.Spec.Template.Spec.Containers[0].StartupProbe.FailureThreshold != expectedFailureThreshold { t.Errorf("Startup probe should have failure threshold set to %d", expectedFailureThreshold) t.Fail() } } func Test_newDeployment_PrometheusScrape_NotOverridden(t *testing.T) { inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", Annotations: map[string]string{ "prometheus.io.scrape": "true", }, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), k8s.DeploymentConfig{ HTTPProbe: false, SetNonRootUser: true, LivenessProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, ReadinessProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, StartupProbe: &k8s.ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, }) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) want := "true" if deployment.Spec.Template.Annotations["prometheus.io.scrape"] != want { t.Errorf("Annotation prometheus.io.scrape should be %s, was: %s", want, deployment.Spec.Template.Annotations["prometheus.io.scrape"]) } } func Test_newDeployment_WithZeroResource(t *testing.T) { quantity, _ := resource.ParseQuantity("0") inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, Resources: &v1.ResourceRequirements{ Limits: v1.ResourceList{consts.ResourceNvidiaGPU: quantity}, }, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), defaultK8sConfig) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) if deployment.Spec.Template.Spec.Containers[0].Env[0].Name != "CUDA_VISIBLE_DEVICES" { t.Errorf("CUDA_VISIBLE_DEVICES should be set to environment variables") t.Fail() } if deployment.Spec.Template.Spec.Containers[0].Env[0].Value != "" { t.Errorf("Empty value should be set to CUDA_VISIBLE_DEVICES") t.Fail() } } func Test_newDeployment_WithNonZeroResource(t *testing.T) { quantity, _ := resource.ParseQuantity("1") inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, Resources: &v1.ResourceRequirements{ Limits: v1.ResourceList{consts.ResourceNvidiaGPU: quantity}, }, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), defaultK8sConfig) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) if deployment.Spec.Template.Spec.Tolerations[0].Key != consts.TolerationGPU { t.Errorf("Tolerations should contain %s", consts.TolerationGPU) t.Fail() } if deployment.Spec.Template.Spec.Tolerations[1].Key != consts.TolerationNvidiaGPUPresent { t.Errorf("Tolerations should contain %s", consts.TolerationNvidiaGPUPresent) t.Fail() } } func Test_newDeployment_WithCommandsAndEnvVars(t *testing.T) { expectEnv := map[string]string{"MOCK": "TEST"} expectCommand := "python main.py" inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, Command: Ptr(expectCommand), EnvVars: expectEnv, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), defaultK8sConfig) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) assertEnv(t, expectEnv, deployment.Spec.Template.Spec.Containers[0].Env) if strings.Join(deployment.Spec.Template.Spec.Containers[0].Command, " ") != expectCommand { t.Errorf("Command should contain value %s", expectCommand) t.Fail() } } ================================================ FILE: modelzetes/pkg/controller/deployment_update_test.go ================================================ package controller import ( "testing" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" ) func Test_Deployment_Need_Update(t *testing.T) { scenarios := []struct { name string inference *v2alpha1.Inference deploy *appsv1.Deployment expected bool }{ { "empty deployment need update", &v2alpha1.Inference{}, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ annotationInferenceSpec: "", }, }, }, true, }, { "bad deployment need update", &v2alpha1.Inference{}, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{annotationInferenceSpec: "bad"}, }, }, true, }, { "equal deployment doesn't need update", &v2alpha1.Inference{}, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ annotationInferenceSpec: "{\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"name\":\"\",\"image\":\"\"}}", }, }, }, false, }, { "unequal deployment need update", &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Scaling: &v2alpha1.ScalingConfig{ MinReplicas: Ptr(int32(2)), }, }, }, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ annotationInferenceSpec: "{\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"name\":\"\",\"image\":\"\"}}", }, }}, true, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { needUpdate := deploymentNeedsUpdate(s.inference, s.deploy) if needUpdate != s.expected { t.Errorf("incorrect judgement of need update: expected %v, got %v", s.expected, needUpdate) t.Fail() } }) } } ================================================ FILE: modelzetes/pkg/controller/factory.go ================================================ package controller import ( "github.com/tensorchord/openmodelz/agent/api/types" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" "github.com/tensorchord/openmodelz/modelzetes/pkg/k8s" ) // FunctionFactory wraps modelzetes factory type FunctionFactory struct { Factory k8s.FunctionFactory } func NewFunctionFactory(clientset kubernetes.Interface, config k8s.DeploymentConfig) FunctionFactory { return FunctionFactory{ k8s.FunctionFactory{ Client: clientset, Config: config, }, } } func functionToResourceRequirements(in *v2alpha1.Inference) types.ResourceRequirements { resources := types.ResourceRequirements{} if in.Spec.Resources == nil { return resources } gpuLimit := in.Spec.Resources.Limits[consts.ResourceNvidiaGPU] gpuLimitPtr := &gpuLimit gpuRequest := in.Spec.Resources.Requests[consts.ResourceNvidiaGPU] gpuRequestsPtr := &gpuRequest resources = types.ResourceRequirements{ Limits: types.ResourceList{ types.ResourceCPU: types.Quantity( in.Spec.Resources.Limits.Cpu().String()), types.ResourceMemory: types.Quantity( in.Spec.Resources.Limits.Memory().String()), types.ResourceGPU: types.Quantity(gpuLimitPtr.String()), }, Requests: types.ResourceList{ types.ResourceCPU: types.Quantity( in.Spec.Resources.Requests.Cpu().String()), types.ResourceMemory: types.Quantity( in.Spec.Resources.Requests.Memory().String()), types.ResourceGPU: types.Quantity(gpuRequestsPtr.String()), }, } return resources } func (f *FunctionFactory) MakeHuggingfacePullThroughCacheEnvVar() *corev1.EnvVar { if f.Factory.Config.HuggingfacePullThroughCache { return &corev1.EnvVar{ Name: "HF_ENDPOINT", Value: f.Factory.Config.HuggingfacePullThroughCacheEndpoint, } } return nil } func (f *FunctionFactory) MakeProbes(function *v2alpha1.Inference, port int) ( *k8s.FunctionProbes, error) { // For old version inference without HTTPProbePath httpProbePath := consts.DefaultHTTPProbePath if (function.Spec.HTTPProbePath != nil) && (*function.Spec.HTTPProbePath != "") { httpProbePath = *function.Spec.HTTPProbePath } return f.Factory.MakeProbes(port, httpProbePath) } func (f *FunctionFactory) ConfigureReadOnlyRootFilesystem(function *v2alpha1.Inference, deployment *appsv1.Deployment) { f.Factory.ConfigureReadOnlyRootFilesystem(deployment) } func (f *FunctionFactory) ConfigureContainerUserID(deployment *appsv1.Deployment) { f.Factory.ConfigureContainerUserID(deployment) } ================================================ FILE: modelzetes/pkg/controller/framework_test.go ================================================ package controller import ( "strconv" "testing" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" ) func Test_newDeployment_FrameworkGradio(t *testing.T) { expectEnv := map[string]string{"GRADIO_SERVER_NAME": "0.0.0.0", "GRADIO_SERVER_PORT": "7860"} inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, Framework: v2alpha1.FrameworkGradio, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), defaultK8sConfig) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) assertEnv(t, expectEnv, deployment.Spec.Template.Spec.Containers[0].Env) } func Test_newDeployment_FrameworkMosec(t *testing.T) { expectEnv := map[string]string{"MOSEC_PORT": strconv.Itoa(defaultPort)} inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, Framework: v2alpha1.FrameworkMosec, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), defaultK8sConfig) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) assertEnv(t, expectEnv, deployment.Spec.Template.Spec.Containers[0].Env) } func Test_newDeployment_FrameworkStreamlit(t *testing.T) { expectEnv := map[string]string{ "STREAMLIT_SERVER_ENABLE_CORS": "false", "STREAMLIT_SERVER_ADDRESS": "0.0.0.0", "STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION": "false"} inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, Framework: v2alpha1.FrameworkStreamlit, }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), defaultK8sConfig) secrets := map[string]*corev1.Secret{} deployment := newDeployment(inference, nil, secrets, factory) assertEnv(t, expectEnv, deployment.Spec.Template.Spec.Containers[0].Env) } ================================================ FILE: modelzetes/pkg/controller/fromconfig.go ================================================ package controller import ( "errors" "fmt" kubeinformers "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" clientset "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned" informers "github.com/tensorchord/openmodelz/modelzetes/pkg/client/informers/externalversions" "github.com/tensorchord/openmodelz/modelzetes/pkg/config" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" "github.com/tensorchord/openmodelz/modelzetes/pkg/k8s" ) func New(c config.Config, stopCh <-chan struct{}) (*Controller, error) { clientCmdConfig, err := clientcmd.BuildConfigFromFlags( c.KubeConfig.MasterURL, c.KubeConfig.Kubeconfig) if err != nil { return nil, fmt.Errorf("error building kubeconfig: %s", err.Error()) } clientCmdConfig.QPS = float32(c.KubeConfig.QPS) clientCmdConfig.Burst = c.KubeConfig.Burst kubeClient, err := kubernetes.NewForConfig(clientCmdConfig) if err != nil { return nil, fmt.Errorf("error building Kubernetes clientset: %s", err.Error()) } inferenceClient, err := clientset.NewForConfig(clientCmdConfig) if err != nil { return nil, fmt.Errorf("error building Inference clientset: %s", err.Error()) } deployConfig := k8s.DeploymentConfig{ HTTPProbe: true, SetNonRootUser: false, ReadinessProbe: &k8s.ProbeConfig{ InitialDelaySeconds: int32(c.Probes.Readiness.InitialDelaySeconds), TimeoutSeconds: int32(c.Probes.Readiness.TimeoutSeconds), PeriodSeconds: int32(c.Probes.Readiness.PeriodSeconds), }, LivenessProbe: &k8s.ProbeConfig{ InitialDelaySeconds: int32(c.Probes.Liveness.InitialDelaySeconds), TimeoutSeconds: int32(c.Probes.Liveness.TimeoutSeconds), PeriodSeconds: int32(c.Probes.Liveness.PeriodSeconds), }, StartupProbe: &k8s.ProbeConfig{ InitialDelaySeconds: int32(c.Probes.Startup.InitialDelaySeconds), TimeoutSeconds: int32(c.Probes.Startup.TimeoutSeconds), PeriodSeconds: int32(c.Probes.Startup.PeriodSeconds), }, ImagePullPolicy: c.Inference.ImagePullPolicy, RuntimeClassNvidia: c.Inference.SetUpRuntimeClassNvidia, ProfilesNamespace: "default", } if c.HuggingfaceProxy.Endpoint == "" { deployConfig.HuggingfacePullThroughCache = false } else { deployConfig.HuggingfacePullThroughCache = true deployConfig.HuggingfacePullThroughCacheEndpoint = c.HuggingfaceProxy.Endpoint } kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, c.KubeConfig.ResyncPeriod) inferenceInformerFactory := informers.NewSharedInformerFactoryWithOptions(inferenceClient, c.KubeConfig.ResyncPeriod) inferences := inferenceInformerFactory.Tensorchord().V2alpha1().Inferences() go inferences.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:inferences", consts.ProviderName), stopCh, inferences.Informer().HasSynced); !ok { return nil, errors.New("failed to wait for inference caches to sync") } deployments := kubeInformerFactory.Apps().V1().Deployments() go deployments.Informer().Run(stopCh) if ok := cache.WaitForNamedCacheSync( fmt.Sprintf("%s:deployments", consts.ProviderName), stopCh, deployments.Informer().HasSynced); !ok { return nil, errors.New("failed to wait for deployment caches to sync") } controllerFactory := NewFunctionFactory(kubeClient, deployConfig) ctr := NewController( kubeClient, inferenceClient, kubeInformerFactory, inferenceInformerFactory, controllerFactory) return ctr, nil } ================================================ FILE: modelzetes/pkg/controller/replicas_test.go ================================================ package controller import ( "testing" appsv1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes/fake" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/k8s" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" ) func Test_Replicas(t *testing.T) { scenarios := []struct { name string inference *v2alpha1.Inference deploy *appsv1.Deployment expected *int32 }{ { "return nil replicas when label is missing and deployment does not exist", &v2alpha1.Inference{}, nil, nil, }, { "return nil replicas when label is missing and deployment has no replicas", &v2alpha1.Inference{}, &appsv1.Deployment{}, nil, }, { "return min replicas when label is present and deployment has nil replicas", &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Scaling: &v2alpha1.ScalingConfig{ MinReplicas: Ptr(int32(2)), }, }, }, &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: nil}}, int32p(2), }, { "return min replicas when label is present and deployment has replicas less than min", &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Scaling: &v2alpha1.ScalingConfig{ MinReplicas: Ptr(int32(2)), }, }, }, &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: int32p(1)}}, int32p(2), }, { "return existing replicas when label is present and deployment has more replicas than min", &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Scaling: &v2alpha1.ScalingConfig{ MinReplicas: Ptr(int32(2)), }, }, }, &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: int32p(3)}}, int32p(3), }, { "return existing replicas when label is missing and deployment has replicas set by HPA", &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Scaling: &v2alpha1.ScalingConfig{}, }, }, &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: int32p(3)}}, int32p(3), }, { "return zero replicas when label is present and deployment has zero replicas", &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Scaling: &v2alpha1.ScalingConfig{ MinReplicas: Ptr(int32(2)), }, }, }, &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: int32p(0)}}, int32p(2), }, } factory := NewFunctionFactory(fake.NewSimpleClientset(), k8s.DeploymentConfig{ LivenessProbe: &k8s.ProbeConfig{}, ReadinessProbe: &k8s.ProbeConfig{}, StartupProbe: &k8s.ProbeConfig{}, }) for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { deploy := newDeployment(s.inference, s.deploy, nil, factory) value := deploy.Spec.Replicas if s.expected != nil && value != nil { if *s.expected != *value { t.Errorf("incorrect replica count: expected %v, got %v", *s.expected, *value) } } else if s.expected != value { t.Errorf("incorrect replica count: expected %v, got %v", s.expected, value) } }) } } ================================================ FILE: modelzetes/pkg/controller/secrets.go ================================================ package controller import ( "fmt" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" ) const ( secretsMountPath = "/var/openfaas/secrets" ) // UpdateSecrets will update the Deployment spec to include secrets that have been deployed // in the kubernetes cluster. For each requested secret, we inspect the type and add it to the // deployment spec as appropriate: secrets with type `SecretTypeDockercfg` are added as ImagePullSecrets // all other secrets are mounted as files in the deployments containers. func UpdateSecrets(function *v2alpha1.Inference, deployment *appsv1.Deployment, existingSecrets map[string]*corev1.Secret) error { // Add / reference pre-existing secrets within Kubernetes secretVolumeProjections := []corev1.VolumeProjection{} for _, secretName := range function.Spec.Secrets { deployedSecret, ok := existingSecrets[secretName] if !ok { return fmt.Errorf("required secret '%s' was not found in the cluster", secretName) } switch deployedSecret.Type { case corev1.SecretTypeDockercfg, corev1.SecretTypeDockerConfigJson: deployment.Spec.Template.Spec.ImagePullSecrets = append( deployment.Spec.Template.Spec.ImagePullSecrets, corev1.LocalObjectReference{ Name: secretName, }, ) default: projectedPaths := []corev1.KeyToPath{} for secretKey := range deployedSecret.Data { projectedPaths = append(projectedPaths, corev1.KeyToPath{Key: secretKey, Path: secretKey}) } projection := &corev1.SecretProjection{Items: projectedPaths} projection.Name = secretName secretProjection := corev1.VolumeProjection{ Secret: projection, } secretVolumeProjections = append(secretVolumeProjections, secretProjection) } } volumeName := fmt.Sprintf("%s-projected-secrets", function.Spec.Name) projectedSecrets := corev1.Volume{ Name: volumeName, VolumeSource: corev1.VolumeSource{ Projected: &corev1.ProjectedVolumeSource{ Sources: secretVolumeProjections, }, }, } // remove the existing secrets volume, if we can find it. The update volume will be // added below existingVolumes := removeVolume(volumeName, deployment.Spec.Template.Spec.Volumes) deployment.Spec.Template.Spec.Volumes = existingVolumes if len(secretVolumeProjections) > 0 { deployment.Spec.Template.Spec.Volumes = append(existingVolumes, projectedSecrets) } // add mount secret as a file updatedContainers := []corev1.Container{} for _, container := range deployment.Spec.Template.Spec.Containers { mount := corev1.VolumeMount{ Name: volumeName, ReadOnly: true, MountPath: secretsMountPath, } // remove the existing secrets volume mount, if we can find it. We update it later. container.VolumeMounts = removeVolumeMount(volumeName, container.VolumeMounts) if len(secretVolumeProjections) > 0 { container.VolumeMounts = append(container.VolumeMounts, mount) } updatedContainers = append(updatedContainers, container) } deployment.Spec.Template.Spec.Containers = updatedContainers return nil } // removeVolume returns a Volume slice with any volumes matching volumeName removed. // Uses the filter without allocation technique // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating func removeVolume(volumeName string, volumes []corev1.Volume) []corev1.Volume { newVolumes := volumes[:0] for _, v := range volumes { if v.Name != volumeName { newVolumes = append(newVolumes, v) } } return newVolumes } // removeVolumeMount returns a VolumeMount slice with any mounts matching volumeName removed // Uses the filter without allocation technique // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating func removeVolumeMount(volumeName string, mounts []corev1.VolumeMount) []corev1.VolumeMount { newMounts := mounts[:0] for _, v := range mounts { if v.Name != volumeName { newMounts = append(newMounts, v) } } return newMounts } ================================================ FILE: modelzetes/pkg/controller/secrets_test.go ================================================ package controller import ( "fmt" "testing" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" ) func Test_UpdateSecrets_DoesNotAddVolumeIfRequestSecretsIsNil(t *testing.T) { request := &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Secrets: nil, }, } existingSecrets := map[string]*corev1.Secret{ "pullsecret": {Type: corev1.SecretTypeDockercfg}, "testsecret": {Type: corev1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } deployment := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } err := UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } validateEmptySecretVolumesAndMounts(t, deployment) } func Test_UpdateSecrets_DoesNotAddVolumeIfRequestSecretsIsEmpty(t *testing.T) { request := &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Secrets: []string{}, }, } existingSecrets := map[string]*corev1.Secret{ "pullsecret": {Type: corev1.SecretTypeDockercfg}, "testsecret": {Type: corev1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } deployment := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } err := UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } validateEmptySecretVolumesAndMounts(t, deployment) } func Test_UpdateSecrets_RemovesAllCopiesOfExitingSecretsVolumes(t *testing.T) { volumeName := "testfunc-projected-secrets" request := &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Secrets: []string{}, }, } existingSecrets := map[string]*corev1.Secret{ "pullsecret": {Type: corev1.SecretTypeDockercfg}, "testsecret": {Type: corev1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } deployment := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "testfunc", Image: "alpine:latest", VolumeMounts: []corev1.VolumeMount{ { Name: volumeName, }, { Name: volumeName, }, }, }, }, Volumes: []corev1.Volume{ { Name: volumeName, }, { Name: volumeName, }, }, }, }, }, } err := UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } validateEmptySecretVolumesAndMounts(t, deployment) } func Test_UpdateSecrets_AddNewSecretVolume(t *testing.T) { request := &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Secrets: []string{"pullsecret", "testsecret"}, }, } existingSecrets := map[string]*corev1.Secret{ "pullsecret": {Type: corev1.SecretTypeDockercfg}, "testsecret": {Type: corev1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } deployment := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } err := UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } validateNewSecretVolumesAndMounts(t, deployment) } func Test_UpdateSecrets_ReplacesPreviousSecretMountWithNewMount(t *testing.T) { request := &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Secrets: []string{"pullsecret", "testsecret"}, }, } existingSecrets := map[string]*corev1.Secret{ "pullsecret": {Type: corev1.SecretTypeDockercfg}, "testsecret": {Type: corev1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } deployment := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } err := UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } // mimic the deployment already existing and deployed with the same secrets by running // UpdateSecrets twice, the first run represents the original deployment, the second run represents // retrieving the deployment from the k8s api and applying the update to it err = UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } validateNewSecretVolumesAndMounts(t, deployment) } func Test_UpdateSecrets_RemovesSecretsVolumeIfRequestSecretsIsEmptyOrNil(t *testing.T) { request := &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Secrets: []string{"pullsecret", "testsecret"}, }, } existingSecrets := map[string]*corev1.Secret{ "pullsecret": {Type: corev1.SecretTypeDockercfg}, "testsecret": {Type: corev1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } deployment := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } err := UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } validateNewSecretVolumesAndMounts(t, deployment) request = &v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Name: "testfunc", Secrets: []string{}, }, } err = UpdateSecrets(request, deployment, existingSecrets) if err != nil { t.Errorf("unexpected error %s", err.Error()) } validateEmptySecretVolumesAndMounts(t, deployment) } func validateEmptySecretVolumesAndMounts(t *testing.T, deployment *appsv1.Deployment) { numVolumes := len(deployment.Spec.Template.Spec.Volumes) if numVolumes != 0 { fmt.Printf("%+v", deployment.Spec.Template.Spec.Volumes) t.Errorf("Incorrect number of volumes: expected 0, got %d", numVolumes) } c := deployment.Spec.Template.Spec.Containers[0] numVolumeMounts := len(c.VolumeMounts) if numVolumeMounts != 0 { t.Errorf("Incorrect number of volumes mounts: expected 0, got %d", numVolumeMounts) } } func validateNewSecretVolumesAndMounts(t *testing.T, deployment *appsv1.Deployment) { numVolumes := len(deployment.Spec.Template.Spec.Volumes) if numVolumes != 1 { t.Errorf("Incorrect number of volumes: expected 1, got %d", numVolumes) } volume := deployment.Spec.Template.Spec.Volumes[0] if volume.Name != "testfunc-projected-secrets" { t.Errorf("Incorrect volume name: expected \"testfunc-projected-secrets\", got \"%s\"", volume.Name) } if volume.VolumeSource.Projected == nil { t.Error("Secrets volume is not a projected volume type") } if volume.VolumeSource.Projected.Sources[0].Secret.Items[0].Key != "filename" { t.Error("Project secret not constructed correctly") } c := deployment.Spec.Template.Spec.Containers[0] numVolumeMounts := len(c.VolumeMounts) if numVolumeMounts != 1 { t.Errorf("Incorrect number of volumes mounts: expected 1, got %d", numVolumeMounts) } mount := c.VolumeMounts[0] if mount.Name != "testfunc-projected-secrets" { t.Errorf("Incorrect volume mounts: expected \"testfunc-projected-secrets\", got \"%s\"", mount.Name) } if mount.MountPath != secretsMountPath { t.Errorf("Incorrect volume mount path: expected \"%s\", got \"%s\"", secretsMountPath, mount.MountPath) } } ================================================ FILE: modelzetes/pkg/controller/service.go ================================================ package controller import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) // newService creates a new ClusterIP Service for a Function resource. It also sets // the appropriate OwnerReferences on the resource so handleObject can discover // the Function resource that 'owns' it. func newService(function *v2alpha1.Inference) *corev1.Service { return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: consts.DefaultServicePrefix + function.Spec.Name, Namespace: function.Namespace, Annotations: map[string]string{"prometheus.io.scrape": "false"}, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(function, schema.GroupVersionKind{ Group: v2alpha1.SchemeGroupVersion.Group, Version: v2alpha1.SchemeGroupVersion.Version, Kind: v2alpha1.Kind, }), }, }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, Selector: map[string]string{consts.LabelInferenceName: function.Spec.Name}, Ports: []corev1.ServicePort{ { Name: "http", Protocol: corev1.ProtocolTCP, Port: functionPort, TargetPort: intstr.IntOrString{ Type: intstr.Int, IntVal: int32(makePort(function)), }, }, }, }, } } ================================================ FILE: modelzetes/pkg/controller/service_test.go ================================================ package controller import ( "strings" "testing" v2alpha1 "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func Test_newService(t *testing.T) { inference := &v2alpha1.Inference{ ObjectMeta: metav1.ObjectMeta{ Name: "kubesec", Namespace: "mock-space", }, Spec: v2alpha1.InferenceSpec{ Name: "kubesec", Image: "docker.io/kubesec/kubesec", HTTPProbePath: Ptr("/"), Annotations: map[string]string{}, }, } service := newService(inference) if !strings.Contains(service.ObjectMeta.Name, inference.ObjectMeta.Name) { t.Errorf("Service name %s should contains inference name %s", service.ObjectMeta.Name, inference.ObjectMeta.Name) t.Fail() } if service.ObjectMeta.Namespace != inference.ObjectMeta.Namespace { t.Errorf("Service namespace %s should be equal to inference namespace %s", service.ObjectMeta.Namespace, inference.ObjectMeta.Namespace) t.Fail() } } ================================================ FILE: modelzetes/pkg/k8s/config.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s // ProbeConfig holds the deployment liveness and readiness options type ProbeConfig struct { InitialDelaySeconds int32 TimeoutSeconds int32 PeriodSeconds int32 } // DeploymentConfig holds the global deployment options type DeploymentConfig struct { HTTPProbe bool ReadinessProbe *ProbeConfig LivenessProbe *ProbeConfig StartupProbe *ProbeConfig HuggingfacePullThroughCache bool HuggingfacePullThroughCacheEndpoint string ImagePullPolicy string RuntimeClassNvidia bool // SetNonRootUser will override the function image user to ensure that it is not root. When // true, the user will set to 12000 for all functions. SetNonRootUser bool // ProfilesNamespace defines which namespace is used to look up available Profiles. ProfilesNamespace string } ================================================ FILE: modelzetes/pkg/k8s/errors.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( k8serrors "k8s.io/apimachinery/pkg/api/errors" ) // isNotFound tests if the error is a kubernetes API error that indicates that the object // was not found or does not exist func IsNotFound(err error) bool { return k8serrors.IsNotFound(err) || k8serrors.IsGone(err) } ================================================ FILE: modelzetes/pkg/k8s/factory.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( "k8s.io/client-go/kubernetes" "github.com/tensorchord/openmodelz/modelzetes/pkg/client/clientset/versioned/typed/modelzetes/v2alpha1" ) // FunctionFactory is handling Kubernetes operations to materialise functions into deployments and services type FunctionFactory struct { Client kubernetes.Interface Config DeploymentConfig } func NewFunctionFactory(clientset kubernetes.Interface, config DeploymentConfig, inferenceclientset v2alpha1.InferenceInterface) FunctionFactory { return FunctionFactory{ Client: clientset, Config: config, } } ================================================ FILE: modelzetes/pkg/k8s/factory_test.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import "k8s.io/client-go/kubernetes/fake" func mockFactory() FunctionFactory { return NewFunctionFactory(fake.NewSimpleClientset(), DeploymentConfig{ HTTPProbe: false, LivenessProbe: &ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, ReadinessProbe: &ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, StartupProbe: &ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, }, }, nil) } ================================================ FILE: modelzetes/pkg/k8s/instance.go ================================================ package k8s import ( types "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" v1 "k8s.io/api/core/v1" ) func MakeLabelSelector(name string) map[string]string { return map[string]string{ "app": name, } } func InstanceFromPod(pod v1.Pod) *types.InferenceDeploymentInstance { i := &types.InferenceDeploymentInstance{ Spec: types.InferenceDeploymentInstanceSpec{ Namespace: pod.Namespace, Name: pod.Name, OwnerReference: pod.Labels[consts.LabelInferenceName], }, Status: types.InferenceDeploymentInstanceStatus{ StartTime: pod.Status.StartTime.Time, Reason: pod.Status.Reason, Message: pod.Status.Message, }, } switch pod.Status.Phase { case v1.PodRunning: i.Status.Phase = types.InstancePhaseRunning case v1.PodPending: i.Status.Phase = types.InstancePhasePending for _, c := range pod.Status.Conditions { if c.Type == v1.PodScheduled && c.Status == v1.ConditionFalse { i.Status.Phase = types.InstancePhaseScheduling break } } case v1.PodFailed: i.Status.Phase = types.InstancePhaseFailed case v1.PodSucceeded: i.Status.Phase = types.InstancePhaseSucceeded case v1.PodUnknown: i.Status.Phase = types.InstancePhaseUnknown } if pod.Status.ContainerStatuses[0].Started != nil && !*pod.Status.ContainerStatuses[0].Started { i.Status.Phase = types.InstancePhaseCreating if pod.Status.ContainerStatuses[0].State.Waiting != nil { i.Status.Reason = pod.Status.ContainerStatuses[0].State.Waiting.Reason i.Status.Message = pod.Status.ContainerStatuses[0].State.Waiting.Message i.Status.Phase = types.InstancePhase( pod.Status.ContainerStatuses[0].State.Waiting.Reason) } else if pod.Status.ContainerStatuses[0].State.Running != nil { i.Status.Phase = types.InstancePhaseInitializing } } return i } ================================================ FILE: modelzetes/pkg/k8s/instance_test.go ================================================ package k8s import ( "testing" "time" "github.com/google/go-cmp/cmp" types "github.com/tensorchord/openmodelz/agent/api/types" . "github.com/tensorchord/openmodelz/modelzetes/pkg/pointer" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var ( mock_time, _ = time.Parse("2006-01-02", "2023-08-31") ) func Test_InstanceFromPod(t *testing.T) { scenarios := []struct { name string pod v1.Pod expected types.InferenceDeploymentInstance }{ { "basic pod", v1.Pod{ Status: v1.PodStatus{ StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ {}, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ StartTime: mock_time, }, }, }, { "phase running pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodRunning, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ {}, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseRunning, StartTime: mock_time, }, }, }, { "phase pending pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodPending, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ {}, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhasePending, StartTime: mock_time, }, }, }, { "phase scheduling pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodPending, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ {}, }, Conditions: []v1.PodCondition{ { Type: v1.PodScheduled, Status: v1.ConditionFalse, }, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseScheduling, StartTime: mock_time, }, }, }, { "phase failed pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodFailed, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ {}, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseFailed, StartTime: mock_time, }, }, }, { "phase succeed pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodSucceeded, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ {}, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseSucceeded, StartTime: mock_time, }, }, }, { "phase unknown pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodUnknown, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ {}, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseUnknown, StartTime: mock_time, }, }, }, { "phase creating pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodUnknown, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ { Started: Ptr(false), }, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseCreating, StartTime: mock_time, }, }, }, { "phase initializing pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodUnknown, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ { Started: Ptr(false), State: v1.ContainerState{ Running: Ptr(v1.ContainerStateRunning{ StartedAt: metav1.NewTime(mock_time), }), }, }, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhaseInitializing, StartTime: mock_time, }, }, }, { "phase waiting pod", v1.Pod{ Status: v1.PodStatus{ Phase: v1.PodUnknown, StartTime: Ptr(metav1.NewTime(mock_time)), ContainerStatuses: []v1.ContainerStatus{ { Started: Ptr(false), State: v1.ContainerState{ Waiting: Ptr(v1.ContainerStateWaiting{ Reason: "mock-reason", Message: "mock-message", }), }, }, }, }, }, types.InferenceDeploymentInstance{ Status: types.InferenceDeploymentInstanceStatus{ Phase: types.InstancePhase("mock-reason"), Reason: "mock-reason", Message: "mock-message", StartTime: mock_time, }, }, }, } for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { instance := InstanceFromPod(s.pod) if diff := cmp.Diff(s.expected, *instance); diff != "" { t.Errorf("Create instance from pod: expected %v, got %v", s.expected, instance) t.Fail() } }) } } ================================================ FILE: modelzetes/pkg/k8s/log.go ================================================ // Copyright 2020 OpenFaaS Author(s) // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( "context" "log" "strings" "time" "github.com/tensorchord/openmodelz/agent/api/types" "k8s.io/client-go/kubernetes" ) // LogRequestor implements the Requestor interface for k8s type LogRequestor struct { client kubernetes.Interface functionNamespace string } // NewLogRequestor returns a new logs.Requestor that uses kail to select and follow pod logs func NewLogRequestor(client kubernetes.Interface, functionNamespace string) *LogRequestor { return &LogRequestor{ client: client, functionNamespace: functionNamespace, } } // Query implements the actual Swarm logs request logic for the Requestor interface // This implementation ignores the r.Limit value because the OF-Provider already handles server side // line limits. func (l LogRequestor) Query(ctx context.Context, r types.LogRequest) (<-chan types.Message, error) { ns := l.functionNamespace if len(r.Namespace) > 0 && strings.ToLower(r.Namespace) != "kube-system" { ns = r.Namespace } var since *time.Time if r.Since != "" { buf, err := time.Parse(time.RFC3339, r.Since) if err != nil { return nil, err } since = &buf } logStream, err := GetLogs(ctx, l.client, r.Name, ns, int64(r.Tail), since, r.Follow) if err != nil { log.Printf("LogRequestor: get logs failed: %s\n", err) return nil, err } msgStream := make(chan types.Message, LogBufferSize) go func() { defer close(msgStream) // here we depend on the fact that logStream will close when the context is cancelled, // this ensures that the go routine will resolve for msg := range logStream { msgStream <- types.Message{ Timestamp: msg.Timestamp, Text: msg.Text, Name: msg.FunctionName, Instance: msg.PodName, Namespace: msg.Namespace, } } }() return msgStream, nil } ================================================ FILE: modelzetes/pkg/k8s/logs.go ================================================ package k8s import ( "bufio" "bytes" "context" "io" "log" "strings" "time" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/informers" "k8s.io/client-go/informers/internalinterfaces" "k8s.io/client-go/kubernetes" v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" ) const ( // podInformerResync is the period between cache syncs in the pod informer podInformerResync = 5 * time.Second // defaultLogSince is the fallback log stream history defaultLogSince = 5 * time.Minute // LogBufferSize number of log messages that may be buffered LogBufferSize = 500 * 2 ) // Log is the object which will be used together with the template to generate // the output. type Log struct { // Text is the log message itself Text string `json:"text"` // Namespace of the pod Namespace string `json:"namespace"` // PodName of the instance PodName string `json:"podName"` // FunctionName of the pod FunctionName string `json:"FunctionName"` // Timestamp of the message Timestamp time.Time `json:"timestamp"` } // GetLogs returns a channel of logs for the given function func GetLogs(ctx context.Context, client kubernetes.Interface, functionName, namespace string, tail int64, since *time.Time, follow bool) (<-chan Log, error) { added, err := startFunctionPodInformer(ctx, client, functionName, namespace) if err != nil { return nil, err } logs := make(chan Log, LogBufferSize) go func() { var watching uint defer close(logs) finished := make(chan error) for { select { case <-ctx.Done(): return case <-finished: watching-- if watching == 0 && !follow { return } case p := <-added: watching++ go func() { finished <- podLogs(ctx, client.CoreV1().Pods(namespace), p, functionName, namespace, tail, since, follow, logs) }() } } }() return logs, nil } // podLogs returns a stream of logs lines from the specified pod func podLogs(ctx context.Context, i v1.PodInterface, pod, container, namespace string, tail int64, since *time.Time, follow bool, dst chan<- Log) error { log.Printf("Logger: starting log stream for %s\n", pod) defer log.Printf("Logger: stopping log stream for %s\n", pod) opts := &corev1.PodLogOptions{ Follow: follow, Timestamps: true, Container: container, } if tail > 0 { opts.TailLines = &tail } if opts.TailLines == nil || since != nil { opts.SinceSeconds = parseSince(since) } stream, err := i.GetLogs(pod, opts).Stream(context.TODO()) if err != nil { return err } defer stream.Close() done := make(chan error) go func() { reader := bufio.NewReader(stream) for { line, err := reader.ReadBytes('\n') if err != nil { done <- err return } msg, ts := extractTimestampAndMsg(string(bytes.Trim(line, "\x00"))) dst <- Log{ Timestamp: ts, Text: msg, PodName: pod, FunctionName: container, Namespace: namespace, } } }() select { case <-ctx.Done(): return ctx.Err() case err := <-done: if err != io.EOF { return err } return nil } } func extractTimestampAndMsg(logText string) (string, time.Time) { // first 32 characters is the k8s timestamp parts := strings.SplitN(logText, " ", 2) ts, err := time.Parse(time.RFC3339Nano, parts[0]) if err != nil { log.Printf("error: invalid timestamp '%s'\n", parts[0]) return "", time.Time{} } if len(parts) == 2 { return parts[1], ts } return "", ts } // parseSince returns the time.Duration of the requested Since value _or_ 5 minutes func parseSince(r *time.Time) *int64 { var since int64 if r == nil || r.IsZero() { since = int64(defaultLogSince.Seconds()) return &since } since = int64(time.Since(*r).Seconds()) return &since } // startFunctionPodInformer will gather the list of existing Pods for the function, it will then watch // and watch for newly added or deleted function instances. func startFunctionPodInformer(ctx context.Context, client kubernetes.Interface, functionName, namespace string) (<-chan string, error) { functionSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{consts.LabelInferenceName: functionName}, } selector, err := metav1.LabelSelectorAsSelector(functionSelector) if err != nil { err = errors.Wrap(err, "unable to build function selector") log.Printf("PodInformer: %s", err) return nil, err } log.Printf("PodInformer: starting informer for %s in: %s\n", selector.String(), namespace) factory := informers.NewFilteredSharedInformerFactory( client, podInformerResync, namespace, withLabels(selector.String()), ) podInformer := factory.Core().V1().Pods() podsResp, err := client.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()}) if err != nil { log.Printf("PodInformer: %s", err) return nil, err } pods := podsResp.Items if len(pods) == 0 { err = errors.New("no matching instances found") log.Printf("PodInformer: %s", err) return nil, k8serrors.NewNotFound(corev1.Resource("pods"), selector.String()) } // prepare channel with enough space for the current instance set added := make(chan string, len(pods)) podInformer.Informer().AddEventHandler(&podLoggerEventHandler{ added: added, }) // will add existing pods to the chan and then listen for any new pods go podInformer.Informer().Run(ctx.Done()) go func() { <-ctx.Done() close(added) }() return added, nil } func withLabels(selector string) internalinterfaces.TweakListOptionsFunc { return func(opts *metav1.ListOptions) { opts.LabelSelector = selector } } type podLoggerEventHandler struct { cache.ResourceEventHandler added chan<- string deleted chan<- string } func (h *podLoggerEventHandler) OnAdd(obj interface{}, isInitialList bool) { pod := obj.(*corev1.Pod) log.Printf("PodInformer: adding instance: %s", pod.Name) h.added <- pod.Name } func (h *podLoggerEventHandler) OnUpdate(oldObj, newObj interface{}) { // purposefully empty, we don't need to do anything for logs on update } func (h *podLoggerEventHandler) OnDelete(obj interface{}) { // this may not be needed, the log stream Reader _should_ close on its own without // us needing to watch and close it // pod := obj.(*corev1.Pod) // h.deleted <- pod.Name } ================================================ FILE: modelzetes/pkg/k8s/probes.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) type FunctionProbes struct { Liveness *corev1.Probe Readiness *corev1.Probe Startup *corev1.Probe } // MakeProbes returns the liveness and readiness probes // by default the health check runs `cat /tmp/.lock` every ten seconds func (f *FunctionFactory) MakeProbes(port int, httpProbePath string) (*FunctionProbes, error) { var handler corev1.ProbeHandler if f.Config.HTTPProbe { handler = corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ Path: httpProbePath, Port: intstr.IntOrString{ Type: intstr.Int, IntVal: int32(port), }, }, } } else { return nil, nil } probes := FunctionProbes{} probes.Readiness = &corev1.Probe{ ProbeHandler: handler, InitialDelaySeconds: f.Config.ReadinessProbe.InitialDelaySeconds, TimeoutSeconds: int32(f.Config.ReadinessProbe.TimeoutSeconds), PeriodSeconds: int32(f.Config.ReadinessProbe.PeriodSeconds), SuccessThreshold: 1, FailureThreshold: 3, } probes.Liveness = &corev1.Probe{ ProbeHandler: handler, InitialDelaySeconds: f.Config.LivenessProbe.InitialDelaySeconds, TimeoutSeconds: int32(f.Config.LivenessProbe.TimeoutSeconds), PeriodSeconds: int32(f.Config.LivenessProbe.PeriodSeconds), SuccessThreshold: 1, FailureThreshold: 3, } probes.Startup = &corev1.Probe{ ProbeHandler: handler, InitialDelaySeconds: f.Config.StartupProbe.InitialDelaySeconds, TimeoutSeconds: int32(f.Config.StartupProbe.TimeoutSeconds), PeriodSeconds: int32(f.Config.StartupProbe.PeriodSeconds), SuccessThreshold: 1, // Set failure threshold to 30 to allow for slow-starting inferences. FailureThreshold: 30, } return &probes, nil } ================================================ FILE: modelzetes/pkg/k8s/probes_test.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( "testing" ) func Test_makeProbes_useHTTPProbe(t *testing.T) { f := mockFactory() f.Config.HTTPProbe = true probes, err := f.MakeProbes(8080, "/") if err != nil { t.Fatal(err) } if probes.Readiness.HTTPGet == nil { t.Errorf("Readiness probe should have had HTTPGet handler") t.Fail() } if probes.Liveness.HTTPGet == nil { t.Errorf("Liveness probe should have had HTTPGet handler") t.Fail() } } func Test_makeProbes_useCustomDurationHTTPProbe(t *testing.T) { f := mockFactory() f.Config.HTTPProbe = true f.Config.LivenessProbe = &ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, } f.Config.ReadinessProbe = &ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, } f.Config.StartupProbe = &ProbeConfig{ PeriodSeconds: 1, TimeoutSeconds: 3, InitialDelaySeconds: 0, } customDelay := "0" probes, err := f.MakeProbes(8080, "/") if err != nil { t.Fatal(err) } if probes.Readiness.HTTPGet == nil { t.Errorf("Readiness probe should have had HTTPGet handler") t.Fail() } if probes.Readiness.InitialDelaySeconds != 0 { t.Errorf("Readiness probe should have initial delay seconds set to %s", customDelay) t.Fail() } if probes.Liveness.HTTPGet == nil { t.Errorf("Liveness probe should have had HTTPGet handler") t.Fail() } if probes.Liveness.InitialDelaySeconds != 0 { t.Errorf("Readiness probe should have had HTTPGet handler set to %s", customDelay) t.Fail() } } ================================================ FILE: modelzetes/pkg/k8s/proxy.go ================================================ // Copyright (c) Alex Ellis 2017. All rights reserved. // Copyright 2020 OpenFaaS Author(s) // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( "fmt" "math/rand" "net/url" "strings" "sync" "github.com/tensorchord/openmodelz/modelzetes/pkg/consts" corelister "k8s.io/client-go/listers/core/v1" ) // watchdogPort for the OpenFaaS function watchdog const watchdogPort = 8080 func NewFunctionLookup(ns string, lister corelister.EndpointsLister) *FunctionLookup { return &FunctionLookup{ DefaultNamespace: ns, EndpointLister: lister, Listers: map[string]corelister.EndpointsNamespaceLister{}, lock: sync.RWMutex{}, } } type FunctionLookup struct { DefaultNamespace string EndpointLister corelister.EndpointsLister Listers map[string]corelister.EndpointsNamespaceLister lock sync.RWMutex } func (f *FunctionLookup) GetLister(ns string) corelister.EndpointsNamespaceLister { f.lock.RLock() defer f.lock.RUnlock() return f.Listers[ns] } func (f *FunctionLookup) SetLister(ns string, lister corelister.EndpointsNamespaceLister) { f.lock.Lock() defer f.lock.Unlock() f.Listers[ns] = lister } func getNamespace(name, defaultNamespace string) string { namespace := defaultNamespace if strings.Contains(name, ".") { namespace = name[strings.LastIndexAny(name, ".")+1:] } return namespace } func (l *FunctionLookup) Resolve(name string) (url.URL, error) { functionName := name namespace := getNamespace(name, l.DefaultNamespace) if err := l.verifyNamespace(namespace); err != nil { return url.URL{}, err } if strings.Contains(name, ".") { functionName = strings.TrimSuffix(name, "."+namespace) } nsEndpointLister := l.GetLister(namespace) if nsEndpointLister == nil { l.SetLister(namespace, l.EndpointLister.Endpoints(namespace)) nsEndpointLister = l.GetLister(namespace) } svcName := consts.DefaultServicePrefix + functionName svc, err := nsEndpointLister.Get(svcName) if err != nil { return url.URL{}, fmt.Errorf("error listing \"%s.%s\": %s", svcName, namespace, err.Error()) } if len(svc.Subsets) == 0 { return url.URL{}, fmt.Errorf("no subsets available for \"%s.%s\"", svcName, namespace) } all := len(svc.Subsets[0].Addresses) if len(svc.Subsets[0].Addresses) == 0 { return url.URL{}, fmt.Errorf("no addresses in subset for \"%s.%s\"", svcName, namespace) } target := rand.Intn(all) serviceIP := svc.Subsets[0].Addresses[target].IP servicePort := svc.Subsets[0].Ports[target].Port urlStr := fmt.Sprintf("http://%s:%d", serviceIP, servicePort) urlRes, err := url.Parse(urlStr) if err != nil { return url.URL{}, err } return *urlRes, nil } func (l *FunctionLookup) verifyNamespace(name string) error { if name != "kube-system" { return nil } // ToDo use global namespace parse and validation return fmt.Errorf("namespace not allowed") } ================================================ FILE: modelzetes/pkg/k8s/proxy_test.go ================================================ package k8s import ( "fmt" "strings" "testing" corelister "k8s.io/client-go/listers/core/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" ) type FakeLister struct { } func (f FakeLister) List(selector labels.Selector) (ret []*corev1.Endpoints, err error) { return nil, nil } func (f FakeLister) Endpoints(namespace string) corelister.EndpointsNamespaceLister { return FakeNSLister{} } type FakeNSLister struct { } func (f FakeNSLister) List(selector labels.Selector) (ret []*corev1.Endpoints, err error) { return nil, nil } func (f FakeNSLister) Get(name string) (*corev1.Endpoints, error) { // make sure that we only send the function name to the lister if strings.Contains(name, ".") { return nil, fmt.Errorf("can not look up function name with a dot!") } ep := corev1.Endpoints{ Subsets: []corev1.EndpointSubset{{ Addresses: []corev1.EndpointAddress{{IP: "127.0.0.1"}}, Ports: []corev1.EndpointPort{{Port: 8080}}, }}, } return &ep, nil } func Test_FunctionLookup(t *testing.T) { lister := FakeLister{} resolver := NewFunctionLookup("testDefault", lister) cases := []struct { name string funcName string expError string expUrl string }{ { name: "function without namespace uses default namespace", funcName: "testfunc", expUrl: "http://127.0.0.1:8080", }, { name: "function with namespace uses the given namespace", funcName: "testfunc.othernamespace", expUrl: "http://127.0.0.1:8080", }, { name: "url parse errors are returned", funcName: "testfunc.kube-system", expError: "namespace not allowed", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { url, err := resolver.Resolve(tc.funcName) if tc.expError == "" && err != nil { t.Fatalf("expected no error, got %s", err) } if tc.expError != "" && (err == nil || !strings.Contains(err.Error(), tc.expError)) { t.Fatalf("expected %s, got %s", tc.expError, err) } if url.String() != tc.expUrl { t.Fatalf("expected url %s, got %s", tc.expUrl, url.String()) } }) } } ================================================ FILE: modelzetes/pkg/k8s/secrets.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( "context" "fmt" "log" "sort" "strings" "github.com/pkg/errors" types "github.com/tensorchord/openmodelz/agent/api/types" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" appsv1 "k8s.io/api/apps/v1" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" typedV1 "k8s.io/client-go/kubernetes/typed/core/v1" ) const ( secretsMountPath = "/var/modelz/secrets" secretLabel = "app.kubernetes.io/managed-by" secretLabelValue = "modelz" secretsProjectVolumeNameTmpl = "projected-secrets" ) // SecretsClient exposes the standardized CRUD behaviors for Kubernetes secrets. These methods // will ensure that the secrets are structured and labelled correctly for use by the modelz system. type SecretsClient interface { // List returns a list of available function secrets. Only the names are returned // to ensure we do not accidentally read or print the sensitive values during // read operations. List(namespace string) (names []string, err error) // Create adds a new secret, with the appropriate labels and structure to be // used as a function secret. Create(secret types.Secret) error // Replace updates the value of a function secret Replace(secret types.Secret) error // Delete removes a function secret Delete(name string, namespace string) error // GetSecrets queries Kubernetes for a list of secrets by name in the given k8s namespace. // This should only be used if you need access to the actual secret structure/value. Specifically, // inside the FunctionFactory. GetSecrets(namespace string, secretNames []string) (map[string]*apiv1.Secret, error) } // SecretInterfacer exposes the SecretInterface getter for the k8s client. // This is implemented by the CoreV1Interface() interface in the Kubernetes client. // The SecretsClient only needs this one interface, but needs to be able to set the // namespaces when the interface is instantiated, meaning, we need the Getter and not the // SecretInterface itself. type SecretInterfacer interface { // Secrets returns a SecretInterface scoped to the specified namespace Secrets(namespace string) typedV1.SecretInterface } type secretClient struct { kube SecretInterfacer } // NewSecretsClient constructs a new SecretsClient using the provided Kubernetes client. func NewSecretsClient(kube kubernetes.Interface) SecretsClient { return &secretClient{ kube: kube.CoreV1(), } } func (c secretClient) List(namespace string) (names []string, err error) { res, err := c.kube.Secrets(namespace).List(context.TODO(), c.selector()) if err != nil { log.Printf("failed to list secrets in %s: %v\n", namespace, err) return nil, err } names = make([]string, len(res.Items)) for idx, item := range res.Items { // this is safe because size of names matches res.Items exactly names[idx] = item.Name } return names, nil } func (c secretClient) Create(secret types.Secret) error { err := c.validateSecret(secret) if err != nil { return err } req := &apiv1.Secret{ Type: apiv1.SecretTypeOpaque, ObjectMeta: metav1.ObjectMeta{ Name: secret.Name, Namespace: secret.Namespace, Labels: map[string]string{ secretLabel: secretLabelValue, }, }, } req.Data = c.getValidSecretData(secret) _, err = c.kube.Secrets(secret.Namespace).Create(context.TODO(), req, metav1.CreateOptions{}) if err != nil { log.Printf("failed to create secret %s.%s: %v\n", secret.Name, secret.Namespace, err) return err } log.Printf("created secret %s.%s\n", secret.Name, secret.Namespace) return nil } func (c secretClient) Replace(secret types.Secret) error { err := c.validateSecret(secret) if err != nil { return err } kube := c.kube.Secrets(secret.Namespace) found, err := kube.Get(context.TODO(), secret.Name, metav1.GetOptions{}) if err != nil { log.Printf("can not retrieve secret for update %s.%s: %v\n", secret.Name, secret.Namespace, err) return err } found.Data = c.getValidSecretData(secret) _, err = kube.Update(context.TODO(), found, metav1.UpdateOptions{}) if err != nil { log.Printf("can not update secret %s.%s: %v\n", secret.Name, secret.Namespace, err) return err } return nil } func (c secretClient) Delete(namespace string, name string) error { err := c.kube.Secrets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) if err != nil { log.Printf("can not delete %s.%s: %v\n", name, namespace, err) } return err } func (c secretClient) GetSecrets(namespace string, secretNames []string) (map[string]*apiv1.Secret, error) { kube := c.kube.Secrets(namespace) opts := metav1.GetOptions{} secrets := map[string]*apiv1.Secret{} for _, secretName := range secretNames { secret, err := kube.Get(context.TODO(), secretName, opts) if err != nil { return nil, err } secrets[secretName] = secret } return secrets, nil } func (c secretClient) selector() metav1.ListOptions { return metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", secretLabel, secretLabelValue), } } func (c secretClient) validateSecret(secret types.Secret) error { if strings.TrimSpace(secret.Namespace) == "" { return errors.New("namespace may not be empty") } if strings.TrimSpace(secret.Name) == "" { return errors.New("name may not be empty") } return nil } func (c secretClient) getValidSecretData(secret types.Secret) map[string][]byte { if len(secret.RawValue) > 0 { return map[string][]byte{ secret.Name: secret.RawValue, } } return map[string][]byte{ secret.Name: []byte(secret.Value), } } // ConfigureSecrets will update the Deployment spec to include secrets that have been deployed // in the kubernetes cluster. For each requested secret, we inspect the type and add it to the // deployment spec as appropriate: secrets with type `SecretTypeDockercfg/SecretTypeDockerjson` // are added as ImagePullSecrets all other secrets are mounted as files in the deployments containers. func (f *FunctionFactory) ConfigureSecrets(request v2alpha1.Inference, deployment *appsv1.Deployment, existingSecrets map[string]*apiv1.Secret) error { // Add / reference pre-existing secrets within Kubernetes secretVolumeProjections := []apiv1.VolumeProjection{} for _, secretName := range request.Spec.Secrets { deployedSecret, ok := existingSecrets[secretName] if !ok { return fmt.Errorf("required secret '%s' was not found in the cluster", secretName) } switch deployedSecret.Type { case apiv1.SecretTypeDockercfg, apiv1.SecretTypeDockerConfigJson: deployment.Spec.Template.Spec.ImagePullSecrets = append( deployment.Spec.Template.Spec.ImagePullSecrets, apiv1.LocalObjectReference{ Name: secretName, }, ) default: projectedPaths := []apiv1.KeyToPath{} for secretKey := range deployedSecret.Data { projectedPaths = append(projectedPaths, apiv1.KeyToPath{Key: secretKey, Path: secretKey}) } projection := &apiv1.SecretProjection{Items: projectedPaths} projection.Name = secretName secretProjection := apiv1.VolumeProjection{ Secret: projection, } secretVolumeProjections = append(secretVolumeProjections, secretProjection) } } volumeName := secretsProjectVolumeNameTmpl projectedSecrets := apiv1.Volume{ Name: volumeName, VolumeSource: apiv1.VolumeSource{ Projected: &apiv1.ProjectedVolumeSource{ Sources: secretVolumeProjections, }, }, } // remove the existing secrets volume, if we can find it. The update volume will be // added below existingVolumes := removeVolume(volumeName, deployment.Spec.Template.Spec.Volumes) deployment.Spec.Template.Spec.Volumes = existingVolumes if len(secretVolumeProjections) > 0 { deployment.Spec.Template.Spec.Volumes = append(existingVolumes, projectedSecrets) } // add mount secret as a file updatedContainers := []apiv1.Container{} for _, container := range deployment.Spec.Template.Spec.Containers { mount := apiv1.VolumeMount{ Name: volumeName, ReadOnly: true, MountPath: secretsMountPath, } // remove the existing secrets volume mount, if we can find it. We update it later. container.VolumeMounts = removeVolumeMount(volumeName, container.VolumeMounts) if len(secretVolumeProjections) > 0 { container.VolumeMounts = append(container.VolumeMounts, mount) } updatedContainers = append(updatedContainers, container) } deployment.Spec.Template.Spec.Containers = updatedContainers return nil } // ReadFunctionSecretsSpec parses the name of the required function secrets. This is the inverse of ConfigureSecrets. func ReadFunctionSecretsSpec(item appsv1.Deployment) []string { secrets := []string{} for _, s := range item.Spec.Template.Spec.ImagePullSecrets { secrets = append(secrets, s.Name) } volumeName := secretsProjectVolumeNameTmpl var sourceSecrets []apiv1.VolumeProjection for _, v := range item.Spec.Template.Spec.Volumes { if v.Name == volumeName { sourceSecrets = v.Projected.Sources break } } for _, s := range sourceSecrets { if s.Secret == nil { continue } secrets = append(secrets, s.Secret.Name) } sort.Strings(secrets) return secrets } ================================================ FILE: modelzetes/pkg/k8s/secrets_factory_test.go ================================================ // Copyright 2020 OpenFaaS Author(s) // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( "fmt" "testing" "github.com/tensorchord/openmodelz/modelzetes/pkg/apis/modelzetes/v2alpha1" appsv1 "k8s.io/api/apps/v1" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func Test_ReadFunctionSecretsSpec(t *testing.T) { f := mockFactory() existingSecrets := map[string]*apiv1.Secret{ "pullsecret": {Type: apiv1.SecretTypeDockercfg}, "testsecret": {Type: apiv1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } functionDep := appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "testfunc"}, Spec: appsv1.DeploymentSpec{ Template: apiv1.PodTemplateSpec{ Spec: apiv1.PodSpec{ Containers: []apiv1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } cases := []struct { name string req v2alpha1.Inference deployment appsv1.Deployment expected []string }{ { name: "empty secrets, returns empty slice", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{}, }, }, deployment: functionDep, expected: []string{}, }, { name: "detects and extracts image pull secret", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{"pullsecret"}, }, }, deployment: functionDep, expected: []string{"pullsecret"}, }, { name: "detects and extracts projected generic secret", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{"testsecret"}, }, }, deployment: functionDep, expected: []string{"testsecret"}, }, { name: "detects and extracts both pull secrets and projected generic secret, result is sorted", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{"pullsecret", "testsecret"}, }, }, deployment: functionDep, expected: []string{"pullsecret", "testsecret"}, }, } for _, tc := range cases { err := f.ConfigureSecrets(tc.req, &tc.deployment, existingSecrets) if err != nil { t.Fatalf("unexpected error result: got %q", err) } parsedSecrets := ReadFunctionSecretsSpec(tc.deployment) if len(tc.expected) != len(parsedSecrets) { t.Fatalf("incorrect secret count, expected: %v, got: %v", tc.expected, parsedSecrets) } for idx, expected := range tc.expected { value := parsedSecrets[idx] if expected != value { t.Fatalf("incorrect secret in idx %d, expected: %q, got: %q", idx, expected, value) } } } } func Test_FunctionFactory_ConfigureSecrets(t *testing.T) { f := mockFactory() existingSecrets := map[string]*apiv1.Secret{ "pullsecret": {Type: apiv1.SecretTypeDockercfg}, "testsecret": {Type: apiv1.SecretTypeOpaque, Data: map[string][]byte{"filename": []byte("contents")}}, } basicDeployment := appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: apiv1.PodTemplateSpec{ Spec: apiv1.PodSpec{ Containers: []apiv1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } volumeName := "projected-secrets" withExistingSecret := appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: apiv1.PodTemplateSpec{ Spec: apiv1.PodSpec{ Containers: []apiv1.Container{ { Name: "testfunc", Image: "alpine:latest", VolumeMounts: []apiv1.VolumeMount{ { Name: volumeName, }, { Name: volumeName, }, }, }, }, Volumes: []apiv1.Volume{ { Name: volumeName, }, { Name: volumeName, }, }, }, }, }, } cases := []struct { name string req v2alpha1.Inference deployment appsv1.Deployment validator func(t *testing.T, deployment *appsv1.Deployment) err error }{ { name: "does not add volume if request secrets is nil", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{}, }, deployment: basicDeployment, validator: validateEmptySecretVolumesAndMounts, }, { name: "does not add volume if request secrets is nil", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{}, }, }, deployment: basicDeployment, validator: validateEmptySecretVolumesAndMounts, }, { name: "removes all copies of exiting secrets volumes", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{}, }, }, deployment: withExistingSecret, validator: validateEmptySecretVolumesAndMounts, }, { name: "add new secret volume", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{"pullsecret", "testsecret"}, }, }, deployment: basicDeployment, validator: validateNewSecretVolumesAndMounts, }, { name: "replaces previous secret mount with new mount", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{"pullsecret", "testsecret"}, }, }, deployment: withExistingSecret, validator: validateNewSecretVolumesAndMounts, }, { name: "removes secrets volume if request secrets is empty or nil", req: v2alpha1.Inference{ Spec: v2alpha1.InferenceSpec{ Secrets: []string{}, }, }, deployment: withExistingSecret, validator: validateEmptySecretVolumesAndMounts, }, } for _, tc := range cases { err := f.ConfigureSecrets(tc.req, &tc.deployment, existingSecrets) if err != tc.err { t.Errorf("unexpected error result: got %v, expected %v", err, tc.err) } tc.validator(t, &tc.deployment) } } func validateEmptySecretVolumesAndMounts(t *testing.T, deployment *appsv1.Deployment) { numVolumes := len(deployment.Spec.Template.Spec.Volumes) if numVolumes != 0 { fmt.Printf("%+v", deployment.Spec.Template.Spec.Volumes) t.Errorf("Incorrect number of volumes: expected 0, got %d", numVolumes) } c := deployment.Spec.Template.Spec.Containers[0] numVolumeMounts := len(c.VolumeMounts) if numVolumeMounts != 0 { t.Errorf("Incorrect number of volumes mounts: expected 0, got %d", numVolumeMounts) } } func validateNewSecretVolumesAndMounts(t *testing.T, deployment *appsv1.Deployment) { numVolumes := len(deployment.Spec.Template.Spec.Volumes) if numVolumes != 1 { t.Errorf("Incorrect number of volumes: expected 1, got %d", numVolumes) } volume := deployment.Spec.Template.Spec.Volumes[0] if volume.Name != "projected-secrets" { t.Errorf("Incorrect volume name: expected \"projected-secrets\", got \"%s\"", volume.Name) } if volume.VolumeSource.Projected == nil { t.Error("Secrets volume is not a projected volume type") } if volume.VolumeSource.Projected.Sources[0].Secret.Items[0].Key != "filename" { t.Error("Project secret not constructed correctly") } c := deployment.Spec.Template.Spec.Containers[0] numVolumeMounts := len(c.VolumeMounts) if numVolumeMounts != 1 { t.Errorf("Incorrect number of volumes mounts: expected 1, got %d", numVolumeMounts) } mount := c.VolumeMounts[0] if mount.Name != "projected-secrets" { t.Errorf("Incorrect volume mounts: expected \"projected-secrets\", got \"%s\"", mount.Name) } if mount.MountPath != secretsMountPath { t.Errorf("Incorrect volume mount path: expected \"%s\", got \"%s\"", secretsMountPath, mount.MountPath) } } ================================================ FILE: modelzetes/pkg/k8s/securityContext.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" ) // nonRootFunctionuserID is the user id that is set when DeployHandlerConfig.SetNonRootUser is true. // value >10000 per the suggestion from https://kubesec.io/basics/containers-securitycontext-runasuser/ const SecurityContextUserID = int64(12000) // ConfigureContainerUserID sets the UID to 12000 for the function Container. Defaults to user // specified in image metadata if `SetNonRootUser` is `false`. Root == 0. func (f *FunctionFactory) ConfigureContainerUserID(deployment *appsv1.Deployment) { userID := SecurityContextUserID var functionUser *int64 if f.Config.SetNonRootUser { functionUser = &userID } if deployment.Spec.Template.Spec.Containers[0].SecurityContext == nil { deployment.Spec.Template.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{} } deployment.Spec.Template.Spec.Containers[0].SecurityContext.RunAsUser = functionUser } // ConfigureReadOnlyRootFilesystem will create or update the required settings and mounts to ensure // that the ReadOnlyRootFilesystem setting works as expected, meaning: // 1. when ReadOnlyRootFilesystem is true, the security context of the container will have ReadOnlyRootFilesystem also // marked as true and a new `/tmp` folder mount will be added to the deployment spec // 2. when ReadOnlyRootFilesystem is false, the security context of the container will also have ReadOnlyRootFilesystem set // to false and there will be no mount for the `/tmp` folder // // This method is safe for both create and update operations. func (f *FunctionFactory) ConfigureReadOnlyRootFilesystem(deployment *appsv1.Deployment) { readonly := false if deployment.Spec.Template.Spec.Containers[0].SecurityContext != nil { deployment.Spec.Template.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readonly } else { deployment.Spec.Template.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{ ReadOnlyRootFilesystem: &readonly, } } existingVolumes := removeVolume("temp", deployment.Spec.Template.Spec.Volumes) deployment.Spec.Template.Spec.Volumes = existingVolumes existingMounts := removeVolumeMount("temp", deployment.Spec.Template.Spec.Containers[0].VolumeMounts) deployment.Spec.Template.Spec.Containers[0].VolumeMounts = existingMounts } ================================================ FILE: modelzetes/pkg/k8s/securityContext_test.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( "testing" appsv1 "k8s.io/api/apps/v1" apiv1 "k8s.io/api/core/v1" ) func readOnlyRootDisabled(t *testing.T, deployment *appsv1.Deployment) { if len(deployment.Spec.Template.Spec.Volumes) != 0 { t.Error("Volumes should be empty if ReadOnlyRootFilesystem is false") } if len(deployment.Spec.Template.Spec.Containers[0].VolumeMounts) != 0 { t.Error("VolumeMounts should be empty if ReadOnlyRootFilesystem is false") } functionContatiner := deployment.Spec.Template.Spec.Containers[0] if functionContatiner.SecurityContext != nil { if *functionContatiner.SecurityContext.ReadOnlyRootFilesystem != false { t.Error("ReadOnlyRootFilesystem should be false on the container SecurityContext") } } } func Test_configureReadOnlyRootFilesystem_Disabled_To_Disabled(t *testing.T) { f := mockFactory() deployment := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: apiv1.PodTemplateSpec{ Spec: apiv1.PodSpec{ Containers: []apiv1.Container{ {Name: "testfunc", Image: "alpine:latest"}, }, }, }, }, } f.ConfigureReadOnlyRootFilesystem(deployment) readOnlyRootDisabled(t, deployment) } ================================================ FILE: modelzetes/pkg/k8s/utils.go ================================================ // Copyright 2020 OpenFaaS Authors // Licensed under the MIT license. See LICENSE file in the project root for full license information. package k8s import ( corev1 "k8s.io/api/core/v1" ) // removeVolume returns a Volume slice with any volumes matching volumeName removed. // Uses the filter without allocation technique // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating func removeVolume(volumeName string, volumes []corev1.Volume) []corev1.Volume { if volumes == nil { return []corev1.Volume{} } newVolumes := volumes[:0] for _, v := range volumes { if v.Name != volumeName { newVolumes = append(newVolumes, v) } } return newVolumes } // removeVolumeMount returns a VolumeMount slice with any mounts matching volumeName removed // Uses the filter without allocation technique // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating func removeVolumeMount(volumeName string, mounts []corev1.VolumeMount) []corev1.VolumeMount { if mounts == nil { return []corev1.VolumeMount{} } newMounts := mounts[:0] for _, v := range mounts { if v.Name != volumeName { newMounts = append(newMounts, v) } } return newMounts } ================================================ FILE: modelzetes/pkg/pointer/ptr.go ================================================ package util func Ptr[T any](v T) *T { return &v } func PtrCopy[T any](v T) *T { n := new(T) *n = v return n } ================================================ FILE: modelzetes/pkg/signals/signal.go ================================================ package signals import ( "os" "os/signal" ) var onlyOneSignalHandler = make(chan struct{}) // SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned // which is closed on one of these signals. If a second signal is caught, the program // is terminated with exit code 1. func SetupSignalHandler() (stopCh <-chan struct{}) { close(onlyOneSignalHandler) // panics when called twice stop := make(chan struct{}) c := make(chan os.Signal, 2) signal.Notify(c, shutdownSignals...) go func() { <-c close(stop) <-c os.Exit(1) // second signal. Exit directly. }() return stop } ================================================ FILE: modelzetes/pkg/signals/signal_posix.go ================================================ //go:build !windows // +build !windows package signals import ( "os" "syscall" ) var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} ================================================ FILE: modelzetes/pkg/signals/signal_windows.go ================================================ package signals import ( "os" ) var shutdownSignals = []os.Signal{os.Interrupt} ================================================ FILE: modelzetes/pkg/version/version.go ================================================ /* Copyright The TensorChord Inc. Copyright The BuildKit Authors. Copyright The containerd 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 version import ( "fmt" "regexp" "runtime" "strings" "sync" ) var ( // Package is filled at linking time Package = "github.com/tensorchord/openmodelz/modelzetes" // Revision is filled with the VCS (e.g. git) revision being used to build // the program at linking time. Revision = "" version = "0.0.0+unknown" buildDate = "1970-01-01T00:00:00Z" // output from `date -u +'%Y-%m-%dT%H:%M:%SZ'` gitCommit = "" // output from `git rev-parse HEAD` gitTag = "" // output from `git describe --exact-match --tags HEAD` (if clean tree state) gitTreeState = "" // determined from `git status --porcelain`. either 'clean' or 'dirty' developmentFlag = "false" ) // Version contains envd version information type Version struct { Version string BuildDate string GitCommit string GitTag string GitTreeState string GoVersion string Compiler string Platform string } func (v Version) String() string { return v.Version } // SetGitTagForE2ETest sets the gitTag for test purpose. func SetGitTagForE2ETest(tag string) { gitTag = tag } // GetEnvdVersion gets Envd version information func GetEnvdVersion() string { var versionStr string if gitCommit != "" && gitTag != "" && gitTreeState == "clean" && developmentFlag == "false" { // if we have a clean tree state and the current commit is tagged, // this is an official release. versionStr = gitTag } else { // otherwise formulate a version string based on as much metadata // information we have available. if strings.HasPrefix(version, "v") { versionStr = version } else { versionStr = "v" + version } if len(gitCommit) >= 7 { versionStr += "+" + gitCommit[0:7] if gitTreeState != "clean" { versionStr += ".dirty" } } else { versionStr += "+unknown" } } return versionStr } // GetVersion returns the version information func GetVersion() Version { return Version{ Version: GetEnvdVersion(), BuildDate: buildDate, GitCommit: gitCommit, GitTag: gitTag, GitTreeState: gitTreeState, GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } } var ( reRelease *regexp.Regexp reDev *regexp.Regexp reOnce sync.Once ) func UserAgent() string { version := GetVersion().String() reOnce.Do(func() { reRelease = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+$`) reDev = regexp.MustCompile(`^(v[0-9]+\.[0-9]+)\.[0-9]+`) }) if matches := reRelease.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] } else if matches := reDev.FindAllStringSubmatch(version, 1); len(matches) > 0 { version = matches[0][1] + "-dev" } return "envd/" + version } ================================================ FILE: modelzetes/vendor.go ================================================ //go:build vendor package main // This file exists to trick "go mod vendor" to include "main" packages. // It is not expected to build, the build tag above is only to prevent this // file from being included in builds. import ( _ "k8s.io/code-generator/cmd/client-gen" _ "k8s.io/code-generator/cmd/deepcopy-gen" _ "k8s.io/code-generator/cmd/defaulter-gen" _ "k8s.io/code-generator/cmd/informer-gen" _ "k8s.io/code-generator/cmd/lister-gen" _ "k8s.io/code-generator/cmd/openapi-gen" ) func main() {} ================================================ FILE: pyproject.toml ================================================ [project] name = "openmodelz" description = "Simplify machine learning deployment for any environments." readme = "README.md" authors = [ {name = "TensorChord", email = "modelz@tensorchord.ai"}, ] license = {text = "Apache-2.0"} keywords = ["machine learning", "deep learning", "model serving"] dynamic = ["version"] requires-python = ">=2.7" classifiers = [ "Environment :: GPU", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Build Tools", ] [project.urls] homepage = "https://modelz.ai/" documentation = "https://docs.open.modelz.ai/" repository = "https://github.com/tensorchord/openmodelz" changelog = "https://github.com/tensorchord/openmodelz/releases" [tool.cibuildwheel] build-frontend = "build" archs = ["auto64"] skip = "pp*" # skip pypy before-all = "" environment = { PIP_NO_CLEAN="yes" } before-build = "ls -la mdz/bin" # help to debug [project.optional-dependencies] [project.scripts] [build-system] requires = ["setuptools>=45", "wheel", "setuptools_scm"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "mdz/_version.py" ================================================ FILE: setup.py ================================================ import os import subprocess import shlex from wheel.bdist_wheel import bdist_wheel from setuptools import setup, find_packages, Extension from setuptools.command.build_ext import build_ext from setuptools_scm import get_version with open("README.md", "r", encoding="utf-8") as f: readme = f.read() class bdist_wheel_universal(bdist_wheel): def get_tag(self): *_, plat = super().get_tag() return "py2.py3", "none", plat def build_if_not_exist(): if os.path.isfile("mdz/bin/mdz"): return version = get_version() print(f"build mdz from source ({version})") errno = subprocess.call(shlex.split( f"make build-release GIT_TAG={version}" ), cwd="mdz") assert errno == 0, f"mdz build failed with code {errno}" class ModelzExtension(Extension): """A custom extension to define the OpenModelz extension.""" class ModelzBuildExt(build_ext): def build_extension(self, ext: Extension) -> None: if not isinstance(ext, ModelzExtension): return super().build_extension(ext) build_if_not_exist() setup( name="openmodelz", use_scm_version=True, description="Simplify machine learning deployment for any environments.", long_description=readme, long_description_content_type="text/markdown", url="https://github.com/tensorchord/openmodelz", license="Apache License 2.0", author="TensorChord", author_email="modelz@tensorchord.ai", packages=find_packages("mdz"), include_package_data=True, data_files=[("bin", ["mdz/bin/mdz"])], zip_safe=False, ext_modules=[ ModelzExtension(name="mdz", sources=["mdz/*"]), ], cmdclass=dict( build_ext=ModelzBuildExt, bdist_wheel=bdist_wheel_universal, ), ) ================================================ FILE: typos.toml ================================================ [files] extend-exclude = ["CHANGELOG.md", "go.mod", "go.sum"] [default.extend-words] requestor = "requestor" ba = "ba"