diff --git a/.github/workflows/dockers-benchmark-job-image.yml b/.github/workflows/dockers-benchmark-job-image.yml new file mode 100644 index 0000000000..3524cb7b0c --- /dev/null +++ b/.github/workflows/dockers-benchmark-job-image.yml @@ -0,0 +1,82 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +name: "Build docker image: benchmark-job" +on: + push: + branches: + - main + tags: + - "*.*.*" + - "v*.*.*" + - "*.*.*-*" + - "v*.*.*-*" + paths: + - ".github/actions/docker-build/actions.yaml" + - ".github/workflows/_docker-image.yaml" + - ".github/workflows/dockers-benchmak-job-image.yml" + - "go.mod" + - "go.sum" + - "internal/**" + - "!internal/**/*_test.go" + - "!internal/db/**" + - "apis/grpc/**" + - "pkg/benchmark/operator/**" + - "cmd/benchmark/operator/**" + - "pkg/benchmark/job/**" + - "cmd/benchmark/job/**" + - "dockers/tools/benchmark/job/Dockerfile" + - "versions/GO_VERSION" + pull_request: + paths: + - ".github/actions/docker-build/actions.yaml" + - ".github/workflows/_docker-image.yaml" + - ".github/workflows/dockers-benchmak-job-image.yml" + - "go.mod" + - "go.sum" + - "internal/**" + - "!internal/**/*_test.go" + - "!internal/db/**" + - "apis/grpc/**" + - "pkg/benchmark/operator/**" + - "cmd/benchmark/operator/**" + - "pkg/benchmark/job/**" + - "cmd/benchmark/job/**" + - "dockers/tools/benchmark/job/Dockerfile" + - "versions/GO_VERSION" + pull_request_target: + paths: + - ".github/actions/docker-build/actions.yaml" + - ".github/workflows/_docker-image.yaml" + - ".github/workflows/dockers-benchmak-job-image.yml" + - "go.mod" + - "go.sum" + - "internal/**" + - "!internal/**/*_test.go" + - "!internal/db/**" + - "apis/grpc/**" + - "pkg/benchmark/operator/**" + - "cmd/benchmark/operator/**" + - "pkg/benchmark/job/**" + - "cmd/benchmark/job/**" + - "dockers/tools/benchmark/job/Dockerfile" + - "versions/GO_VERSION" + +jobs: + build: + uses: ./.github/workflows/_docker-image.yaml + with: + target: benchmark-job + secrets: inherit diff --git a/.github/workflows/dockers-benchmark-operator-image.yaml b/.github/workflows/dockers-benchmark-operator-image.yaml new file mode 100644 index 0000000000..71aeec8673 --- /dev/null +++ b/.github/workflows/dockers-benchmark-operator-image.yaml @@ -0,0 +1,82 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +name: "Build docker image: benchmark-operator" +on: + push: + branches: + - main + tags: + - "*.*.*" + - "v*.*.*" + - "*.*.*-*" + - "v*.*.*-*" + paths: + - ".github/actions/docker-build/actions.yaml" + - ".github/workflows/_docker-image.yaml" + - ".github/workflows/dockers-benchmak-operator-image.yml" + - "go.mod" + - "go.sum" + - "internal/**" + - "!internal/**/*_test.go" + - "!internal/db/**" + - "apis/grpc/**" + - "pkg/benchmark/operator/**" + - "cmd/benchmark/operator/**" + - "pkg/benchmark/job/**" + - "cmd/benchmark/job/**" + - "dockers/tools/benchmark/operator/Dockerfile" + - "versions/GO_VERSION" + pull_request: + paths: + - ".github/actions/docker-build/actions.yaml" + - ".github/workflows/_docker-image.yaml" + - ".github/workflows/dockers-benchmak-operator-image.yml" + - "go.mod" + - "go.sum" + - "internal/**" + - "!internal/**/*_test.go" + - "!internal/db/**" + - "apis/grpc/**" + - "pkg/benchmark/operator/**" + - "cmd/benchmark/operator/**" + - "pkg/benchmark/job/**" + - "cmd/benchmark/job/**" + - "dockers/tools/benchmark/operator/Dockerfile" + - "versions/GO_VERSION" + pull_request_target: + paths: + - ".github/actions/docker-build/actions.yaml" + - ".github/workflows/_docker-image.yaml" + - ".github/workflows/dockers-benchmak-operator-image.yml" + - "go.mod" + - "go.sum" + - "internal/**" + - "!internal/**/*_test.go" + - "!internal/db/**" + - "apis/grpc/**" + - "pkg/benchmark/operator/**" + - "cmd/benchmark/operator/**" + - "pkg/benchmark/job/**" + - "cmd/benchmark/job/**" + - "dockers/tools/benchmark/operator/Dockerfile" + - "versions/GO_VERSION" + +jobs: + build: + uses: ./.github/workflows/_docker-image.yaml + with: + target: benchmark-operator + secrets: inherit diff --git a/Makefile b/Makefile index efb61aec8b..9c8a8d998a 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,8 @@ INDEX_CREATION_IMAGE = $(NAME)-index-creation INDEX_SAVE_IMAGE = $(NAME)-index-save READREPLICA_ROTATE_IMAGE = $(NAME)-readreplica-rotate MANAGER_INDEX_IMAGE = $(NAME)-manager-index +BENCHMARK_JOB_IMAGE = $(NAME)-benchmark-job +BENCHMARK_OPERATOR_IMAGE = $(NAME)-benchmark-operator MAINTAINER = "$(ORG).org $(NAME) team <$(NAME)@$(ORG).org>" VERSION ?= $(eval VERSION := $(shell cat versions/VALD_VERSION))$(VERSION) @@ -72,6 +74,8 @@ TELEPRESENCE_VERSION := $(eval TELEPRESENCE_VERSION := $(shell cat versions VALDCLI_VERSION := $(eval VALDCLI_VERSION := $(shell cat versions/VALDCLI_VERSION))$(VALDCLI_VERSION) YQ_VERSION := $(eval YQ_VERSION := $(shell cat versions/YQ_VERSION))$(YQ_VERSION) BUF_VERSION := $(eval BUF_VERSION := $(shell cat versions/BUF_VERSION))$(BUF_VERSION) +ZLIB_VERSION := $(eval ZLIB_VERSION := $(shell cat versions/ZLIB_VERSION))$(ZLIB_VERSION) +HDF5_VERSION := $(eval HDF5_VERSION := $(shell cat versions/HDF5_VERSION))$(HDF5_VERSION) OTEL_OPERATOR_RELEASE_NAME ?= opentelemetry-operator PROMETHEUS_RELEASE_NAME ?= prometheus diff --git a/Makefile.d/build.mk b/Makefile.d/build.mk index 4e947097ae..4cbd8e842b 100644 --- a/Makefile.d/build.mk +++ b/Makefile.d/build.mk @@ -22,7 +22,9 @@ binary/build: \ cmd/discoverer/k8s/discoverer \ cmd/gateway/lb/lb \ cmd/gateway/filter/filter \ - cmd/manager/index/index + cmd/manager/index/index \ + cmd/tools/benchmark/job/job \ + cmd/tools/benchmark/operator/operator cmd/agent/core/ngt/ngt: \ ngt/install \ @@ -322,6 +324,64 @@ cmd/index/job/readreplica/rotate/readreplica-rotate: \ $(dir $@)main.go $@ -version +cmd/tools/benchmark/job/job: \ + $(GO_SOURCES_INTERNAL) \ + $(PBGOS) \ + $(shell find ./cmd/tools/benchmark/job -type f -name '*.go' -not -name '*_test.go' -not -name 'doc.go') \ + $(shell find ./pkg/tools/benchmark/job -type f -name '*.go' -not -name '*_test.go' -not -name 'doc.go') + CGO_ENABLED=$(CGO_ENABLED) \ + GO111MODULE=on \ + GOPRIVATE=$(GOPRIVATE) \ + go build \ + --ldflags "-w -linkmode 'external' \ + -extldflags '-static -fPIC -pthread -fopenmp -std=gnu++20 -lhdf5 -lhdf5_hl -lm -ldl' \ + -X '$(GOPKG)/internal/info.Version=$(VERSION)' \ + -X '$(GOPKG)/internal/info.GitCommit=$(GIT_COMMIT)' \ + -X '$(GOPKG)/internal/info.BuildTime=$(DATETIME)' \ + -X '$(GOPKG)/internal/info.GoVersion=$(GO_VERSION)' \ + -X '$(GOPKG)/internal/info.GoOS=$(GOOS)' \ + -X '$(GOPKG)/internal/info.GoArch=$(GOARCH)' \ + -X '$(GOPKG)/internal/info.CGOEnabled=${CGO_ENABLED}' \ + -X '$(GOPKG)/internal/info.NGTVersion=$(NGT_VERSION)' \ + -X '$(GOPKG)/internal/info.BuildCPUInfoFlags=$(CPU_INFO_FLAGS)' \ + -buildid=" \ + -mod=readonly \ + -modcacherw \ + -a \ + -tags "cgo osusergo netgo static_build" \ + -trimpath \ + -o $@ \ + $(dir $@)main.go + $@ -version + +cmd/tools/benchmark/operator/operator: \ + $(GO_SOURCES_INTERNAL) \ + $(PBGOS) \ + $(shell find ./cmd/tools/benchmark/operator -type f -name '*.go' -not -name '*_test.go' -not -name 'doc.go') \ + $(shell find ./pkg/tools/benchmark/operator -type f -name '*.go' -not -name '*_test.go' -not -name 'doc.go') + CFLAGS="$(CFLAGS)" \ + CXXFLAGS="$(CXXFLAGS)" \ + CGO_ENABLED=1 \ + CGO_CXXFLAGS="-g -Ofast -march=native" \ + CGO_FFLAGS="-g -Ofast -march=native" \ + CGO_LDFLAGS="-g -Ofast -march=native" \ + GO111MODULE=on \ + GOPRIVATE=$(GOPRIVATE) \ + go build \ + --ldflags "-w -extldflags=-static \ + -X '$(GOPKG)/internal/info.CGOEnabled=${CGO_ENABLED}' \ + -X '$(GOPKG)/internal/info.NGTVersion=$(NGT_VERSION)' \ + -X '$(GOPKG)/internal/info.BuildCPUInfoFlags=$(CPU_INFO_FLAGS)' \ + -buildid=" \ + -mod=readonly \ + -modcacherw \ + -a \ + -tags "cgo osusergo netgo static_build" \ + -trimpath \ + -o $@ \ + $(dir $@)main.go + $@ -version + .PHONY: binary/build/zip ## build all binaries and zip them binary/build/zip: \ @@ -356,3 +416,10 @@ artifacts/vald-manager-index-$(GOOS)-$(GOARCH).zip: cmd/manager/index/index $(call mkdir, $(dir $@)) zip --junk-paths $@ $< +artifacts/vald-benchmark-job-$(GOOS)-$(GOARCH).zip: cmd/tools/benchmark/job/job + $(call mkdir, $(dir $@)) + zip --junk-paths $@ $< + +artifacts/vald-benchmark-operator-$(GOOS)-$(GOARCH).zip: cmd/tools/benchmark/operator/operator + $(call mkdir, $(dir $@)) + zip --junk-paths $@ $< diff --git a/Makefile.d/dependencies.mk b/Makefile.d/dependencies.mk index 049bc4dc67..f423031547 100644 --- a/Makefile.d/dependencies.mk +++ b/Makefile.d/dependencies.mk @@ -35,7 +35,9 @@ update/libs: \ update/telepresence \ update/vald \ update/valdcli \ - update/yq + update/yq \ + update/zlib \ + update/hdf5 .PHONY: go/download ## download Go package dependencies @@ -163,6 +165,16 @@ update/telepresence: update/yq: curl --silent https://api.github.com/repos/mikefarah/yq/releases/latest | grep -Po '"tag_name": "\K.*?(?=")' > $(ROOTDIR)/versions/YQ_VERSION +.PHONY: update/zlib +## update zlib version +update/zlib: + curl --silent https://api.github.com/repos/madler/zlib/releases/latest | grep -Po '"tag_name": "\K.*?(?=")' | sed 's/v//g' > $(ROOTDIR)/versions/ZLIB_VERSION + +.PHONY: update/hdf5 +## update hdf5 version +update/hdf5: + curl --silent https://api.github.com/repos/HDFGroup/hdf5/releases/latest | grep -Po '"tag_name": "\K.*?(?=")' | sed 's/v//g' > $(ROOTDIR)/versions/HDF5_VERSION + .PHONY: update/vald ## update vald it's self version update/vald: diff --git a/Makefile.d/docker.mk b/Makefile.d/docker.mk index c061f610d8..cc45bfbd6b 100644 --- a/Makefile.d/docker.mk +++ b/Makefile.d/docker.mk @@ -22,6 +22,8 @@ docker/build: \ docker/build/gateway-lb \ docker/build/gateway-filter \ docker/build/manager-index \ + docker/build/benchmark-job \ + docker/build/benchmark-operator \ docker/build/operator/helm .PHONY: docker/name/org @@ -226,3 +228,26 @@ docker/build/readreplica-rotate: @make DOCKERFILE="$(ROOTDIR)/dockers/index/job/readreplica/rotate/Dockerfile" \ IMAGE=$(READREPLICA_ROTATE_IMAGE) \ docker/build/image + +.PHONY: docker/name/benchmark-job +docker/name/benchmark-job: + @echo "$(ORG)/$(BENCHMARK_JOB_IMAGE)" + +.PHONY: docker/build/benchmark-job +## build benchmark job +docker/build/benchmark-job: + @make DOCKERFILE="$(ROOTDIR)/dockers/tools/benchmark/job/Dockerfile" \ + IMAGE=$(BENCHMARK_JOB_IMAGE) \ + DOCKER_OPTS="--build-arg ZLIB_VERSION=$(ZLIB_VERSION) --build-arg HDF5_VERSION=$(HDF5_VERSION)" \ + docker/build/image + +.PHONY: docker/name/benchmark-operator +docker/name/benchmark-operator: + @echo "$(ORG)/$(BENCHMARK_OPERATOR_IMAGE)" + +.PHONY: docker/build/benchmark-operator +## build benchmark operator +docker/build/benchmark-operator: + @make DOCKERFILE="$(ROOTDIR)/dockers/tools/benchmark/operator/Dockerfile" \ + IMAGE=$(BENCHMARK_OPERATOR_IMAGE) \ + docker/build/image diff --git a/Makefile.d/functions.mk b/Makefile.d/functions.mk index b9c2d74b2c..2306dc04be 100644 --- a/Makefile.d/functions.mk +++ b/Makefile.d/functions.mk @@ -206,3 +206,12 @@ define gen-go-option-test-sources fi; \ done endef + +define gen-vald-crd + mv charts/$1/crds/$2.yaml $(TEMP_DIR)/$2.yaml + GOPRIVATE=$(GOPRIVATE) \ + go run -mod=readonly hack/helm/schema/crd/main.go \ + charts/$1/$3.yaml > $(TEMP_DIR)/$2-spec.yaml + $(BINDIR)/yq eval-all 'select(fileIndex==0).spec.versions[0].schema.openAPIV3Schema.properties.spec = select(fileIndex==1).spec | select(fileIndex==0)' \ + $(TEMP_DIR)/$2.yaml $(TEMP_DIR)/$2-spec.yaml > charts/$1/crds/$2.yaml +endef diff --git a/Makefile.d/helm.mk b/Makefile.d/helm.mk index 81d858d2de..4f781debb9 100644 --- a/Makefile.d/helm.mk +++ b/Makefile.d/helm.mk @@ -100,6 +100,37 @@ charts/vald-helm-operator/values.schema.json: \ GOPRIVATE=$(GOPRIVATE) \ go run -mod=readonly hack/helm/schema/gen/main.go charts/vald-helm-operator/values.yaml > charts/vald-helm-operator/values.schema.json +.PHONY: helm/schema/vald-benchmark-job +## generate json schema for Vald Benchmark Job Chart +helm/schema/vald-benchmark-job: charts/vald-benchmark-operator/job-values.schema.json + +charts/vald-benchmark-operator/job-values.schema.json: \ + charts/vald-benchmark-operator/schemas/job-values.yaml \ + hack/helm/schema/gen/main.go + GOPRIVATE=$(GOPRIVATE) \ + go run -mod=readonly hack/helm/schema/gen/main.go charts/vald-benchmark-operator/schemas/job-values.yaml > charts/vald-benchmark-operator/job-values.schema.json + +.PHONY: helm/schema/vald-benchmark-job +## generate json schema for Vald Benchmark Job Chart +helm/schema/vald-benchmark-scenario: charts/vald-benchmark-operator/scenario-values.schema.json + +charts/vald-benchmark-operator/scenario-values.schema.json: \ + charts/vald-benchmark-operator/schemas/scenario-values.yaml \ + hack/helm/schema/gen/main.go + GOPRIVATE=$(GOPRIVATE) \ + go run -mod=readonly hack/helm/schema/gen/main.go charts/vald-benchmark-operator/schemas/scenario-values.yaml > charts/vald-benchmark-operator/scenario-values.schema.json + +.PHONY: helm/schema/vald-benchmark-operator +## generate json schema for Vald Benchmark Operator Chart +helm/schema/vald-benchmark-operator: charts/vald-benchmark-operator/values.schema.json + +charts/vald-benchmark-operator/values.schema.json: \ + charts/vald-benchmark-operator/values.yaml \ + hack/helm/schema/gen/main.go + GOPRIVATE=$(GOPRIVATE) \ + go run -mod=readonly hack/helm/schema/gen/main.go charts/vald-benchmark-operator/values.yaml > charts/vald-benchmark-operator/values.schema.json + + .PHONY: yq/install ## install yq yq/install: $(BINDIR)/yq @@ -131,3 +162,21 @@ helm/schema/crd/vald-helm-operator: \ charts/vald-helm-operator/values.yaml > $(TEMP_DIR)/valdhelmoperatorrelease-spec.yaml $(BINDIR)/yq eval-all 'select(fileIndex==0).spec.versions[0].schema.openAPIV3Schema.properties.spec = select(fileIndex==1).spec | select(fileIndex==0)' \ $(TEMP_DIR)/valdhelmoperatorrelease.yaml $(TEMP_DIR)/valdhelmoperatorrelease-spec.yaml > charts/vald-helm-operator/crds/valdhelmoperatorrelease.yaml + +.PHONY: helm/schema/crd/vald-benchmark-job +## generate OpenAPI v3 schema for ValdBenchmarkJobRelease +helm/schema/crd/vald-benchmark-job: \ + yq/install + @$(call gen-vald-crd,vald-benchmark-operator,valdbenchmarkjob,schemas/job-values) + +.PHONY: helm/schema/crd/vald-benchmark-scenario +## generate OpenAPI v3 schema for ValdBenchmarkScenarioRelease +helm/schema/crd/vald-benchmark-scenario: \ + yq/install + @$(call gen-vald-crd,vald-benchmark-operator,valdbenchmarkscenario,schemas/scenario-values) + +.PHONY: helm/schema/crd/vald-benchmark-operator +## generate OpenAPI v3 schema for ValdBenchmarkOperatorRelease +helm/schema/crd/vald-benchmark-operator: \ + yq/install + @$(call gen-vald-crd,vald-benchmark-operator,valdbenchmarkoperatorrelease,values) diff --git a/Makefile.d/k8s.mk b/Makefile.d/k8s.mk index de907c172c..2a1b419c2d 100644 --- a/Makefile.d/k8s.mk +++ b/Makefile.d/k8s.mk @@ -67,6 +67,23 @@ k8s/manifest/helm-operator/update: \ rm -rf $(TEMP_DIR) cp -r charts/vald-helm-operator/crds k8s/operator/helm/crds +.PHONY: k8s/manifest/benchmark-operator/clean +## clean k8s manifests for benchmark-operator +k8s/manifest/benchmark-operator/clean: + rm -rf \ + k8s/tools/benchmark/operator + +.PHONY: k8s/manifest/benchmark-operator/update +## update k8s manifests for benchmark-operator using helm templates +k8s/manifest/benchmark-operator/update: \ + k8s/manifest/benchmark-operator/clean + helm template \ + --output-dir $(TEMP_DIR) \ + charts/vald-benchmark-operator + mkdir -p k8s/tools/benchmark + mv $(TEMP_DIR)/vald-benchmark-operator/templates k8s/tools/benchmark/operator + rm -rf $(TEMP_DIR) + cp -r charts/vald-benchmark-operator/crds k8s/tools/benchmark/operator/crds .PHONY: k8s/vald/deploy ## deploy vald sample cluster to k8s @@ -208,6 +225,34 @@ k8s/vr/delete: \ k8s/metrics/metrics-server/delete kubectl delete vr vald-cluster +.PHONY: k8s/vald-benchmark-operator/deploy +## deploy vald-benchmark-operator to k8s +k8s/vald-benchmark-operator/deploy: + helm template \ + --output-dir $(TEMP_DIR) \ + --set image.tag=${VERSION} \ + --include-crds \ + charts/vald-benchmark-operator + kubectl create -f $(TEMP_DIR)/vald-benchmark-operator/crds/valdbenchmarkjob.yaml + kubectl create -f $(TEMP_DIR)/vald-benchmark-operator/crds/valdbenchmarkscenario.yaml + kubectl create -f $(TEMP_DIR)/vald-benchmark-operator/crds/valdbenchmarkoperatorrelease.yaml + kubectl apply -f $(TEMP_DIR)/vald-benchmark-operator/templates + sleep 2 + kubectl wait --for=condition=ready pod -l name=vald-benchmark-operator --timeout=600s + +.PHONY: k8s/vald-benchmark-operator/delete +## delete vald-benchmark-operator from k8s +k8s/vald-benchmark-operator/delete: + helm template \ + --output-dir $(TEMP_DIR) \ + --set image.tag=${VERSION} \ + --include-crds \ + charts/vald-benchmark-operator + kubectl delete -f $(TEMP_DIR)/vald-benchmark-operator/templates + kubectl wait --for=delete pod -l name=vald-benchmark-operator --timeout=600s + kubectl delete -f $(TEMP_DIR)/vald-benchmark-operator/crds + rm -rf $(TEMP_DIR) + .PHONY: k8s/external/cert-manager/deploy ## deploy cert-manager k8s/external/cert-manager/deploy: diff --git a/charts/vald-benchmark-operator/Chart.yaml b/charts/vald-benchmark-operator/Chart.yaml new file mode 100644 index 0000000000..aea5e8c563 --- /dev/null +++ b/charts/vald-benchmark-operator/Chart.yaml @@ -0,0 +1,62 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +appVersion: "1.16.0" +# +# Copyright (C) 2019-2023 vdaas.org vald team +# +# 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 +# +# https://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. +# + +apiVersion: v2 +name: vald-benchmark-operator +version: v1.7.5 +description: A benchmark operator for benchmarking the Vald cluster. +type: application +keywords: + - Vald + - NGT + - vector + - search + - approximate-nearest-neighbor-search + - nearest-neighbor-search + - vector-search-engine + - similarity-search + - image-search + - Kubernetes + - k8s + - AI + - artificial-intelligence +home: https://vald.vdaas.org +icon: https://raw.githubusercontent.com/vdaas/vald/main/assets/image/svg/symbol.svg +sources: + - https://github.com/vdaas/vald +maintainers: + - name: kpango + email: kpango@vdaas.org + - name: vankichi + email: vankichi@vdaas.org + - name: kmrmt + email: ksk@vdaas.org diff --git a/charts/vald-benchmark-operator/crds/valdbenchmarkjob.yaml b/charts/vald-benchmark-operator/crds/valdbenchmarkjob.yaml new file mode 100644 index 0000000000..53367d67c8 --- /dev/null +++ b/charts/vald-benchmark-operator/crds/valdbenchmarkjob.yaml @@ -0,0 +1,824 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: valdbenchmarkjobs.vald.vdaas.org +spec: + group: vald.vdaas.org + names: + kind: ValdBenchmarkJob + listKind: ValdBenchmarkJobList + plural: valdbenchmarkjobs + singular: valdbenchmarkjob + shortNames: + - vbj + - vbjs + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .spec.replica + name: REPLICAS + type: integer + - jsonPath: .status + name: STATUS + type: string + schema: + openAPIV3Schema: + description: ValdBenchmarkJob is the Schema for the valdbenchmarkjobs API + type: object + 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 + status: + description: ValdBenchmarkJobStatus defines the observed state of ValdBenchmarkJob + enum: + - NotReady + - Completed + - Available + - Healthy + type: string + spec: + type: object + properties: + client_config: + type: object + properties: + addrs: + type: array + items: + type: string + backoff: + type: object + properties: + backoff_factor: + type: number + backoff_time_limit: + type: string + enable_error_log: + type: boolean + initial_duration: + type: string + jitter_limit: + type: string + maximum_duration: + type: string + retry_count: + type: integer + call_option: + type: object + x-kubernetes-preserve-unknown-fields: true + circuit_breaker: + type: object + properties: + closed_error_rate: + type: number + closed_refresh_timeout: + type: string + half_open_error_rate: + type: number + min_samples: + type: integer + open_timeout: + type: string + connection_pool: + type: object + properties: + enable_dns_resolver: + type: boolean + enable_rebalance: + type: boolean + old_conn_close_duration: + type: string + rebalance_duration: + type: string + size: + type: integer + dial_option: + type: object + properties: + backoff_base_delay: + type: string + backoff_jitter: + type: number + backoff_max_delay: + type: string + backoff_multiplier: + type: number + enable_backoff: + type: boolean + initial_connection_window_size: + type: integer + initial_window_size: + type: integer + insecure: + type: boolean + interceptors: + type: array + items: + type: string + enum: + - TraceInterceptor + keepalive: + type: object + properties: + permit_without_stream: + type: boolean + time: + type: string + timeout: + type: string + max_msg_size: + type: integer + min_connection_timeout: + type: string + net: + type: object + properties: + dialer: + type: object + properties: + dual_stack_enabled: + type: boolean + keepalive: + type: string + timeout: + type: string + dns: + type: object + properties: + cache_enabled: + type: boolean + cache_expiration: + type: string + refresh_duration: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + tls: + type: object + properties: + ca: + type: string + cert: + type: string + enabled: + type: boolean + insecure_skip_verify: + type: boolean + key: + type: string + read_buffer_size: + type: integer + timeout: + type: string + write_buffer_size: + type: integer + health_check_duration: + type: string + max_recv_msg_size: + type: integer + max_retry_rpc_buffer_size: + type: integer + max_send_msg_size: + type: integer + tls: + type: object + properties: + ca: + type: string + cert: + type: string + enabled: + type: boolean + insecure_skip_verify: + type: boolean + key: + type: string + wait_for_ready: + type: boolean + concurrency_limit: + type: integer + maximum: 65535 + minimum: 0 + dataset: + type: object + properties: + group: + type: string + minLength: 1 + indexes: + type: integer + minimum: 0 + name: + type: string + enum: + - original + - fashion-mnist + range: + type: object + properties: + end: + type: integer + minimum: 1 + start: + type: integer + minimum: 1 + required: + - start + - end + url: + type: string + required: + - name + - indexes + - group + - range + dimension: + type: integer + minimum: 1 + global_config: + type: object + properties: + logging: + type: object + properties: + format: + type: string + enum: + - raw + - json + level: + type: string + enum: + - debug + - info + - warn + - error + - fatal + logger: + type: string + enum: + - glg + - zap + time_zone: + type: string + version: + type: string + insert_config: + type: object + properties: + skip_strict_exist_check: + type: boolean + timestamp: + type: string + job_type: + type: string + enum: + - insert + - update + - upsert + - search + - remove + - getobject + - exists + object_config: + type: object + properties: + filter_config: + type: object + properties: + host: + type: string + remove_config: + type: object + properties: + skip_strict_exist_check: + type: boolean + timestamp: + type: string + repetition: + type: integer + minimum: 1 + replica: + type: integer + minimum: 1 + rps: + type: integer + maximum: 65535 + minimum: 0 + rules: + type: array + items: + type: string + search_config: + type: object + properties: + aggregation_algorithm: + type: string + enum: + - Unknown + - ConcurrentQueue + - SortSlice + - SortPoolSlice + - PairingHeap + enable_linear_search: + type: boolean + epsilon: + type: number + min_num: + type: integer + num: + type: integer + radius: + type: number + timeout: + type: string + server_config: + type: object + properties: + healths: + type: object + properties: + liveness: + type: object + properties: + enabled: + type: boolean + host: + type: string + livenessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + port: + type: integer + maximum: 65535 + minimum: 0 + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + readiness: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + maximum: 65535 + minimum: 0 + readinessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + startup: + type: object + properties: + enabled: + type: boolean + port: + type: integer + maximum: 65535 + minimum: 0 + startupProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + servers: + type: object + properties: + grpc: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + maximum: 65535 + minimum: 0 + server: + type: object + properties: + grpc: + type: object + properties: + bidirectional_stream_concurrency: + type: integer + connection_timeout: + type: string + enable_reflection: + type: boolean + header_table_size: + type: integer + initial_conn_window_size: + type: integer + initial_window_size: + type: integer + interceptors: + type: array + items: + type: string + enum: + - RecoverInterceptor + - AccessLogInterceptor + - TraceInterceptor + - MetricInterceptor + keepalive: + type: object + properties: + max_conn_age: + type: string + max_conn_age_grace: + type: string + max_conn_idle: + type: string + min_time: + type: string + permit_without_stream: + type: boolean + time: + type: string + timeout: + type: string + max_header_list_size: + type: integer + max_receive_message_size: + type: integer + max_send_message_size: + type: integer + read_buffer_size: + type: integer + write_buffer_size: + type: integer + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + restart: + type: boolean + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + rest: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + maximum: 65535 + minimum: 0 + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + target: + type: object + properties: + host: + type: string + minLength: 1 + port: + type: integer + maximum: 65535 + minimum: 0 + required: + - host + - port + ttl_seconds_after_finished: + type: integer + maximum: 65535 + minimum: 0 + update_config: + type: object + properties: + disable_balance_update: + type: boolean + skip_strict_exist_check: + type: boolean + timestamp: + type: string + upsert_config: + type: object + properties: + disable_balance_update: + type: boolean + skip_strict_exist_check: + type: boolean + timestamp: + type: string diff --git a/charts/vald-benchmark-operator/crds/valdbenchmarkoperatorrelease.yaml b/charts/vald-benchmark-operator/crds/valdbenchmarkoperatorrelease.yaml new file mode 100644 index 0000000000..8886ca72ea --- /dev/null +++ b/charts/vald-benchmark-operator/crds/valdbenchmarkoperatorrelease.yaml @@ -0,0 +1,523 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: valdbenchmarkoperatorreleases.vald.vdaas.org +spec: + group: vald.vdaas.org + names: + kind: ValdBenchmarkOperatorRelease + listKind: ValdBenchmarkOperatorReleaseList + plural: valdbenchmarkoperatorreleases + singular: valdbenchmarkoperatorrelease + shortNames: + - vbor + - vbors + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .status + name: STATUS + type: string + schema: + openAPIV3Schema: + description: ValdBenchmarkScenario is the Schema for the valdbenchmarkscenarios API + type: object + 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 + status: + description: ValdBenchmarkScenarioStatus defines the observed state of ValdBenchmarkScenario + enum: + - NotReady + - Completed + - Available + - Healthy + type: string + spec: + type: object + properties: + affinity: + type: object + x-kubernetes-preserve-unknown-fields: true + annotations: + type: object + x-kubernetes-preserve-unknown-fields: true + image: + type: object + properties: + pullPolicy: + type: string + enum: + - Always + - Never + - IfNotPresent + repository: + type: string + tag: + type: string + job_image: + type: object + properties: + pullPolicy: + type: string + enum: + - Always + - Never + - IfNotPresent + repository: + type: string + tag: + type: string + logging: + type: object + properties: + format: + type: string + enum: + - raw + - json + level: + type: string + enum: + - debug + - info + - warn + - error + - fatal + logger: + type: string + enum: + - glg + - zap + name: + type: string + nodeSelector: + type: object + x-kubernetes-preserve-unknown-fields: true + observability: + type: object + properties: + enabled: + type: boolean + otlp: + type: object + properties: + attribute: + type: object + properties: + metrics: + type: object + properties: + enable_cgo: + type: boolean + enable_goroutine: + type: boolean + enable_memory: + type: boolean + enable_version_info: + type: boolean + version_info_labels: + type: array + items: + type: string + namespace: + type: string + node_name: + type: string + pod_name: + type: string + service_name: + type: string + collector_endpoint: + type: string + metrics_export_interval: + type: string + metrics_export_timeout: + type: string + trace_batch_timeout: + type: string + trace_export_timeout: + type: string + trace_max_export_batch_size: + type: integer + trace_max_queue_size: + type: integer + trace: + type: object + properties: + enabled: + type: boolean + sampling_rate: + type: integer + podAnnotations: + type: object + x-kubernetes-preserve-unknown-fields: true + podSecurityContext: + type: object + x-kubernetes-preserve-unknown-fields: true + rbac: + type: object + properties: + create: + type: boolean + name: + type: string + replicas: + type: integer + resources: + type: object + properties: + limits: + type: object + x-kubernetes-preserve-unknown-fields: true + requests: + type: object + x-kubernetes-preserve-unknown-fields: true + securityContext: + type: object + x-kubernetes-preserve-unknown-fields: true + server_config: + type: object + properties: + full_shutdown_duration: + type: string + healths: + type: object + properties: + liveness: + type: object + properties: + enabled: + type: boolean + host: + type: string + livenessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + port: + type: integer + server: + type: object + properties: + http: + type: object + properties: + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + timeout: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + probe_wait_time: + type: string + socket_path: + type: string + servicePort: + type: integer + readiness: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + readinessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + probe_wait_time: + type: string + socket_path: + type: string + servicePort: + type: integer + startup: + type: object + properties: + enabled: + type: boolean + startupProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + metrics: + type: object + properties: + pprof: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + probe_wait_time: + type: string + socket_path: + type: string + servers: + type: object + properties: + grpc: + type: object + properties: + enabled: + type: boolean + host: + type: string + name: + type: string + port: + type: integer + server: + type: object + properties: + grpc: + type: object + properties: + bidirectional_stream_concurrency: + type: integer + connection_timeout: + type: string + enable_reflection: + type: boolean + header_table_size: + type: integer + initial_conn_window_size: + type: integer + initial_window_size: + type: integer + interceptors: + type: array + items: + type: string + keepalive: + type: object + properties: + max_conn_age: + type: string + max_conn_age_grace: + type: string + max_conn_idle: + type: string + min_time: + type: string + permit_without_stream: + type: boolean + time: + type: string + timeout: + type: string + max_header_list_size: + type: integer + max_receive_message_size: + type: integer + max_send_msg_size: + type: integer + read_buffer_size: + type: integer + write_buffer_size: + type: integer + mode: + type: string + network: + type: string + probe_wait_time: + type: string + restart: + type: boolean + socket_path: + type: string + servicePort: + type: integer + rest: + type: object + properties: + enabled: + type: boolean + tls: + type: object + properties: + ca: + type: string + cert: + type: string + enabled: + type: boolean + insecure_skip_verify: + type: boolean + key: + type: string + service: + type: object + properties: + annotations: + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + type: boolean + externalTrafficPolicy: + type: string + labels: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + enum: + - ClusterIP + - LoadBalancer + - NodePort + serviceAccount: + type: object + properties: + create: + type: boolean + name: + type: string + time_zone: + type: string + tolerations: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + version: + type: string diff --git a/charts/vald-benchmark-operator/crds/valdbenchmarkscenario.yaml b/charts/vald-benchmark-operator/crds/valdbenchmarkscenario.yaml new file mode 100644 index 0000000000..3f7492d8b3 --- /dev/null +++ b/charts/vald-benchmark-operator/crds/valdbenchmarkscenario.yaml @@ -0,0 +1,115 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: valdbenchmarkscenarios.vald.vdaas.org +spec: + group: vald.vdaas.org + names: + kind: ValdBenchmarkScenario + listKind: ValdBenchmarkScenarioList + plural: valdbenchmarkscenarios + singular: valdbenchmarkscenario + shortNames: + - vbs + - vbss + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .status + name: STATUS + type: string + schema: + openAPIV3Schema: + description: ValdBenchmarkScenario is the Schema for the valdbenchmarkscenarios API + type: object + 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 + status: + description: ValdBenchmarkScenarioStatus defines the observed state of ValdBenchmarkScenario + enum: + - NotReady + - Completed + - Available + - Healthy + type: string + spec: + type: object + properties: + dataset: + type: object + properties: + group: + type: string + minLength: 1 + indexes: + type: integer + minimum: 0 + name: + type: string + enum: + - original + - fashion-mnist + range: + type: object + properties: + end: + type: integer + minimum: 1 + start: + type: integer + minimum: 1 + required: + - start + - end + url: + type: string + required: + - name + - indexes + - group + - range + jobs: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + target: + type: object + properties: + host: + type: string + minLength: 1 + port: + type: integer + maximum: 65535 + minimum: 0 + required: + - host + - port diff --git a/charts/vald-benchmark-operator/job-values.schema.json b/charts/vald-benchmark-operator/job-values.schema.json new file mode 100644 index 0000000000..582302dfdf --- /dev/null +++ b/charts/vald-benchmark-operator/job-values.schema.json @@ -0,0 +1,1247 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "Values", + "type": "object", + "properties": { + "client_config": { + "type": "object", + "description": "gRPC client config for request to the Vald cluster", + "properties": { + "addrs": { + "type": "array", + "description": "gRPC client addresses", + "items": { "type": "string" } + }, + "backoff": { + "type": "object", + "properties": { + "backoff_factor": { + "type": "number", + "description": "gRPC client backoff factor" + }, + "backoff_time_limit": { + "type": "string", + "description": "gRPC client backoff time limit" + }, + "enable_error_log": { + "type": "boolean", + "description": "gRPC client backoff log enabled" + }, + "initial_duration": { + "type": "string", + "description": "gRPC client backoff initial duration" + }, + "jitter_limit": { + "type": "string", + "description": "gRPC client backoff jitter limit" + }, + "maximum_duration": { + "type": "string", + "description": "gRPC client backoff maximum duration" + }, + "retry_count": { + "type": "integer", + "description": "gRPC client backoff retry count" + } + } + }, + "call_option": { "type": "object" }, + "circuit_breaker": { + "type": "object", + "properties": { + "closed_error_rate": { + "type": "number", + "description": "gRPC client circuitbreaker closed error rate" + }, + "closed_refresh_timeout": { + "type": "string", + "description": "gRPC client circuitbreaker closed refresh timeout" + }, + "half_open_error_rate": { + "type": "number", + "description": "gRPC client circuitbreaker half-open error rate" + }, + "min_samples": { + "type": "integer", + "description": "gRPC client circuitbreaker minimum sampling count" + }, + "open_timeout": { + "type": "string", + "description": "gRPC client circuitbreaker open timeout" + } + } + }, + "connection_pool": { + "type": "object", + "properties": { + "enable_dns_resolver": { + "type": "boolean", + "description": "enables gRPC client connection pool dns resolver, when enabled vald uses ip handshake exclude dns discovery which improves network performance" + }, + "enable_rebalance": { + "type": "boolean", + "description": "enables gRPC client connection pool rebalance" + }, + "old_conn_close_duration": { + "type": "string", + "description": "makes delay before gRPC client connection closing during connection pool rebalance" + }, + "rebalance_duration": { + "type": "string", + "description": "gRPC client connection pool rebalance duration" + }, + "size": { + "type": "integer", + "description": "gRPC client connection pool size" + } + } + }, + "dial_option": { + "type": "object", + "properties": { + "backoff_base_delay": { + "type": "string", + "description": "gRPC client dial option base backoff delay" + }, + "backoff_jitter": { + "type": "number", + "description": "gRPC client dial option base backoff delay" + }, + "backoff_max_delay": { + "type": "string", + "description": "gRPC client dial option max backoff delay" + }, + "backoff_multiplier": { + "type": "number", + "description": "gRPC client dial option base backoff delay" + }, + "enable_backoff": { + "type": "boolean", + "description": "gRPC client dial option backoff enabled" + }, + "initial_connection_window_size": { + "type": "integer", + "description": "gRPC client dial option initial connection window size" + }, + "initial_window_size": { + "type": "integer", + "description": "gRPC client dial option initial window size" + }, + "insecure": { + "type": "boolean", + "description": "gRPC client dial option insecure enabled" + }, + "interceptors": { + "type": "array", + "description": "gRPC client interceptors", + "items": { "type": "string", "enum": ["TraceInterceptor"] } + }, + "keepalive": { + "type": "object", + "properties": { + "permit_without_stream": { + "type": "boolean", + "description": "gRPC client keep alive permit without stream" + }, + "time": { + "type": "string", + "description": "gRPC client keep alive time" + }, + "timeout": { + "type": "string", + "description": "gRPC client keep alive timeout" + } + } + }, + "max_msg_size": { + "type": "integer", + "description": "gRPC client dial option max message size" + }, + "min_connection_timeout": { + "type": "string", + "description": "gRPC client dial option minimum connection timeout" + }, + "net": { + "type": "object", + "properties": { + "dialer": { + "type": "object", + "properties": { + "dual_stack_enabled": { + "type": "boolean", + "description": "gRPC client TCP dialer dual stack enabled" + }, + "keepalive": { + "type": "string", + "description": "gRPC client TCP dialer keep alive" + }, + "timeout": { + "type": "string", + "description": "gRPC client TCP dialer timeout" + } + } + }, + "dns": { + "type": "object", + "properties": { + "cache_enabled": { + "type": "boolean", + "description": "gRPC client TCP DNS cache enabled" + }, + "cache_expiration": { + "type": "string", + "description": "gRPC client TCP DNS cache expiration" + }, + "refresh_duration": { + "type": "string", + "description": "gRPC client TCP DNS cache refresh duration" + } + } + }, + "socket_option": { + "type": "object", + "properties": { + "ip_recover_destination_addr": { + "type": "boolean", + "description": "server listen socket option for ip_recover_destination_addr functionality" + }, + "ip_transparent": { + "type": "boolean", + "description": "server listen socket option for ip_transparent functionality" + }, + "reuse_addr": { + "type": "boolean", + "description": "server listen socket option for reuse_addr functionality" + }, + "reuse_port": { + "type": "boolean", + "description": "server listen socket option for reuse_port functionality" + }, + "tcp_cork": { + "type": "boolean", + "description": "server listen socket option for tcp_cork functionality" + }, + "tcp_defer_accept": { + "type": "boolean", + "description": "server listen socket option for tcp_defer_accept functionality" + }, + "tcp_fast_open": { + "type": "boolean", + "description": "server listen socket option for tcp_fast_open functionality" + }, + "tcp_no_delay": { + "type": "boolean", + "description": "server listen socket option for tcp_no_delay functionality" + }, + "tcp_quick_ack": { + "type": "boolean", + "description": "server listen socket option for tcp_quick_ack functionality" + } + } + }, + "tls": { + "type": "object", + "properties": { + "ca": { "type": "string" }, + "cert": { "type": "string" }, + "enabled": { "type": "boolean" }, + "insecure_skip_verify": { "type": "boolean" }, + "key": { "type": "string" } + } + } + } + }, + "read_buffer_size": { + "type": "integer", + "description": "gRPC client dial option read buffer size" + }, + "timeout": { + "type": "string", + "description": "gRPC client dial option timeout" + }, + "write_buffer_size": { + "type": "integer", + "description": "gRPC client dial option write buffer size" + } + } + }, + "health_check_duration": { + "type": "string", + "description": "gRPC client health check duration" + }, + "max_recv_msg_size": { "type": "integer" }, + "max_retry_rpc_buffer_size": { "type": "integer" }, + "max_send_msg_size": { "type": "integer" }, + "tls": { + "type": "object", + "properties": { + "ca": { "type": "string", "description": "TLS ca path" }, + "cert": { "type": "string", "description": "TLS cert path" }, + "enabled": { "type": "boolean", "description": "TLS enabled" }, + "insecure_skip_verify": { + "type": "boolean", + "description": "enable/disable skip SSL certificate verification" + }, + "key": { "type": "string", "description": "TLS key path" } + } + }, + "wait_for_ready": { "type": "boolean" } + } + }, + "concurrency_limit": { + "type": "integer", + "description": "concurrency_limit represents the goroutine limit count. It affects the job performance.", + "maximum": 65535, + "minimum": 0 + }, + "dataset": { + "type": "object", + "description": "dataset information", + "properties": { + "group": { + "type": "string", + "description": "the hdf5 group name of dataset", + "minLength": 1 + }, + "indexes": { + "type": "integer", + "description": "the amount of indexes", + "minimum": 0 + }, + "name": { + "type": "string", + "description": "the name of dataset", + "enum": ["original", "fashion-mnist"] + }, + "range": { + "type": "object", + "description": "the data range of indexes", + "properties": { + "end": { + "type": "integer", + "description": "end index number", + "minimum": 1 + }, + "start": { + "type": "integer", + "description": "start index number", + "minimum": 1 + } + }, + "required": ["start", "end"] + }, + "url": { + "type": "string", + "description": "the dataset url which is used for executing benchmark job with user defined hdf5 file" + } + }, + "required": ["name", "indexes", "group", "range"] + }, + "dimension": { + "type": "integer", + "description": "vector dimension", + "minimum": 1 + }, + "global_config": { + "type": "object", + "properties": { + "logging": { + "type": "object", + "properties": { + "format": { "type": "string", "enum": ["raw", "json"] }, + "level": { + "type": "string", + "enum": ["debug", "info", "warn", "error", "fatal"] + }, + "logger": { "type": "string", "enum": ["glg", "zap"] } + } + }, + "time_zone": { "type": "string" }, + "version": { "type": "string" } + } + }, + "insert_config": { + "type": "object", + "description": "insert config", + "properties": { + "skip_strict_exist_check": { + "type": "boolean", + "description": "skip strict exists check flag config" + }, + "timestamp": { "type": "string", "description": "index timestamp" } + } + }, + "job_type": { + "type": "string", + "description": "job type name", + "enum": [ + "insert", + "update", + "upsert", + "search", + "remove", + "getobject", + "exists" + ] + }, + "object_config": { + "type": "object", + "description": "object config", + "properties": { + "filter_config": { + "type": "object", + "description": "filter target config", + "properties": { + "host": { "type": "string", "description": "filter target host" } + } + } + } + }, + "remove_config": { + "type": "object", + "description": "remove config", + "properties": { + "skip_strict_exist_check": { + "type": "boolean", + "description": "skip strict exists check flag config" + }, + "timestamp": { "type": "string", "description": "index timestamp" } + } + }, + "repetition": { + "type": "integer", + "description": "the number of repeat job", + "minimum": 1 + }, + "replica": { + "type": "integer", + "description": "the number of running concurrency job", + "minimum": 1 + }, + "rps": { + "type": "integer", + "description": "desired request per sec", + "maximum": 65535, + "minimum": 0 + }, + "rules": { + "type": "array", + "description": "executing rule", + "items": { "type": "string" } + }, + "search_config": { + "type": "object", + "description": "upsert config", + "properties": { + "aggregation_algorithm": { + "type": "string", + "description": "search result aggregation algorithm", + "enum": [ + "Unknown", + "ConcurrentQueue", + "SortSlice", + "SortPoolSlice", + "PairingHeap" + ] + }, + "enable_linear_search": { + "type": "boolean", + "description": "enable linear search for calculation recall" + }, + "epsilon": { "type": "number", "description": "epsilon" }, + "min_num": { + "type": "integer", + "description": "minimum number of top-k" + }, + "num": { "type": "integer", "description": "number of top-k" }, + "radius": { "type": "number", "description": "radius" }, + "timeout": { + "type": "string", + "description": "search operation timeout" + } + } + }, + "server_config": { + "type": "object", + "properties": { + "healths": { + "type": "object", + "properties": { + "liveness": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "liveness server enabled" + }, + "host": { + "type": "string", + "description": "liveness server host" + }, + "livenessProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer", + "description": "liveness probe failure threshold" + }, + "httpGet": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "liveness probe path" + }, + "port": { + "type": "string", + "description": "liveness probe port" + }, + "scheme": { + "type": "string", + "description": "liveness probe scheme" + } + } + }, + "initialDelaySeconds": { + "type": "integer", + "description": "liveness probe initial delay seconds" + }, + "periodSeconds": { + "type": "integer", + "description": "liveness probe period seconds" + }, + "successThreshold": { + "type": "integer", + "description": "liveness probe success threshold" + }, + "timeoutSeconds": { + "type": "integer", + "description": "liveness probe timeout seconds" + } + } + }, + "port": { + "type": "integer", + "description": "liveness server port", + "maximum": 65535, + "minimum": 0 + }, + "server": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "handler_timeout": { + "type": "string", + "description": "liveness server handler timeout" + }, + "idle_timeout": { + "type": "string", + "description": "liveness server idle timeout" + }, + "read_header_timeout": { + "type": "string", + "description": "liveness server read header timeout" + }, + "read_timeout": { + "type": "string", + "description": "liveness server read timeout" + }, + "shutdown_duration": { + "type": "string", + "description": "liveness server shutdown duration" + }, + "write_timeout": { + "type": "string", + "description": "liveness server write timeout" + } + } + }, + "mode": { + "type": "string", + "description": "liveness server mode" + }, + "network": { + "type": "string", + "description": "mysql network", + "enum": [ + "tcp", + "tcp4", + "tcp6", + "udp", + "udp4", + "udp6", + "unix", + "unixgram", + "unixpacket" + ] + }, + "probe_wait_time": { + "type": "string", + "description": "liveness server probe wait time" + }, + "socket_option": { + "type": "object", + "properties": { + "ip_recover_destination_addr": { + "type": "boolean", + "description": "server listen socket option for ip_recover_destination_addr functionality" + }, + "ip_transparent": { + "type": "boolean", + "description": "server listen socket option for ip_transparent functionality" + }, + "reuse_addr": { + "type": "boolean", + "description": "server listen socket option for reuse_addr functionality" + }, + "reuse_port": { + "type": "boolean", + "description": "server listen socket option for reuse_port functionality" + }, + "tcp_cork": { + "type": "boolean", + "description": "server listen socket option for tcp_cork functionality" + }, + "tcp_defer_accept": { + "type": "boolean", + "description": "server listen socket option for tcp_defer_accept functionality" + }, + "tcp_fast_open": { + "type": "boolean", + "description": "server listen socket option for tcp_fast_open functionality" + }, + "tcp_no_delay": { + "type": "boolean", + "description": "server listen socket option for tcp_no_delay functionality" + }, + "tcp_quick_ack": { + "type": "boolean", + "description": "server listen socket option for tcp_quick_ack functionality" + } + } + }, + "socket_path": { + "type": "string", + "description": "mysql socket_path" + } + } + }, + "servicePort": { + "type": "integer", + "description": "liveness server service port", + "maximum": 65535, + "minimum": 0 + } + } + }, + "readiness": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "readiness server enabled" + }, + "host": { + "type": "string", + "description": "readiness server host" + }, + "port": { + "type": "integer", + "description": "readiness server port", + "maximum": 65535, + "minimum": 0 + }, + "readinessProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer", + "description": "readiness probe failure threshold" + }, + "httpGet": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "readiness probe path" + }, + "port": { + "type": "string", + "description": "readiness probe port" + }, + "scheme": { + "type": "string", + "description": "readiness probe scheme" + } + } + }, + "initialDelaySeconds": { + "type": "integer", + "description": "readiness probe initial delay seconds" + }, + "periodSeconds": { + "type": "integer", + "description": "readiness probe period seconds" + }, + "successThreshold": { + "type": "integer", + "description": "readiness probe success threshold" + }, + "timeoutSeconds": { + "type": "integer", + "description": "readiness probe timeout seconds" + } + } + }, + "server": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "handler_timeout": { + "type": "string", + "description": "readiness server handler timeout" + }, + "idle_timeout": { + "type": "string", + "description": "readiness server idle timeout" + }, + "read_header_timeout": { + "type": "string", + "description": "readiness server read header timeout" + }, + "read_timeout": { + "type": "string", + "description": "readiness server read timeout" + }, + "shutdown_duration": { + "type": "string", + "description": "readiness server shutdown duration" + }, + "write_timeout": { + "type": "string", + "description": "readiness server write timeout" + } + } + }, + "mode": { + "type": "string", + "description": "readiness server mode" + }, + "network": { + "type": "string", + "description": "mysql network", + "enum": [ + "tcp", + "tcp4", + "tcp6", + "udp", + "udp4", + "udp6", + "unix", + "unixgram", + "unixpacket" + ] + }, + "probe_wait_time": { + "type": "string", + "description": "readiness server probe wait time" + }, + "socket_option": { + "type": "object", + "properties": { + "ip_recover_destination_addr": { + "type": "boolean", + "description": "server listen socket option for ip_recover_destination_addr functionality" + }, + "ip_transparent": { + "type": "boolean", + "description": "server listen socket option for ip_transparent functionality" + }, + "reuse_addr": { + "type": "boolean", + "description": "server listen socket option for reuse_addr functionality" + }, + "reuse_port": { + "type": "boolean", + "description": "server listen socket option for reuse_port functionality" + }, + "tcp_cork": { + "type": "boolean", + "description": "server listen socket option for tcp_cork functionality" + }, + "tcp_defer_accept": { + "type": "boolean", + "description": "server listen socket option for tcp_defer_accept functionality" + }, + "tcp_fast_open": { + "type": "boolean", + "description": "server listen socket option for tcp_fast_open functionality" + }, + "tcp_no_delay": { + "type": "boolean", + "description": "server listen socket option for tcp_no_delay functionality" + }, + "tcp_quick_ack": { + "type": "boolean", + "description": "server listen socket option for tcp_quick_ack functionality" + } + } + }, + "socket_path": { + "type": "string", + "description": "mysql socket_path" + } + } + }, + "servicePort": { + "type": "integer", + "description": "readiness server service port", + "maximum": 65535, + "minimum": 0 + } + } + }, + "startup": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "startup server enabled" + }, + "port": { + "type": "integer", + "description": "startup server port", + "maximum": 65535, + "minimum": 0 + }, + "startupProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer", + "description": "startup probe failure threshold" + }, + "httpGet": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "startup probe path" + }, + "port": { + "type": "string", + "description": "startup probe port" + }, + "scheme": { + "type": "string", + "description": "startup probe scheme" + } + } + }, + "initialDelaySeconds": { + "type": "integer", + "description": "startup probe initial delay seconds" + }, + "periodSeconds": { + "type": "integer", + "description": "startup probe period seconds" + }, + "successThreshold": { + "type": "integer", + "description": "startup probe success threshold" + }, + "timeoutSeconds": { + "type": "integer", + "description": "startup probe timeout seconds" + } + } + } + } + } + } + }, + "servers": { + "type": "object", + "properties": { + "grpc": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "gRPC server enabled" + }, + "host": { "type": "string", "description": "gRPC server host" }, + "port": { + "type": "integer", + "description": "gRPC server port", + "maximum": 65535, + "minimum": 0 + }, + "server": { + "type": "object", + "properties": { + "grpc": { + "type": "object", + "properties": { + "bidirectional_stream_concurrency": { + "type": "integer", + "description": "gRPC server bidirectional stream concurrency" + }, + "connection_timeout": { + "type": "string", + "description": "gRPC server connection timeout" + }, + "enable_reflection": { + "type": "boolean", + "description": "gRPC server reflection option" + }, + "header_table_size": { + "type": "integer", + "description": "gRPC server header table size" + }, + "initial_conn_window_size": { + "type": "integer", + "description": "gRPC server initial connection window size" + }, + "initial_window_size": { + "type": "integer", + "description": "gRPC server initial window size" + }, + "interceptors": { + "type": "array", + "description": "gRPC server interceptors", + "items": { + "type": "string", + "enum": [ + "RecoverInterceptor", + "AccessLogInterceptor", + "TraceInterceptor", + "MetricInterceptor" + ] + } + }, + "keepalive": { + "type": "object", + "properties": { + "max_conn_age": { + "type": "string", + "description": "gRPC server keep alive max connection age" + }, + "max_conn_age_grace": { + "type": "string", + "description": "gRPC server keep alive max connection age grace" + }, + "max_conn_idle": { + "type": "string", + "description": "gRPC server keep alive max connection idle" + }, + "min_time": { + "type": "string", + "description": "gRPC server keep alive min_time" + }, + "permit_without_stream": { + "type": "boolean", + "description": "gRPC server keep alive permit_without_stream" + }, + "time": { + "type": "string", + "description": "gRPC server keep alive time" + }, + "timeout": { + "type": "string", + "description": "gRPC server keep alive timeout" + } + } + }, + "max_header_list_size": { + "type": "integer", + "description": "gRPC server max header list size" + }, + "max_receive_message_size": { + "type": "integer", + "description": "gRPC server max receive message size" + }, + "max_send_message_size": { + "type": "integer", + "description": "gRPC server max send message size" + }, + "read_buffer_size": { + "type": "integer", + "description": "gRPC server read buffer size" + }, + "write_buffer_size": { + "type": "integer", + "description": "gRPC server write buffer size" + } + } + }, + "mode": { + "type": "string", + "description": "gRPC server server mode" + }, + "network": { + "type": "string", + "description": "mysql network", + "enum": [ + "tcp", + "tcp4", + "tcp6", + "udp", + "udp4", + "udp6", + "unix", + "unixgram", + "unixpacket" + ] + }, + "probe_wait_time": { + "type": "string", + "description": "gRPC server probe wait time" + }, + "restart": { + "type": "boolean", + "description": "gRPC server restart" + }, + "socket_option": { + "type": "object", + "properties": { + "ip_recover_destination_addr": { + "type": "boolean", + "description": "server listen socket option for ip_recover_destination_addr functionality" + }, + "ip_transparent": { + "type": "boolean", + "description": "server listen socket option for ip_transparent functionality" + }, + "reuse_addr": { + "type": "boolean", + "description": "server listen socket option for reuse_addr functionality" + }, + "reuse_port": { + "type": "boolean", + "description": "server listen socket option for reuse_port functionality" + }, + "tcp_cork": { + "type": "boolean", + "description": "server listen socket option for tcp_cork functionality" + }, + "tcp_defer_accept": { + "type": "boolean", + "description": "server listen socket option for tcp_defer_accept functionality" + }, + "tcp_fast_open": { + "type": "boolean", + "description": "server listen socket option for tcp_fast_open functionality" + }, + "tcp_no_delay": { + "type": "boolean", + "description": "server listen socket option for tcp_no_delay functionality" + }, + "tcp_quick_ack": { + "type": "boolean", + "description": "server listen socket option for tcp_quick_ack functionality" + } + } + }, + "socket_path": { + "type": "string", + "description": "mysql socket_path" + } + } + }, + "servicePort": { + "type": "integer", + "description": "gRPC server service port", + "maximum": 65535, + "minimum": 0 + } + } + }, + "rest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "REST server enabled" + }, + "host": { "type": "string", "description": "REST server host" }, + "port": { + "type": "integer", + "description": "REST server port", + "maximum": 65535, + "minimum": 0 + }, + "server": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "handler_timeout": { + "type": "string", + "description": "REST server handler timeout" + }, + "idle_timeout": { + "type": "string", + "description": "REST server idle timeout" + }, + "read_header_timeout": { + "type": "string", + "description": "REST server read header timeout" + }, + "read_timeout": { + "type": "string", + "description": "REST server read timeout" + }, + "shutdown_duration": { + "type": "string", + "description": "REST server shutdown duration" + }, + "write_timeout": { + "type": "string", + "description": "REST server write timeout" + } + } + }, + "mode": { + "type": "string", + "description": "REST server server mode" + }, + "network": { + "type": "string", + "description": "mysql network", + "enum": [ + "tcp", + "tcp4", + "tcp6", + "udp", + "udp4", + "udp6", + "unix", + "unixgram", + "unixpacket" + ] + }, + "probe_wait_time": { + "type": "string", + "description": "REST server probe wait time" + }, + "socket_option": { + "type": "object", + "properties": { + "ip_recover_destination_addr": { + "type": "boolean", + "description": "server listen socket option for ip_recover_destination_addr functionality" + }, + "ip_transparent": { + "type": "boolean", + "description": "server listen socket option for ip_transparent functionality" + }, + "reuse_addr": { + "type": "boolean", + "description": "server listen socket option for reuse_addr functionality" + }, + "reuse_port": { + "type": "boolean", + "description": "server listen socket option for reuse_port functionality" + }, + "tcp_cork": { + "type": "boolean", + "description": "server listen socket option for tcp_cork functionality" + }, + "tcp_defer_accept": { + "type": "boolean", + "description": "server listen socket option for tcp_defer_accept functionality" + }, + "tcp_fast_open": { + "type": "boolean", + "description": "server listen socket option for tcp_fast_open functionality" + }, + "tcp_no_delay": { + "type": "boolean", + "description": "server listen socket option for tcp_no_delay functionality" + }, + "tcp_quick_ack": { + "type": "boolean", + "description": "server listen socket option for tcp_quick_ack functionality" + } + } + }, + "socket_path": { + "type": "string", + "description": "mysql socket_path" + } + } + }, + "servicePort": { + "type": "integer", + "description": "REST server service port", + "maximum": 65535, + "minimum": 0 + } + } + } + } + } + } + }, + "target": { + "type": "object", + "description": "target cluster location", + "properties": { + "host": { + "type": "string", + "description": "target cluster host", + "minLength": 1 + }, + "port": { + "type": "integer", + "description": "target cluster port", + "maximum": 65535, + "minimum": 0 + } + }, + "required": ["host", "port"] + }, + "ttl_seconds_after_finished": { + "type": "integer", + "description": "limits the lifetime of a Job that has finished execution.", + "maximum": 65535, + "minimum": 0 + }, + "update_config": { + "type": "object", + "description": "update config", + "properties": { + "disable_balance_update": { + "type": "boolean", + "description": "disabled balance update flag" + }, + "skip_strict_exist_check": { + "type": "boolean", + "description": "skip strict exists check flag config" + }, + "timestamp": { "type": "string", "description": "index timestamp" } + } + }, + "upsert_config": { + "type": "object", + "description": "upsert config", + "properties": { + "disable_balance_update": { + "type": "boolean", + "description": "disabled balance update flag" + }, + "skip_strict_exist_check": { + "type": "boolean", + "description": "skip strict exists check flag config" + }, + "timestamp": { "type": "string", "description": "index timestamp" } + } + } + } +} diff --git a/charts/vald-benchmark-operator/scenario-values.schema.json b/charts/vald-benchmark-operator/scenario-values.schema.json new file mode 100644 index 0000000000..7817e50c38 --- /dev/null +++ b/charts/vald-benchmark-operator/scenario-values.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "Values", + "type": "object", + "properties": { + "dataset": { + "type": "object", + "description": "dataset information", + "properties": { + "group": { + "type": "string", + "description": "the hdf5 group name of dataset", + "minLength": 1 + }, + "indexes": { + "type": "integer", + "description": "the amount of indexes", + "minimum": 0 + }, + "name": { + "type": "string", + "description": "the name of dataset", + "enum": ["original", "fashion-mnist"] + }, + "range": { + "type": "object", + "description": "the data range of indexes", + "properties": { + "end": { + "type": "integer", + "description": "end index number", + "minimum": 1 + }, + "start": { + "type": "integer", + "description": "start index number", + "minimum": 1 + } + }, + "required": ["start", "end"] + }, + "url": { + "type": "string", + "description": "the dataset url which is used for executing benchmark job with user defined hdf5 file" + } + }, + "required": ["name", "indexes", "group", "range"] + }, + "jobs": { "type": "array", "items": { "type": "object" } }, + "target": { + "type": "object", + "description": "target cluster location", + "properties": { + "host": { + "type": "string", + "description": "target cluster host", + "minLength": 1 + }, + "port": { + "type": "integer", + "description": "target cluster port", + "maximum": 65535, + "minimum": 0 + } + }, + "required": ["host", "port"] + } + } +} diff --git a/charts/vald-benchmark-operator/schemas/job-values.yaml b/charts/vald-benchmark-operator/schemas/job-values.yaml new file mode 100644 index 0000000000..8b7de934cd --- /dev/null +++ b/charts/vald-benchmark-operator/schemas/job-values.yaml @@ -0,0 +1,819 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# + +# @schema {"name": "target", "type": "object", "required": ["host", "port"]} +# target -- target cluster location +target: + # @schema {"name": "target.host", "type": "string", "minLength": 1} + # target.host -- target cluster host + host: "vald-lb-gateway.default.svc.cluster.local" + # @schema {"name": "target.port", "type": "integer", "minimum": 0, "maximum": 65535} + # target.port -- target cluster port + port: 8081 +# @schema {"name": "dataset", "type": "object", "required": ["name", "indexes", "group", "range"]} +# dataset -- dataset information +dataset: + # @schema {"name": "dataset.name", "type": "string", "enum": ["original", "fashion-mnist"] } + # dataset.name -- the name of dataset + name: "fashion-mnist" + # @schema {"name": "dataset.indexes", "type": "integer", "minimum": 0} + # dataset.indexes -- the amount of indexes + indexes: 1000 + # @schema {"name": "dataset.group", "type": "string", "minLength": 1} + # dataset.group -- the hdf5 group name of dataset + group: "test" + # @schema {"name": "dataset.range", "type": "object", "required": ["start", "end"]} + # dataset.range -- the data range of indexes + range: + # @schema {"name": "dataset.range.start", "type": "integer", "minimum": 1} + # dataset.range.start -- start index number + start: 1 + # @schema {"name": "dataset.range.end", "type": "integer", "minimum": 1} + # dataset.range.end -- end index number + end: 1000 + # @schema {"name": "dataset.url", "type": "string"} + # dataset.url -- the dataset url which is used for executing benchmark job with user defined hdf5 file + url: "" +# @schema {"name": "dimension", "type": "integer", "minimum": 1} +# dimension -- vector dimension +dimension: 784 +# @schema {"name": "replica", "type": "integer", "minimum": 1} +# replica -- the number of running concurrency job +replica: 1 +# @schema {"name": "repetition", "type": "integer", "minimum": 1} +# repetition -- the number of repeat job +repetition: 1 +# @schema {"name": "rules", "type": "array", "items": {"type": "string"}} +# rules -- executing rule +rules: [] +# @schema {"name": "rps", "type": "integer", "minimum": 0, "maximum": 65535} +# rps -- desired request per sec +rps: 1000 +# @schema {"name": "concurrency_limit", "type": "integer", "minimum": 0, "maximum": 65535} +# concurrency_limit -- concurrency_limit represents the goroutine limit count. It affects the job performance. +concurrency_limit: 200 +# @schema {"name": "ttl_seconds_after_finished", "type": "integer", "minimum": 0, "maximum": 65535} +# ttl_seconds_after_finished -- limits the lifetime of a Job that has finished execution. +ttl_seconds_after_finished: 10 +# @schema {"name": "job_type", "type": "string", "enum": ["insert", "update", "upsert", "search", "remove", "getobject", "exists"]} +# job_type -- job type name +job_type: "search" +# @schema {"name": "insert_config", "type": "object"} +# insert_config -- insert config +insert_config: + # @schema {"name": "insert_config.skip_strict_exist_check", "type": "boolean"} + # insert_config.skip_strict_exist_check -- skip strict exists check flag config + skip_strict_exist_check: false + # @schema {"name": "insert_config.timestamp", "type": "string"} + # insert_config.timestamp -- index timestamp + timestamp: "" +# @schema {"name": "update_config", "type": "object"} +# update_config -- update config +update_config: + # @schema {"name": "update_config.skip_strict_exist_check", "type": "boolean"} + # update_config.skip_strict_exist_check -- skip strict exists check flag config + skip_strict_exist_check: false + # @schema {"name": "update_config.timestamp", "type": "string"} + # update_config.timestamp -- index timestamp + timestamp: "" + # @schema {"name": "update_config.disable_balance_update", "type": "boolean"} + # update_config.disable_balance_update -- disabled balance update flag + disable_balance_update: false +# @schema {"name": "upsert_config", "type": "object"} +# upsert_config -- upsert config +upsert_config: + # @schema {"name": "upsert_config.skip_strict_exist_check", "type": "boolean"} + # upsert_config.skip_strict_exist_check -- skip strict exists check flag config + skip_strict_exist_check: false + # @schema {"name": "upsert_config.timestamp", "type": "string"} + # upsert_config.timestamp -- index timestamp + timestamp: "" + # @schema {"name": "upsert_config.disable_balance_update", "type": "boolean"} + # upsert_config.disable_balance_update -- disabled balance update flag + disable_balance_update: false +# @schema {"name": "search_config", "type": "object"} +# search_config -- upsert config +search_config: + # @schema {"name": "search_config.epsilon", "type": "number"} + # search_config.epsilon -- epsilon + epsilon: 0.1 + # @schema {"name": "search_config.radius", "type": "number"} + # search_config.radius -- radius + radius: -1 + # @schema {"name": "search_config.num", "type": "integer"} + # search_config.num -- number of top-k + num: 10 + # @schema {"name": "search_config.min_num", "type": "integer"} + # search_config.min_num -- minimum number of top-k + min_num: 10 + # @schema {"name": "search_config.timeout", "type": "string"} + # search_config.timeout -- search operation timeout + timeout: "10s" + # @schema {"name": "search_config.enable_linear_search", "type": "boolean"} + # search_config.enable_linear_search -- enable linear search for calculation recall + enable_linear_search: true + # @schema {"name": "search_config.aggregation_algorithm", "type": "string", "enum": ["Unknown", "ConcurrentQueue", "SortSlice", "SortPoolSlice", "PairingHeap"]} + # search_config.aggregation_algorithm -- search result aggregation algorithm + aggregation_algorithm: "Unknown" +# @schema {"name": "remove_config", "type": "object"} +# remove_config -- remove config +remove_config: + # @schema {"name": "remove_config.skip_strict_exist_check", "type": "boolean"} + # remove_config.skip_strict_exist_check -- skip strict exists check flag config + skip_strict_exist_check: false + # @schema {"name": "remove_config.timestamp", "type": "string"} + # remove_config.timestamp -- index timestamp + timestamp: "" +# @schema {"name": "object_config", "type": "object"} +# object_config -- object config +object_config: + # @schema {"name": "object_config.filter_config", "type": "object"} + # object_config.filter_config -- filter target config + filter_config: + # @schema {"name": "object_config.filter_config.host", "type": "string"} + # object_config.filter_config.host -- filter target host + host: 0.0.0.0 + # @schema {"name": "object_config.filter_config.host", "type": "integer"} + # object_config.filter_config.port -- filter target host + port: 8081 +# @schema {"name": "client_config", "type": "object"} +# client_config -- gRPC client config for request to the Vald cluster +client_config: + # @schema {"name": "client_config.addrs", "type": "array", "items": {"type": "string"}} + # client_config.addrs -- gRPC client addresses + addrs: [] + # @schema {"name": "client_config.health_check_duration", "type": "string"} + # client_config.health_check_duration -- gRPC client health check duration + health_check_duration: "1s" + # @schema {"name": "client_config.connection_pool", "type": "object"} + connection_pool: + # @schema {"name": "client_config.connection_pool.enable_dns_resolver", "type": "boolean"} + # client_config.connection_pool.enable_dns_resolver -- enables gRPC client connection pool dns resolver, when enabled vald uses ip handshake exclude dns discovery which improves network performance + enable_dns_resolver: true + # @schema {"name": "client_config.connection_pool.enable_rebalance", "type": "boolean"} + # client_config.connection_pool.enable_rebalance -- enables gRPC client connection pool rebalance + enable_rebalance: true + # @schema {"name": "client_config.connection_pool.rebalance_duration", "type": "string"} + # client_config.connection_pool.rebalance_duration -- gRPC client connection pool rebalance duration + rebalance_duration: 30m + # @schema {"name": "client_config.connection_pool.size", "type": "integer"} + # client_config.connection_pool.size -- gRPC client connection pool size + size: 3 + # @schema {"name": "client_config.connection_pool.old_conn_close_duration", "type": "string"} + # client_config.connection_pool.old_conn_close_duration -- makes delay before gRPC client connection closing during connection pool rebalance + old_conn_close_duration: "2m" + # @schema {"name": "client_config.backoff", "type": "object", "anchor": "backoff"} + backoff: + # @schema {"name": "client_config.backoff.initial_duration", "type": "string"} + # client_config.backoff.initial_duration -- gRPC client backoff initial duration + initial_duration: 5ms + # @schema {"name": "client_config.backoff.backoff_time_limit", "type": "string"} + # client_config.backoff.backoff_time_limit -- gRPC client backoff time limit + backoff_time_limit: 5s + # @schema {"name": "client_config.backoff.maximum_duration", "type": "string"} + # client_config.backoff.maximum_duration -- gRPC client backoff maximum duration + maximum_duration: 5s + # @schema {"name": "client_config.backoff.jitter_limit", "type": "string"} + # client_config.backoff.jitter_limit -- gRPC client backoff jitter limit + jitter_limit: 100ms + # @schema {"name": "client_config.backoff.backoff_factor", "type": "number"} + # client_config.backoff.backoff_factor -- gRPC client backoff factor + backoff_factor: 1.1 + # @schema {"name": "client_config.backoff.retry_count", "type": "integer"} + # client_config.backoff.retry_count -- gRPC client backoff retry count + retry_count: 100 + # @schema {"name": "client_config.backoff.enable_error_log", "type": "boolean"} + # client_config.backoff.enable_error_log -- gRPC client backoff log enabled + enable_error_log: true + # @schema {"name": "client_config.circuit_breaker", "type": "object"} + circuit_breaker: + # @schema {"name": "client_config.circuit_breaker.closed_error_rate", "type": "number"} + # client_config.circuit_breaker.closed_error_rate -- gRPC client circuitbreaker closed error rate + closed_error_rate: 0.7 + # @schema {"name": "client_config.circuit_breaker.half_open_error_rate", "type": "number"} + # client_config.circuit_breaker.half_open_error_rate -- gRPC client circuitbreaker half-open error rate + half_open_error_rate: 0.5 + # @schema {"name": "client_config.circuit_breaker.min_samples", "type": "integer"} + # client_config.circuit_breaker.min_samples -- gRPC client circuitbreaker minimum sampling count + min_samples: 1000 + # @schema {"name": "client_config.circuit_breaker.open_timeout", "type": "string"} + # client_config.circuit_breaker.open_timeout -- gRPC client circuitbreaker open timeout + open_timeout: "1s" + # @schema {"name": "client_config.circuit_breaker.closed_refresh_timeout", "type": "string"} + # client_config.circuit_breaker.closed_refresh_timeout -- gRPC client circuitbreaker closed refresh timeout + closed_refresh_timeout: "10s" + # @schema {"name": "client_config.call_option", "type": "object"} + call_option: + # @schema {"name": "client_config.wait_for_ready", "type": "boolean"} + # client_config.call_option.wait_for_ready -- gRPC client call option wait for ready + wait_for_ready: true + # @schema {"name": "client_config.max_retry_rpc_buffer_size", "type": "integer"} + # client_config.call_option.max_retry_rpc_buffer_size -- gRPC client call option max retry rpc buffer size + max_retry_rpc_buffer_size: 0 + # @schema {"name": "client_config.max_recv_msg_size", "type": "integer"} + # client_config.call_option.max_recv_msg_size -- gRPC client call option max receive message size + max_recv_msg_size: 0 + # @schema {"name": "client_config.max_send_msg_size", "type": "integer"} + # client_config.call_option.max_send_msg_size -- gRPC client call option max send message size + max_send_msg_size: 0 + # @schema {"name": "client_config.dial_option", "type": "object"} + dial_option: + # @schema {"name": "client_config.dial_option.write_buffer_size", "type": "integer"} + # client_config.dial_option.write_buffer_size -- gRPC client dial option write buffer size + write_buffer_size: 0 + # @schema {"name": "client_config.dial_option.read_buffer_size", "type": "integer"} + # client_config.dial_option.read_buffer_size -- gRPC client dial option read buffer size + read_buffer_size: 0 + # @schema {"name": "client_config.dial_option.initial_window_size", "type": "integer"} + # client_config.dial_option.initial_window_size -- gRPC client dial option initial window size + initial_window_size: 0 + # @schema {"name": "client_config.dial_option.initial_connection_window_size", "type": "integer"} + # client_config.dial_option.initial_connection_window_size -- gRPC client dial option initial connection window size + initial_connection_window_size: 0 + # @schema {"name": "client_config.dial_option.max_msg_size", "type": "integer"} + # client_config.dial_option.max_msg_size -- gRPC client dial option max message size + max_msg_size: 0 + # @schema {"name": "client_config.dial_option.backoff_max_delay", "type": "string"} + # client_config.dial_option.backoff_max_delay -- gRPC client dial option max backoff delay + backoff_max_delay: "120s" + # @schema {"name": "client_config.dial_option.backoff_base_delay", "type": "string"} + # client_config.dial_option.backoff_base_delay -- gRPC client dial option base backoff delay + backoff_base_delay: "1s" + # @schema {"name": "client_config.dial_option.backoff_multiplier", "type": "number"} + # client_config.dial_option.backoff_multiplier -- gRPC client dial option base backoff delay + backoff_multiplier: 1.6 + # @schema {"name": "client_config.dial_option.backoff_jitter", "type": "number"} + # client_config.dial_option.backoff_jitter -- gRPC client dial option base backoff delay + backoff_jitter: 0.2 + # @schema {"name": "client_config.dial_option.min_connection_timeout", "type": "string"} + # client_config.dial_option.min_connection_timeout -- gRPC client dial option minimum connection timeout + min_connection_timeout: "20s" + # @schema {"name": "client_config.dial_option.enable_backoff", "type": "boolean"} + # client_config.dial_option.enable_backoff -- gRPC client dial option backoff enabled + enable_backoff: false + # @schema {"name": "client_config.dial_option.insecure", "type": "boolean"} + # client_config.dial_option.insecure -- gRPC client dial option insecure enabled + insecure: true + # @schema {"name": "client_config.dial_option.timeout", "type": "string"} + # client_config.dial_option.timeout -- gRPC client dial option timeout + timeout: "" + # @schema {"name": "client_config.dial_option.interceptors", "type": "array", "items": {"type": "string", "enum": ["TraceInterceptor"]}} + # client_config.dial_option.interceptors -- gRPC client interceptors + interceptors: [] + # @schema {"name": "client_config.dial_option.net", "type": "object", "anchor": "net"} + net: + # @schema {"name": "client_config.dial_option.net.dns", "type": "object"} + dns: + # @schema {"name": "client_config.dial_option.net.dns.cache_enabled", "type": "boolean"} + # client_config.dial_option.net.dns.cache_enabled -- gRPC client TCP DNS cache enabled + cache_enabled: true + # @schema {"name": "client_config.dial_option.net.dns.refresh_duration", "type": "string"} + # client_config.dial_option.net.dns.refresh_duration -- gRPC client TCP DNS cache refresh duration + refresh_duration: 30m + # @schema {"name": "client_config.dial_option.net.dns.cache_expiration", "type": "string"} + # client_config.dial_option.net.dns.cache_expiration -- gRPC client TCP DNS cache expiration + cache_expiration: 1h + # @schema {"name": "client_config.dial_option.net.dialer", "type": "object"} + dialer: + # @schema {"name": "client_config.dial_option.net.dialer.timeout", "type": "string"} + # client_config.dial_option.net.dialer.timeout -- gRPC client TCP dialer timeout + timeout: "" + # @schema {"name": "client_config.dial_option.net.dialer.keepalive", "type": "string"} + # client_config.dial_option.net.dialer.keepalive -- gRPC client TCP dialer keep alive + keepalive: "" + # @schema {"name": "client_config.dial_option.net.dialer.dual_stack_enabled", "type": "boolean"} + # client_config.dial_option.net.dialer.dual_stack_enabled -- gRPC client TCP dialer dual stack enabled + dual_stack_enabled: true + # @schema {"name": "client_config.dial_option.net.tls", "type": "object"} + tls: + # @schema {"name": "client_config.dial_option.net.tls.enabled", "type": "boolean"} + # client_config.tls.enabled -- TLS enabled + enabled: false + # @schema {"name": "client_config.dial_option.net.tls.cert", "type": "string"} + # client_config.tls.cert -- TLS cert path + cert: /path/to/cert + # @schema {"name": "client_config.dial_option.net.tls.key", "type": "string"} + # client_config.tls.key -- TLS key path + key: /path/to/key + # @schema {"name": "client_config.dial_option.net.tls.ca", "type": "string"} + # client_config.tls.ca -- TLS ca path + ca: /path/to/ca + # @schema {"name": "client_config.dial_option.net.tls.insecure_skip_verify", "type": "boolean"} + # client_config.tls.insecure_skip_verify -- enable/disable skip SSL certificate verification + insecure_skip_verify: false + # @schema {"name": "client_config.dial_option.net.socket_option", "type": "object"} + socket_option: + # @schema {"name": "client_config.dial_option.net.socket_option.reuse_port", "type": "boolean"} + # client_config.dial_option.net.socket_option.reuse_port -- server listen socket option for reuse_port functionality + reuse_port: true + # @schema {"name": "client_config.dial_option.net.socket_option.reuse_addr", "type": "boolean"} + # client_config.dial_option.net.socket_option.reuse_addr -- server listen socket option for reuse_addr functionality + reuse_addr: true + # @schema {"name": "client_config.dial_option.net.socket_option.tcp_fast_open", "type": "boolean"} + # client_config.dial_option.net.socket_option.tcp_fast_open -- server listen socket option for tcp_fast_open functionality + tcp_fast_open: true + # @schema {"name": "client_config.dial_option.net.socket_option.tcp_no_delay", "type": "boolean"} + # client_config.dial_option.net.socket_option.tcp_no_delay -- server listen socket option for tcp_no_delay functionality + tcp_no_delay: true + # @schema {"name": "client_config.dial_option.net.socket_option.tcp_cork", "type": "boolean"} + # client_config.dial_option.net.socket_option.tcp_cork -- server listen socket option for tcp_cork functionality + tcp_cork: false + # @schema {"name": "client_config.dial_option.net.socket_option.tcp_quick_ack", "type": "boolean"} + # client_config.dial_option.net.socket_option.tcp_quick_ack -- server listen socket option for tcp_quick_ack functionality + tcp_quick_ack: true + # @schema {"name": "client_config.dial_option.net.socket_option.tcp_defer_accept", "type": "boolean"} + # client_config.dial_option.net.socket_option.tcp_defer_accept -- server listen socket option for tcp_defer_accept functionality + tcp_defer_accept: true + # @schema {"name": "client_config.dial_option.net.socket_option.ip_transparent", "type": "boolean"} + # client_config.dial_option.net.socket_option.ip_transparent -- server listen socket option for ip_transparent functionality + ip_transparent: false + # @schema {"name": "client_config.dial_option.net.socket_option.ip_recover_destination_addr", "type": "boolean"} + # client_config.dial_option.net.socket_option.ip_recover_destination_addr -- server listen socket option for ip_recover_destination_addr functionality + ip_recover_destination_addr: false + # @schema {"name": "client_config.dial_option.keepalive", "type": "object"} + keepalive: + # @schema {"name": "client_config.dial_option.keepalive.time", "type": "string"} + # client_config.dial_option.keepalive.time -- gRPC client keep alive time + time: "120s" + # @schema {"name": "client_config.dial_option.keepalive.timeout", "type": "string"} + # client_config.dial_option.keepalive.timeout -- gRPC client keep alive timeout + timeout: "30s" + # @schema {"name": "client_config.dial_option.keepalive.permit_without_stream", "type": "boolean"} + # client_config.dial_option.keepalive.permit_without_stream -- gRPC client keep alive permit without stream + permit_without_stream: true + # @schema {"name": "client_config.tls", "type": "object"} + tls: + # @schema {"name": "client_config.tls.enabled", "type": "boolean"} + # client_config.tls.enabled -- TLS enabled + enabled: false + # @schema {"name": "client_config.tls.cert", "type": "string"} + # client_config.tls.cert -- TLS cert path + cert: /path/to/cert + # @schema {"name": "client_config.tls.key", "type": "string"} + # client_config.tls.key -- TLS key path + key: /path/to/key + # @schema {"name": "client_config.tls.ca", "type": "string"} + # client_config.tls.ca -- TLS ca path + ca: /path/to/ca + # @schema {"name": "client_config.tls.insecure_skip_verify", "type": "boolean"} + # client_config.tls.insecure_skip_verify -- enable/disable skip SSL certificate verification + insecure_skip_verify: false +# @schema {"name": "global_config", "type": "object"} +global_config: + # @schema {"name": "global_config.version", "type": "string", "default": "v0.0.1"} + # version -- version info + version: "v0.0.1" + # @schema {"name": "global_config.time_zone", "type": "string"} + # time_zone -- Time zone + time_zone: UTC + # @schema {"name": "global_config.logging", "type": "object", "anchor": "logging"} + logging: + # @schema {"name": "global_config.logging.logger", "type": "string", "enum": ["glg", "zap"]} + # logging.logger -- logger name. + # currently logger must be `glg` or `zap`. + logger: glg + # @schema {"name": "global_config.logging.level", "type": "string", "enum": ["debug", "info", "warn", "error", "fatal"]} + # logging.level -- logging level. + # logging level must be `debug`, `info`, `warn`, `error` or `fatal`. + level: debug + # @schema {"name": "global_config.logging.format", "type": "string", "enum": ["raw", "json"]} + # logging.format -- logging format. + # logging format must be `raw` or `json` + format: raw +# @schema {"name": "server_config", "type": "object", "anchor": "server_config"} +server_config: + # @schema {"name": "server_config.servers", "type": "object"} + servers: + # @schema {"name": "server_config.servers.rest", "type": "object"} + rest: + # @schema {"name": "server_config.servers.rest.enabled", "type": "boolean"} + # server_config.servers.rest.enabled -- REST server enabled + enabled: false + # @schema {"name": "server_config.servers.rest.host", "type": "string"} + # server_config.servers.rest.host -- REST server host + host: 0.0.0.0 + # @schema {"name": "server_config.servers.rest.port", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.servers.rest.port -- REST server port + port: 8080 + # @schema {"name": "server_config.servers.rest.servicePort", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.servers.rest.servicePort -- REST server service port + servicePort: 8080 + # @schema {"name": "server_config.servers.rest.server", "type": "object"} + server: + # @schema {"name": "server_config.servers.rest.server.mode", "type": "string"} + # server_config.servers.rest.server.mode -- REST server server mode + mode: REST + # @schema {"name": "server_config.servers.rest.server.probe_wait_time", "type": "string"} + # server_config.servers.rest.server.probe_wait_time -- REST server probe wait time + probe_wait_time: 3s + # @schema {"name": "server_config.servers.rest.server.network", "type": "string", "enum": ["tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "unix", "unixgram", "unixpacket"]} + # server_config.servers.rest.server.network -- mysql network + network: tcp + # @schema {"name": "server_config.servers.rest.server.socket_path", "type": "string"} + # server_config.servers.rest.server.socket_path -- mysql socket_path + socket_path: "" + # @schema {"name": "server_config.servers.rest.server.http", "type": "object"} + http: + # @schema {"name": "server_config.servers.rest.server.http.shutdown_duration", "type": "string"} + # server_config.servers.rest.server.http.shutdown_duration -- REST server shutdown duration + shutdown_duration: 5s + # @schema {"name": "server_config.servers.rest.server.http.handler_timeout", "type": "string"} + # server_config.servers.rest.server.http.handler_timeout -- REST server handler timeout + handler_timeout: 5s + # @schema {"name": "server_config.servers.rest.server.http.idle_timeout", "type": "string"} + # server_config.servers.rest.server.http.idle_timeout -- REST server idle timeout + idle_timeout: 2s + # @schema {"name": "server_config.servers.rest.server.http.read_header_timeout", "type": "string"} + # server_config.servers.rest.server.http.read_header_timeout -- REST server read header timeout + read_header_timeout: 1s + # @schema {"name": "server_config.servers.rest.server.http.read_timeout", "type": "string"} + # server_config.servers.rest.server.http.read_timeout -- REST server read timeout + read_timeout: 1s + # @schema {"name": "server_config.servers.rest.server.http.write_timeout", "type": "string"} + # server_config.servers.rest.server.http.write_timeout -- REST server write timeout + write_timeout: 1s + # @schema {"name": "server_config.servers.rest.server.socket_option", "type": "object", "anchor": "socket_option"} + socket_option: + # @schema {"name": "server_config.servers.rest.server.socket_option.reuse_port", "type": "boolean"} + # server_config.servers.rest.server.socket_option.reuse_port -- server listen socket option for reuse_port functionality + reuse_port: true + # @schema {"name": "server_config.servers.rest.server.socket_option.reuse_addr", "type": "boolean"} + # server_config.servers.rest.server.socket_option.reuse_addr -- server listen socket option for reuse_addr functionality + reuse_addr: true + # @schema {"name": "server_config.servers.rest.server.socket_option.tcp_fast_open", "type": "boolean"} + # server_config.servers.rest.server.socket_option.tcp_fast_open -- server listen socket option for tcp_fast_open functionality + tcp_fast_open: true + # @schema {"name": "server_config.servers.rest.server.socket_option.tcp_no_delay", "type": "boolean"} + # server_config.servers.rest.server.socket_option.tcp_no_delay -- server listen socket option for tcp_no_delay functionality + tcp_no_delay: true + # @schema {"name": "server_config.servers.rest.server.socket_option.tcp_cork", "type": "boolean"} + # server_config.servers.rest.server.socket_option.tcp_cork -- server listen socket option for tcp_cork functionality + tcp_cork: false + # @schema {"name": "server_config.servers.rest.server.socket_option.tcp_quick_ack", "type": "boolean"} + # server_config.servers.rest.server.socket_option.tcp_quick_ack -- server listen socket option for tcp_quick_ack functionality + tcp_quick_ack: true + # @schema {"name": "server_config.servers.rest.server.socket_option.tcp_defer_accept", "type": "boolean"} + # server_config.servers.rest.server.socket_option.tcp_defer_accept -- server listen socket option for tcp_defer_accept functionality + tcp_defer_accept: true + # @schema {"name": "server_config.servers.rest.server.socket_option.ip_transparent", "type": "boolean"} + # server_config.servers.rest.server.socket_option.ip_transparent -- server listen socket option for ip_transparent functionality + ip_transparent: false + # @schema {"name": "server_config.servers.rest.server.socket_option.ip_recover_destination_addr", "type": "boolean"} + # server_config.servers.rest.server.socket_option.ip_recover_destination_addr -- server listen socket option for ip_recover_destination_addr functionality + ip_recover_destination_addr: false + # @schema {"name": "server_config.servers.grpc", "type": "object"} + grpc: + # @schema {"name": "server_config.servers.grpc.enabled", "type": "boolean"} + # server_config.servers.grpc.enabled -- gRPC server enabled + enabled: true + # @schema {"name": "server_config.servers.grpc.host", "type": "string"} + # server_config.servers.grpc.host -- gRPC server host + host: 0.0.0.0 + # @schema {"name": "server_config.servers.grpc.port", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.servers.grpc.port -- gRPC server port + port: 8081 + # @schema {"name": "server_config.servers.grpc.servicePort", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.servers.grpc.servicePort -- gRPC server service port + servicePort: 8081 + # @schema {"name": "server_config.servers.grpc.server", "type": "object"} + server: + # @schema {"name": "server_config.servers.grpc.server.mode", "type": "string"} + # server_config.servers.grpc.server.mode -- gRPC server server mode + mode: GRPC + # @schema {"name": "server_config.servers.grpc.server.probe_wait_time", "type": "string"} + # server_config.servers.grpc.server.probe_wait_time -- gRPC server probe wait time + probe_wait_time: "3s" + # @schema {"name": "server_config.servers.grpc.server.network", "type": "string", "enum": ["tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "unix", "unixgram", "unixpacket"]} + # server_config.servers.grpc.server.network -- mysql network + network: tcp + # @schema {"name": "server_config.servers.grpc.server.socket_path", "type": "string"} + # server_config.servers.grpc.server.socket_path -- mysql socket_path + socket_path: "" + # @schema {"name": "server_config.servers.grpc.server.grpc", "type": "object"} + grpc: + # @schema {"name": "server_config.servers.grpc.server.grpc.bidirectional_stream_concurrency", "type": "integer"} + # server_config.servers.grpc.server.grpc.bidirectional_stream_concurrency -- gRPC server bidirectional stream concurrency + bidirectional_stream_concurrency: 20 + # @schema {"name": "server_config.servers.grpc.server.grpc.max_receive_message_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.max_receive_message_size -- gRPC server max receive message size + max_receive_message_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.max_send_message_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.max_send_message_size -- gRPC server max send message size + max_send_message_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.initial_window_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.initial_window_size -- gRPC server initial window size + initial_window_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.initial_conn_window_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.initial_conn_window_size -- gRPC server initial connection window size + initial_conn_window_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive", "type": "object"} + keepalive: + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.max_conn_idle", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.max_conn_idle -- gRPC server keep alive max connection idle + max_conn_idle: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.max_conn_age", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.max_conn_age -- gRPC server keep alive max connection age + max_conn_age: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.max_conn_age_grace", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.max_conn_age_grace -- gRPC server keep alive max connection age grace + max_conn_age_grace: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.time", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.time -- gRPC server keep alive time + time: "120s" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.timeout", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.timeout -- gRPC server keep alive timeout + timeout: "30s" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.min_time", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.min_time -- gRPC server keep alive min_time + min_time: "60s" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.permit_without_stream", "type": "boolean"} + # server_config.servers.grpc.server.grpc.keepalive.permit_without_stream -- gRPC server keep alive permit_without_stream + permit_without_stream: true + # @schema {"name": "server_config.servers.grpc.server.grpc.write_buffer_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.write_buffer_size -- gRPC server write buffer size + write_buffer_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.read_buffer_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.read_buffer_size -- gRPC server read buffer size + read_buffer_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.connection_timeout", "type": "string"} + # server_config.servers.grpc.server.grpc.connection_timeout -- gRPC server connection timeout + connection_timeout: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.max_header_list_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.max_header_list_size -- gRPC server max header list size + max_header_list_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.header_table_size", "type": "integer"} + # server_config.servers.grpc.server.grpc.header_table_size -- gRPC server header table size + header_table_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.interceptors", "type": "array", "items": {"type": "string", "enum": ["RecoverInterceptor", "AccessLogInterceptor", "TraceInterceptor", "MetricInterceptor"]}} + # server_config.servers.grpc.server.grpc.interceptors -- gRPC server interceptors + interceptors: + - "RecoverInterceptor" + # @schema {"name": "server_config.servers.grpc.server.grpc.enable_reflection", "type": "boolean"} + # server_config.servers.grpc.server.grpc.enable_reflection -- gRPC server reflection option + enable_reflection: true + # @schema {"name": "server_config.servers.grpc.server.socket_option", "alias": "socket_option"} + socket_option: + # server_config.servers.grpc.server.socket_option.reuse_port -- server listen socket option for reuse_port functionality + reuse_port: true + # server_config.servers.grpc.server.socket_option.reuse_addr -- server listen socket option for reuse_addr functionality + reuse_addr: true + # server_config.servers.grpc.server.socket_option.tcp_fast_open -- server listen socket option for tcp_fast_open functionality + tcp_fast_open: true + # server_config.servers.grpc.server.socket_option.tcp_no_delay -- server listen socket option for tcp_no_delay functionality + tcp_no_delay: true + # server_config.servers.grpc.server.socket_option.tcp_cork -- server listen socket option for tcp_cork functionality + tcp_cork: false + # server_config.servers.grpc.server.socket_option.tcp_quick_ack -- server listen socket option for tcp_quick_ack functionality + tcp_quick_ack: true + # server_config.servers.grpc.server.socket_option.tcp_defer_accept -- server listen socket option for tcp_defer_accept functionality + tcp_defer_accept: true + # server_config.servers.grpc.server.socket_option.ip_transparent -- server listen socket option for ip_transparent functionality + ip_transparent: false + # server_config.servers.grpc.server.socket_option.ip_recover_destination_addr -- server listen socket option for ip_recover_destination_addr functionality + ip_recover_destination_addr: false + # @schema {"name": "server_config.servers.grpc.server.restart", "type": "boolean"} + # server_config.servers.grpc.server.restart -- gRPC server restart + restart: true + # @schema {"name": "server_config.healths", "type": "object"} + healths: + # @schema {"name": "server_config.healths.startup", "type": "object"} + startup: + # @schema {"name": "server_config.healths.startup.enabled", "type": "boolean"} + # server_config.healths.startup.enabled -- startup server enabled + enabled: true + # @schema {"name": "server_config.healths.startup.port", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.healths.startup.port -- startup server port + port: 3000 + # @schema {"name": "server_config.healths.startup.startupProbe", "type": "object"} + startupProbe: + # @schema {"name": "server_config.healths.startup.startupProbe.httpGet", "type": "object"} + httpGet: + # @schema {"name": "server_config.healths.startup.startupProbe.httpGet.path", "type": "string"} + # server_config.healths.startup.startupProbe.httpGet.path -- startup probe path + path: /liveness + # @schema {"name": "server_config.healths.startup.startupProbe.httpGet.port", "type": "string"} + # server_config.healths.startup.startupProbe.httpGet.port -- startup probe port + port: liveness + # @schema {"name": "server_config.healths.startup.startupProbe.httpGet.scheme", "type": "string"} + # server_config.healths.startup.startupProbe.httpGet.scheme -- startup probe scheme + scheme: HTTP + # @schema {"name": "server_config.healths.startup.startupProbe.initialDelaySeconds", "type": "integer"} + # server_config.healths.startup.startupProbe.initialDelaySeconds -- startup probe initial delay seconds + initialDelaySeconds: 5 + # @schema {"name": "server_config.healths.startup.startupProbe.timeoutSeconds", "type": "integer"} + # server_config.healths.startup.startupProbe.timeoutSeconds -- startup probe timeout seconds + timeoutSeconds: 2 + # @schema {"name": "server_config.healths.startup.startupProbe.successThreshold", "type": "integer"} + # server_config.healths.startup.startupProbe.successThreshold -- startup probe success threshold + successThreshold: 1 + # @schema {"name": "server_config.healths.startup.startupProbe.failureThreshold", "type": "integer"} + # server_config.healths.startup.startupProbe.failureThreshold -- startup probe failure threshold + failureThreshold: 30 + # @schema {"name": "server_config.healths.startup.startupProbe.periodSeconds", "type": "integer"} + # server_config.healths.startup.startupProbe.periodSeconds -- startup probe period seconds + periodSeconds: 5 + # @schema {"name": "server_config.healths.liveness", "type": "object"} + liveness: + # @schema {"name": "server_config.healths.liveness.enabled", "type": "boolean"} + # server_config.healths.liveness.enabled -- liveness server enabled + enabled: true + # @schema {"name": "server_config.healths.liveness.host", "type": "string"} + # server_config.healths.liveness.host -- liveness server host + host: 0.0.0.0 + # @schema {"name": "server_config.healths.liveness.port", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.healths.liveness.port -- liveness server port + port: 3000 + # @schema {"name": "server_config.healths.liveness.servicePort", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.healths.liveness.servicePort -- liveness server service port + servicePort: 3000 + # @schema {"name": "server_config.healths.liveness.livenessProbe", "type": "object"} + livenessProbe: + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet", "type": "object"} + httpGet: + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet.path", "type": "string"} + # server_config.healths.liveness.livenessProbe.httpGet.path -- liveness probe path + path: /liveness + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet.port", "type": "string"} + # server_config.healths.liveness.livenessProbe.httpGet.port -- liveness probe port + port: liveness + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet.scheme", "type": "string"} + # server_config.healths.liveness.livenessProbe.httpGet.scheme -- liveness probe scheme + scheme: HTTP + # @schema {"name": "server_config.healths.liveness.livenessProbe.initialDelaySeconds", "type": "integer"} + # server_config.healths.liveness.livenessProbe.initialDelaySeconds -- liveness probe initial delay seconds + initialDelaySeconds: 5 + # @schema {"name": "server_config.healths.liveness.livenessProbe.timeoutSeconds", "type": "integer"} + # server_config.healths.liveness.livenessProbe.timeoutSeconds -- liveness probe timeout seconds + timeoutSeconds: 2 + # @schema {"name": "server_config.healths.liveness.livenessProbe.successThreshold", "type": "integer"} + # server_config.healths.liveness.livenessProbe.successThreshold -- liveness probe success threshold + successThreshold: 1 + # @schema {"name": "server_config.healths.liveness.livenessProbe.failureThreshold", "type": "integer"} + # server_config.healths.liveness.livenessProbe.failureThreshold -- liveness probe failure threshold + failureThreshold: 2 + # @schema {"name": "server_config.healths.liveness.livenessProbe.periodSeconds", "type": "integer"} + # server_config.healths.liveness.livenessProbe.periodSeconds -- liveness probe period seconds + periodSeconds: 3 + # @schema {"name": "server_config.healths.liveness.server", "type": "object"} + server: + # @schema {"name": "server_config.healths.liveness.server.mode", "type": "string"} + # server_config.healths.liveness.server.mode -- liveness server mode + mode: "" + # @schema {"name": "server_config.healths.liveness.server.probe_wait_time", "type": "string"} + # server_config.healths.liveness.server.probe_wait_time -- liveness server probe wait time + probe_wait_time: "3s" + # @schema {"name": "server_config.healths.liveness.server.network", "type": "string", "enum": ["tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "unix", "unixgram", "unixpacket"]} + # server_config.healths.liveness.server.network -- mysql network + network: tcp + # @schema {"name": "server_config.healths.liveness.server.socket_path", "type": "string"} + # server_config.healths.liveness.server.socket_path -- mysql socket_path + socket_path: "" + # @schema {"name": "server_config.healths.liveness.server.http", "type": "object"} + http: + # @schema {"name": "server_config.healths.liveness.server.http.shutdown_duration", "type": "string"} + # server_config.healths.liveness.server.http.shutdown_duration -- liveness server shutdown duration + shutdown_duration: "5s" + # @schema {"name": "server_config.healths.liveness.server.http.handler_timeout", "type": "string"} + # server_config.healths.liveness.server.http.handler_timeout -- liveness server handler timeout + handler_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.idle_timeout", "type": "string"} + # server_config.healths.liveness.server.http.idle_timeout -- liveness server idle timeout + idle_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.read_header_timeout", "type": "string"} + # server_config.healths.liveness.server.http.read_header_timeout -- liveness server read header timeout + read_header_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.read_timeout", "type": "string"} + # server_config.healths.liveness.server.http.read_timeout -- liveness server read timeout + read_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.write_timeout", "type": "string"} + # server_config.healths.liveness.server.http.write_timeout -- liveness server write timeout + write_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.socket_option", "alias": "socket_option"} + socket_option: + # server_config.healths.liveness.server.socket_option.reuse_port -- server listen socket option for reuse_port functionality + reuse_port: true + # server_config.healths.liveness.server.socket_option.reuse_addr -- server listen socket option for reuse_addr functionality + reuse_addr: true + # server_config.healths.liveness.server.socket_option.tcp_fast_open -- server listen socket option for tcp_fast_open functionality + tcp_fast_open: true + # server_config.healths.liveness.server.socket_option.tcp_no_delay -- server listen socket option for tcp_no_delay functionality + tcp_no_delay: true + # server_config.healths.liveness.server.socket_option.tcp_cork -- server listen socket option for tcp_cork functionality + tcp_cork: false + # server_config.healths.liveness.server.socket_option.tcp_quick_ack -- server listen socket option for tcp_quick_ack functionality + tcp_quick_ack: true + # server_config.healths.liveness.server.socket_option.tcp_defer_accept -- server listen socket option for tcp_defer_accept functionality + tcp_defer_accept: true + # server_config.healths.liveness.server.socket_option.ip_transparent -- server listen socket option for ip_transparent functionality + ip_transparent: false + # server_config.healths.liveness.server.socket_option.ip_recover_destination_addr -- server listen socket option for ip_recover_destination_addr functionality + ip_recover_destination_addr: false + # @schema {"name": "server_config.healths.readiness", "type": "object"} + readiness: + # @schema {"name": "server_config.healths.readiness.enabled", "type": "boolean"} + # server_config.healths.readiness.enabled -- readiness server enabled + enabled: true + # @schema {"name": "server_config.healths.readiness.host", "type": "string"} + # server_config.healths.readiness.host -- readiness server host + host: 0.0.0.0 + # @schema {"name": "server_config.healths.readiness.port", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.healths.readiness.port -- readiness server port + port: 3001 + # @schema {"name": "server_config.healths.readiness.servicePort", "type": "integer", "minimum": 0, "maximum": 65535} + # server_config.healths.readiness.servicePort -- readiness server service port + servicePort: 3001 + # @schema {"name": "server_config.healths.readiness.readinessProbe", "type": "object"} + readinessProbe: + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet", "type": "object"} + httpGet: + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet.path", "type": "string"} + # server_config.healths.readiness.readinessProbe.httpGet.path -- readiness probe path + path: /readiness + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet.port", "type": "string"} + # server_config.healths.readiness.readinessProbe.httpGet.port -- readiness probe port + port: readiness + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet.scheme", "type": "string"} + # server_config.healths.readiness.readinessProbe.httpGet.scheme -- readiness probe scheme + scheme: HTTP + # @schema {"name": "server_config.healths.readiness.readinessProbe.initialDelaySeconds", "type": "integer"} + # server_config.healths.readiness.readinessProbe.initialDelaySeconds -- readiness probe initial delay seconds + initialDelaySeconds: 10 + # @schema {"name": "server_config.healths.readiness.readinessProbe.timeoutSeconds", "type": "integer"} + # server_config.healths.readiness.readinessProbe.timeoutSeconds -- readiness probe timeout seconds + timeoutSeconds: 2 + # @schema {"name": "server_config.healths.readiness.readinessProbe.successThreshold", "type": "integer"} + # server_config.healths.readiness.readinessProbe.successThreshold -- readiness probe success threshold + successThreshold: 1 + # @schema {"name": "server_config.healths.readiness.readinessProbe.failureThreshold", "type": "integer"} + # server_config.healths.readiness.readinessProbe.failureThreshold -- readiness probe failure threshold + failureThreshold: 2 + # @schema {"name": "server_config.healths.readiness.readinessProbe.periodSeconds", "type": "integer"} + # server_config.healths.readiness.readinessProbe.periodSeconds -- readiness probe period seconds + periodSeconds: 3 + # @schema {"name": "server_config.healths.readiness.server", "type": "object"} + server: + # @schema {"name": "server_config.healths.readiness.server.mode", "type": "string"} + # server_config.healths.readiness.server.mode -- readiness server mode + mode: "" + # @schema {"name": "server_config.healths.readiness.server.probe_wait_time", "type": "string"} + # server_config.healths.readiness.server.probe_wait_time -- readiness server probe wait time + probe_wait_time: "3s" + # @schema {"name": "server_config.healths.readiness.server.network", "type": "string", "enum": ["tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "unix", "unixgram", "unixpacket"]} + # server_config.healths.readiness.server.network -- mysql network + network: tcp + # @schema {"name": "server_config.healths.readiness.server.socket_path", "type": "string"} + # server_config.healths.readiness.server.socket_path -- mysql socket_path + socket_path: "" + # @schema {"name": "server_config.healths.readiness.server.http", "type": "object"} + http: + # @schema {"name": "server_config.healths.readiness.server.http.shutdown_duration", "type": "string"} + # server_config.healths.readiness.server.http.shutdown_duration -- readiness server shutdown duration + shutdown_duration: "0s" + # @schema {"name": "server_config.healths.readiness.server.http.handler_timeout", "type": "string"} + # server_config.healths.readiness.server.http.handler_timeout -- readiness server handler timeout + handler_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.idle_timeout", "type": "string"} + # server_config.healths.readiness.server.http.idle_timeout -- readiness server idle timeout + idle_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.read_header_timeout", "type": "string"} + # server_config.healths.readiness.server.http.read_header_timeout -- readiness server read header timeout + read_header_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.read_timeout", "type": "string"} + # server_config.healths.readiness.server.http.read_timeout -- readiness server read timeout + read_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.write_timeout", "type": "string"} + # server_config.healths.readiness.server.http.write_timeout -- readiness server write timeout + write_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.socket_option", "alias": "socket_option"} + socket_option: + # server_config.healths.readiness.server.socket_option.reuse_port -- server listen socket option for reuse_port functionality + reuse_port: true + # server_config.healths.readiness.server.socket_option.reuse_addr -- server listen socket option for reuse_addr functionality + reuse_addr: true + # server_config.healths.readiness.server.socket_option.tcp_fast_oepn -- server listen socket option for tcp_fast_open functionality + tcp_fast_open: true + # server_config.healths.readiness.server.socket_option.tcp_no_delay -- server listen socket option for tcp_no_delay functionality + tcp_no_delay: true + # server_config.healths.readiness.server.socket_option.tcp_cork -- server listen socket option for tcp_cork functionality + tcp_cork: false + # server_config.healths.readiness.server.socket_option.tcp_quick_ack -- server listen socket option for tcp_quick_ack functionality + tcp_quick_ack: true + # server_config.healths.readiness.server.socket_option.tcp_defer_accept -- server listen socket option for tcp_defer_accept functionality + tcp_defer_accept: true + # server_config.healths.readiness.server.socket_option.ip_transparent -- server listen socket option for ip_transparent functionality + ip_transparent: false + # server_config.healths.readiness.server.socket_option.ip_recover_destination_addr -- server listen socket option for ip_recover_destination_addr functionality + ip_recover_destination_addr: false diff --git a/charts/vald-benchmark-operator/schemas/scenario-values.yaml b/charts/vald-benchmark-operator/schemas/scenario-values.yaml new file mode 100644 index 0000000000..c1333060ab --- /dev/null +++ b/charts/vald-benchmark-operator/schemas/scenario-values.yaml @@ -0,0 +1,174 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# + +# @schema {"name": "target", "type": "object", "required": ["host", "port"]} +# target -- target cluster location +target: + # @schema {"name": "target.host", "type": "string", "minLength": 1} + # target.host -- target cluster host + host: + "vald-lb-gateway.default.svc.cluster.local" + # @schema {"name": "target.port", "type": "integer", "minimum": 0, "maximum": 65535} + # target.port -- target cluster port + port: 8081 + +# @schema {"name": "dataset", "type": "object", "required": ["name", "indexes", "group", "range"]} +# dataset -- dataset information +dataset: + # @schema {"name": "dataset.name", "type": "string", "enum": ["original", "fashion-mnist"] } + # dataset.name -- the name of dataset + name: "fashion-mnist" + # @schema {"name": "dataset.indexes", "type": "integer", "minimum": 0} + # dataset.indexes -- the amount of indexes + indexes: 1000 + # @schema {"name": "dataset.group", "type": "string", "minLength": 1} + # dataset.group -- the hdf5 group name of dataset + group: "test" + # @schema {"name": "dataset.range", "type": "object", "required": ["start", "end"]} + # dataset.range -- the data range of indexes + range: + # @schema {"name": "dataset.range.start", "type": "integer", "minimum": 1} + # dataset.range.start -- start index number + start: 1 + # @schema {"name": "dataset.range.end", "type": "integer", "minimum": 1} + # dataset.range.end -- end index number + end: 1000 + # @schema {"name": "dataset.url", "type": "string"} + # dataset.url -- the dataset url which is used for executing benchmark job with user defined hdf5 file + url: "" + +# @schema {"name": "jobs", "type": "array", "items": {"type": "object"}} +jobs: + - target: + host: "vald-lb-gateway.default.svc.cluster.local" + port: 8081 + dataset: + name: "fashion-mnist" + indexes: 1000 + group: "test" + range: + start: 1 + end: 1000 + dimension: 784 + replica: 1 + repetition: 1 + job_type: "search" + insert_config: + skip_strict_exist_check: false + timestamp: "" + update_config: + skip_strict_exist_check: false + timestamp: "" + disable_balance_update: false + upsert_config: + skip_strict_exist_check: false + timestamp: "" + disable_balance_update: false + search_config: + epsilon: 0.1 + radius: -1 + num: 10 + min_num: 10 + timeout: "10s" + enable_linear_search: true + remove_config: + skip_strict_exist_check: false + timestamp: "" + object_config: + filter_config: + host: 0.0.0.0 + port: 8081 + client_config: + addrs: [] + health_check_duration: "1s" + connection_pool: + enable_dns_resolver: true + enable_rebalance: true + rebalance_duration: 30m + size: 3 + old_conn_close_duration: "2m" + backoff: + initial_duration: 5ms + backoff_time_limit: 5s + maximum_duration: 5s + jitter_limit: 100ms + backoff_factor: 1.1 + retry_count: 100 + enable_error_log: true + circuit_breaker: + closed_error_rate: 0.7 + half_open_error_rate: 0.5 + min_samples: 1000 + open_timeout: "1s" + closed_refresh_timeout: "10s" + call_option: + wait_for_ready: true + max_retry_rpc_buffer_size: 0 + max_recv_msg_size: 0 + max_send_msg_size: 0 + dial_option: + write_buffer_size: 0 + read_buffer_size: 0 + initial_window_size: 0 + initial_connection_window_size: 0 + max_msg_size: 0 + backoff_max_delay: "120s" + backoff_base_delay: "1s" + backoff_multiplier: 1.6 + backoff_jitter: 0.2 + min_connection_timeout: "20s" + enable_backoff: false + insecure: true + timeout: "" + interceptors: [] + net: + dns: + cache_enabled: true + refresh_duration: 30m + cache_expiration: 1h + dialer: + timeout: "" + keepalive: "" + dual_stack_enabled: true + tls: + enabled: false + cert: /path/to/cert + key: /path/to/key + ca: /path/to/ca + insecure_skip_verify: false + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: true + tcp_no_delay: true + tcp_cork: false + tcp_quick_ack: true + tcp_defer_accept: true + ip_transparent: false + ip_recover_destination_addr: false + keepalive: + time: "120s" + timeout: "30s" + permit_without_stream: true + tls: + enabled: false + cert: /path/to/cert + key: /path/to/key + ca: /path/to/ca + insecure_skip_verify: false + rules: [] + rps: 1000 + ttl_seconds_after_finished: 100 diff --git a/charts/vald-benchmark-operator/templates/NOTES.txt b/charts/vald-benchmark-operator/templates/NOTES.txt new file mode 100644 index 0000000000..8dff4bbfb1 --- /dev/null +++ b/charts/vald-benchmark-operator/templates/NOTES.txt @@ -0,0 +1 @@ +Release {{ .Release.Name }} is created. diff --git a/charts/vald-benchmark-operator/templates/_helpers.tpl b/charts/vald-benchmark-operator/templates/_helpers.tpl new file mode 100644 index 0000000000..42ae6d7c72 --- /dev/null +++ b/charts/vald-benchmark-operator/templates/_helpers.tpl @@ -0,0 +1,310 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "vald.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "vald.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "vald.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "vald.labels" -}} +app.kubernetes.io/name: {{ include "vald.name" . }} +helm.sh/chart: {{ include "vald.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +joinListWithSpace +*/}} +{{- define "vald.utils.joinListWithSpace" -}} +{{- $local := dict "first" true -}} +{{- range $k, $v := . -}}{{- if not $local.first -}}{{- " " -}}{{- end -}}{{- $v -}}{{- $_ := set $local "first" false -}}{{- end -}} +{{- end -}} + +{{/* +joinListWithComma +*/}} +{{- define "vald.utils.joinListWithComma" -}} +{{- $local := dict "first" true -}} +{{- range $k, $v := . -}}{{- if not $local.first -}},{{- end -}}{{- $v -}}{{- $_ := set $local "first" false -}}{{- end -}} +{{- end -}} + +{{/* +logging settings +*/}} +{{- define "vald.logging"}} +{{- if .Values -}} +logger: {{ .Values.logger | quote }} +level: {{ .Values.level | quote }} +format: {{ .Values.format | quote }} +{{- end }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "vald.selectorLabels" -}} +app.kubernetes.io/name: {{ include "vald.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "vald.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "vald.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end -}} + +{{/* +Server configures that inserted into server_config +*/}} +{{- define "vald.servers" -}} +servers: + {{- $restEnabled := false }} + {{- if hasKey .Values.servers.rest "enabled" }} + {{- $restEnabled = .Values.servers.rest.enabled }} + {{- end }} + {{- if $restEnabled }} + - name: rest + host: {{ .Values.servers.rest.host }} + port: {{ .Values.servers.rest.port }} + {{- if .Values.servers.rest.server }} + mode: {{ .Values.servers.rest.server.mode }} + probe_wait_time: {{ .Values.servers.rest.server.probe_wait_time }} + network: {{ .Values.servers.rest.server.network | quote }} + socket_path: {{ .Values.servers.rest.server.socket_path | quote }} + http: + {{- if .Values.servers.rest.server.http }} + shutdown_duration: {{ .Values.servers.rest.server.http.shutdown_duration }} + handler_timeout: {{.Values.servers.rest.server.http.handler_timeout }} + idle_timeout: {{ .Values.servers.rest.server.http.idle_timeout }} + read_header_timeout: {{ .Values.servers.rest.server.http.read_header_timeout }} + read_timeout: {{ .Values.servers.rest.server.http.read_timeout }} + write_timeout: {{ .Values.servers.rest.server.http.write_timeout }} + {{- end }} + {{- end }} + {{- end }} + {{- $grpcEnabled := false }} + {{- if hasKey .Values.servers.grpc "enabled" }} + {{- $grpcEnabled = .Values.servers.grpc.enabled }} + {{- end }} + {{- if $grpcEnabled }} + - name: grpc + host: {{ .Values.servers.grpc.host }} + port: {{ .Values.servers.grpc.port }} + {{- if .Values.servers.grpc.server }} + mode: {{ .Values.servers.grpc.server.mode }} + probe_wait_time: {{ .Values.servers.grpc.server.probe_wait_time | quote }} + network: {{ .Values.servers.grpc.server.network | quote }} + socket_path: {{ .Values.servers.grpc.server.socket_path | quote }} + grpc: + {{- if .Values.servers.grpc.server.grpc }} + bidirectional_stream_concurrency: {{ .Values.servers.grpc.server.grpc.bidirectional_stream_concurrency }} + connection_timeout: {{ .Values.servers.grpc.server.grpc.connection_timeout | quote }} + max_receive_message_size: {{ .Values.servers.grpc.server.grpc.max_receive_message_size }} + max_send_message_size: {{ .Values.servers.grpc.server.grpc.max_send_message_size }} + initial_window_size: {{ .Values.servers.grpc.server.grpc.initial_window_size }} + initial_conn_window_size: {{ .Values.servers.grpc.server.grpc.initial_conn_window_size }} + keepalive: + {{- if .Values.servers.grpc.server.grpc.keepalive }} + max_conn_idle: {{ .Values.servers.grpc.server.grpc.keepalive.max_conn_idle | quote }} + max_conn_age: {{ .Values.servers.grpc.server.grpc.keepalive.max_conn_age | quote }} + max_conn_age_grace: {{ .Values.servers.grpc.server.grpc.keepalive.max_conn_age_grace | quote }} + time: {{ .Values.servers.grpc.server.grpc.keepalive.time | quote }} + timeout: {{ .Values.servers.grpc.server.grpc.keepalive.timeout | quote }} + min_time: {{ .Values.servers.grpc.server.grpc.keepalive.min_time | quote }} + permit_without_stream: {{ .Values.servers.grpc.server.grpc.keepalive.permit_without_stream }} + {{- end }} + write_buffer_size: {{ .Values.servers.grpc.server.grpc.write_buffer_size }} + read_buffer_size: {{ .Values.servers.grpc.server.grpc.read_buffer_size }} + max_header_list_size: {{ .Values.servers.grpc.server.grpc.max_header_list_size }} + header_table_size: {{ .Values.servers.grpc.server.grpc.header_table_size }} + {{- if .Values.servers.grpc.server.grpc.interceptors }} + interceptors: + {{- toYaml .Values.servers.grpc.server.grpc.interceptors | nindent 8 }} + {{- else }} + interceptors: [] + {{- end }} + enable_reflection: {{ .Values.servers.grpc.server.grpc.enable_reflection }} + {{- end }} + restart: {{ .Values.servers.grpc.server.restart }} + {{- end }} + {{- end }} +health_check_servers: + {{- $livenessEnabled := false }} + {{- if hasKey .Values.healths.liveness "enabled" }} + {{- $livenessEnabled = .Values.healths.liveness.enabled }} + {{- end }} + {{- if $livenessEnabled }} + - name: liveness + host: {{ .Values.healths.liveness.host }} + port: {{ .Values.healths.liveness.port }} + {{- if .Values.healths.liveness.server }} + mode: {{ .Values.healths.liveness.server.mode | quote }} + probe_wait_time: {{ .Values.healths.liveness.server.probe_wait_time | quote }} + network: {{ .Values.healths.liveness.server.network | quote }} + socket_path: {{ .Values.healths.liveness.server.socket_path | quote }} + http: + {{- if .Values.healths.liveness.server.http }} + shutdown_duration: {{ .Values.healths.liveness.server.http.shutdown_duration | quote }} + handler_timeout: {{ .Values.healths.liveness.server.http.handler_timeout | quote }} + idle_timeout: {{ .Values.healths.liveness.server.http.idle_timeout | quote }} + read_header_timeout: {{ .Values.healths.liveness.server.http.read_header_timeout | quote }} + read_timeout: {{ .Values.healths.liveness.server.http.read_timeout | quote }} + write_timeout: {{ .Values.healths.liveness.server.http.write_timeout | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- $readinessEnabled := false }} + {{- if hasKey .Values.healths.readiness "enabled" }} + {{- $readinessEnabled = .Values.healths.readiness.enabled }} + {{- end }} + {{- if $readinessEnabled }} + - name: readiness + host: {{ .Values.healths.readiness.host }} + port: {{ .Values.healths.readiness.port }} + {{- if .Values.healths.readiness.server }} + mode: {{ .Values.healths.readiness.server.mode | quote }} + probe_wait_time: {{ .Values.healths.readiness.server.probe_wait_time | quote }} + network: {{ .Values.healths.readiness.server.network | quote }} + socket_path: {{ .Values.healths.readiness.server.socket_path | quote }} + http: + {{- if .Values.healths.readiness.server.http }} + shutdown_duration: {{ .Values.healths.readiness.server.http.shutdown_duration | quote }} + handler_timeout: {{ .Values.healths.readiness.server.http.handler_timeout | quote }} + idle_timeout: {{ .Values.healths.readiness.server.http.idle_timeout | quote }} + read_header_timeout: {{ .Values.healths.readiness.server.http.read_header_timeout | quote }} + read_timeout: {{ .Values.healths.readiness.server.http.read_timeout | quote }} + write_timeout: {{ .Values.healths.readiness.server.http.write_timeout | quote }} + {{- end }} + {{- end }} + {{- end }} +metrics_servers: + {{- $pprofEnabled := false }} + {{- if hasKey .Values.metrics.pprof "enabled" }} + {{- $pprofEnabled = .Values.metrics.pprof.enabled }} + {{- end }} + {{- if $pprofEnabled }} + - name: pprof + host: {{ .Values.metrics.pprof.host }} + port: {{ .Values.metrics.pprof.port }} + {{- if .Values.metrics.pprof.server }} + mode: {{ .Values.metrics.pprof.server.mode }} + probe_wait_time: {{ .Values.metrics.pprof.server.probe_wait_time }} + network: {{ .Values.metrics.pprof.server.network | quote }} + socket_path: {{ .Values.metrics.pprof.server.socket_path | quote }} + http: + {{- if .Values.metrics.pprof.server.http }} + shutdown_duration: {{ .Values.metrics.pprof.server.http.shutdown_duration }} + handler_timeout: {{ .Values.metrics.pprof.server.http.handler_timeout }} + idle_timeout: {{ .Values.metrics.pprof.server.http.idle_timeout }} + read_header_timeout: {{ .Values.metrics.pprof.server.http.read_header_timeout }} + read_timeout: {{ .Values.metrics.pprof.server.http.read_timeout }} + write_timeout: {{ .Values.metrics.pprof.server.http.write_timeout }} + {{- end }} + {{- end }} + {{- end }} +startup_strategy: + {{- if $livenessEnabled }} + - liveness + {{- end }} + {{- if $pprofEnabled }} + - pprof + {{- end }} + {{- if $grpcEnabled }} + - grpc + {{- end }} + {{- if $restEnabled }} + - rest + {{- end }} + {{- if $readinessEnabled }} + - readiness + {{- end }} +full_shutdown_duration: {{ .Values.full_shutdown_duration }} +tls: + {{- if .Values.tls }} + enabled: {{ .Values.tls.enabled }} + cert: {{ .Values.tls.cert | quote }} + key: {{ .Values.tls.key | quote }} + ca: {{ .Values.tls.ca | quote }} + insecure_skip_verify: {{ .Values.tls.insecure_skip_verify }} + {{- end }} +{{- end -}} + +{{/* +observability +*/}} +{{- define "vald.observability" -}} +enabled: {{ .Values.enabled }} +otlp: + {{- if .Values.otlp }} + collector_endpoint: {{ .Values.otlp.collector_endpoint | quote }} + trace_batch_timeout: {{ .Values.otlp.trace_batch_timeout | quote }} + trace_export_timeout: {{ .Values.otlp.trace_export_timeout | quote }} + trace_max_export_batch_size: {{ .Values.otlp.trace_max_export_batch_size }} + trace_max_queue_size: {{ .Values.otlp.trace_max_queue_size }} + metrics_export_interval: {{ .Values.otlp.metrics_export_interval | quote }} + metrics_export_timeout: {{ .Values.otlp.metrics_export_timeout | quote }} + attribute: + {{- if .Values.otlp.attribute }} + namespace: {{ .Values.otlp.attribute.namespace | quote }} + pod_name: {{ .Values.otlp.attribute.pod_name | quote }} + node_name: {{ .Values.otlp.attribute.node_name | quote }} + service_name: {{ .Values.otlp.attribute.service_name | quote }} + {{- end }} + {{- end }} +metrics: + {{- if .Values.metrics }} + enable_version_info: {{ .Values.metrics.enable_version_info }} + {{- if .Values.metrics.version_info_labels }} + version_info_labels: + {{- toYaml .Values.metrics.version_info_labels | nindent 4 }} + {{- else }} + version_info_labels: [] + {{- end }} + enable_memory: {{ .Values.metrics.enable_memory }} + enable_goroutine: {{ .Values.metrics.enable_goroutine }} + enable_cgo: {{ .Values.metrics.enable_cgo }} + {{- end }} +trace: + {{- if .Values.trace }} + enabled: {{ .Values.trace.enabled }} + sampling_rate: {{ .Values.trace.sampling_rate }} + {{- end }} +{{- end -}} + diff --git a/charts/vald-benchmark-operator/templates/clusterrole.yaml b/charts/vald-benchmark-operator/templates/clusterrole.yaml new file mode 100644 index 0000000000..505aed2ffe --- /dev/null +++ b/charts/vald-benchmark-operator/templates/clusterrole.yaml @@ -0,0 +1,116 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: {{ .Values.rbac.name }} + namespace: {{ .Release.Namespace }} +rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkscenarios + - valdbenchmarkscenario + - valdbenchmarkjob + - valdbenchmarkjobs + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkscenarios/finalizers + - valdbenchmarkscenario/finalizers + - valdbenchmarkjob/finalizers + - valdbenchmarkjobs/finalizers + verbs: + - update + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkscenario/status + - valdbenchmarkscenarios/status + - valdbenchmarkjob/status + - valdbenchmarkjobs/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - update + - apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +{{- end }} diff --git a/charts/vald-benchmark-operator/templates/clusterrolebinding.yaml b/charts/vald-benchmark-operator/templates/clusterrolebinding.yaml new file mode 100644 index 0000000000..4593c64de3 --- /dev/null +++ b/charts/vald-benchmark-operator/templates/clusterrolebinding.yaml @@ -0,0 +1,30 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +{{- if .Values.rbac.create }} +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ .Values.serviceAccount.name }} + namespace: {{ .Release.Namespace }} +subjects: + - kind: ServiceAccount + name: {{ .Values.serviceAccount.name }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ .Values.rbac.name }} + apiGroup: rbac.authorization.k8s.io +{{- end }} diff --git a/charts/vald-benchmark-operator/templates/configmap.yaml b/charts/vald-benchmark-operator/templates/configmap.yaml new file mode 100644 index 0000000000..f2ddf91f85 --- /dev/null +++ b/charts/vald-benchmark-operator/templates/configmap.yaml @@ -0,0 +1,43 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.name }}-config + labels: + app.kubernetes.io/name: {{ include "vald.name" . }} + helm.sh/chart: {{ include "vald.chart" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/version: {{ .Chart.Version }} + app.kubernetes.io/component: benchmark-operator +data: + config.yaml: | + --- + version: {{ .Values.version }} + time_zone: {{ .Values.time_zone | quote }} + logging: + {{- $logging := dict "Values" .Values.logging }} + {{- include "vald.logging" $logging | nindent 6 }} + server_config: + {{- $servers := dict "Values" .Values.server_config }} + {{- include "vald.servers" $servers | nindent 6 }} + observability: + {{- $observability := dict "Values" .Values.observability}} + {{- include "vald.observability" $observability | nindent 6 }} + job_image: + image: "{{ .Values.job_image.repository }}:{{ .Values.job_image.tag }}" + pullPolicy: {{ .Values.job_image.pullPolicy }} diff --git a/charts/vald-benchmark-operator/templates/deployment.yaml b/charts/vald-benchmark-operator/templates/deployment.yaml new file mode 100644 index 0000000000..a1dcf4ab59 --- /dev/null +++ b/charts/vald-benchmark-operator/templates/deployment.yaml @@ -0,0 +1,166 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.name }} + namespace: {{ .Release.Namespace }} + labels: + app: {{ .Values.name }} + app.kubernetes.io/name: {{ include "vald.name" . }} + helm.sh/chart: {{ include "vald.chart" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/version: {{ .Chart.Version }} + app.kubernetes.io/component: benchmark-operator + {{- if .Values.annotations }} + annotations: + {{- toYaml .Values.annotations | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + name: {{ .Values.name }} + template: + metadata: + labels: + name: {{ .Values.name }} + app.kubernetes.io/name: {{ include "vald.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: benchmark-operator + {{- with .Values.podAnnotations }} + annotations: + {{- if .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.server_config.metrics.pprof.enabeld }} + pyroscope.io/scrape: "true" + pyroscope.io/application-name: {{ .Values.name }} + pyroscope.io/profile-cpu-enabled: "true" + pyroscope.io/profile-mem-enabled: "true" + pyroscope.io/port: "{{ .Values.server_config.metrics.pprof.port }}" + {{- end}} + {{- end }} + spec: + serviceAccountName: {{ .Values.serviceAccount.name }} + containers: + - name: {{ .Values.name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- $liveness := .Values.server_config.healths.liveness }} + {{- if $liveness.enabled }} + livenessProbe: + failureThreshold: {{ $liveness.livenessProbe.failureThreshold }} + httpGet: + path: {{ $liveness.livenessProbe.httpGet.path }} + port: {{ $liveness.livenessProbe.httpGet.port }} + scheme: {{ $liveness.livenessProbe.httpGet.scheme }} + initialDelaySeconds: {{ $liveness.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ $liveness.livenessProbe.periodSeconds }} + successThreshold: {{ $liveness.livenessProbe.successThreshold }} + timeoutSeconds: {{ $liveness.livenessProbe.timeoutSeconds }} + {{- end}} + {{- $readiness := .Values.server_config.healths.readiness }} + {{- if $readiness.enabled }} + readinessProbe: + failureThreshold: {{ $readiness.readinessProbe.failureThreshold }} + httpGet: + path: {{ $readiness.readinessProbe.httpGet.path }} + port: {{ $readiness.readinessProbe.httpGet.port }} + scheme: {{ $readiness.readinessProbe.httpGet.scheme }} + initialDelaySeconds: {{ $readiness.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ $readiness.readinessProbe.periodSeconds }} + successThreshold: {{ $readiness.readinessProbe.successThreshold }} + timeoutSeconds: {{ $readiness.readinessProbe.timeoutSeconds }} + {{- end}} + {{- $startup := .Values.server_config.healths.startup }} + {{- if $startup.enabled }} + startupProbe: + failureThreshold: {{ $startup.startupProbe.failureThreshold }} + httpGet: + path: {{ $startup.startupProbe.httpGet.path }} + port: {{ $startup.startupProbe.httpGet.port }} + scheme: {{ $startup.startupProbe.httpGet.scheme }} + initialDelaySeconds: {{ $startup.startupProbe.initialDelaySeconds }} + periodSeconds: {{ $startup.startupProbe.periodSeconds }} + successThreshold: {{ $startup.startupProbe.successThreshold }} + timeoutSeconds: {{ $startup.startupProbe.timeoutSeconds }} + {{- end}} + ports: + {{- if $liveness.enabled }} + - name: liveness + protocol: TCP + containerPort: {{ $liveness.port }} + {{- end}} + {{- if $readiness.enabled }} + - name: readiness + protocol: TCP + containerPort: {{ $readiness.port }} + {{- end}} + - name: grpc + protocol: TCP + containerPort: {{ default 8081 .Values.grpc }} + - name: pprof + protocol: TCP + containerPort: {{ default 6060 .Values.pprof }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: {{ .Values.name }}-config + mountPath: /etc/server + env: + - name: JOB_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + restartPolicy: Always + volumes: + - name: {{ .Values.name }}-config + configMap: + defaultMode: 420 + name: {{ .Values.name }}-config + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + + {{- if .Values.nodeName }} + nodeName: {{ .Values.nodeName }} + {{- end }} + {{- if .Values.nodeSelector }} + nodeSelector: + {{- toYaml .Values.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.tolerations }} + tolerations: + {{- toYaml .Values.tolerations | nindent 8 }} + {{- end }} + {{- if .Values.podPriority }} + {{- if .Values.podPriority.enabled }} + priorityClassName: {{ .Release.Namespace }}-{{ .Values.name }}-priority + {{- end }} + {{- end }} diff --git a/charts/vald-benchmark-operator/templates/service.yaml b/charts/vald-benchmark-operator/templates/service.yaml new file mode 100644 index 0000000000..851c6524de --- /dev/null +++ b/charts/vald-benchmark-operator/templates/service.yaml @@ -0,0 +1,51 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +{{- if .Values.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.name }} + {{- if .Values.service.annotations }} + annotations: + {{- toYaml .Values.service.annotations | nindent 4 }} + {{- end }} + labels: + app.kubernetes.io/name: {{ include "vald.name" . }} + helm.sh/chart: {{ include "vald.chart" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/version: {{ .Chart.Version }} + app.kubernetes.io/component: helm-operator + {{- if .Values.service.labels }} + {{- toYaml .Values.service.labels | nindent 4 }} + {{- end }} +spec: + ports: + - name: prometheus + port: {{ .Values.server_config.metrics.pprof.port }} + targetPort: {{ .Values.server_config.metrics.pprof.port }} + protocol: TCP + selector: + app.kubernetes.io/name: {{ include "vald.name" . }} + app.kubernetes.io/component: helm-operator + {{- if eq .Values.service.type "ClusterIP" }} + clusterIP: None + {{- end }} + type: {{ .Values.service.type }} + {{- if .Values.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} +{{- end }} diff --git a/charts/vald-benchmark-operator/templates/serviceaccount.yaml b/charts/vald-benchmark-operator/templates/serviceaccount.yaml new file mode 100644 index 0000000000..95475ae510 --- /dev/null +++ b/charts/vald-benchmark-operator/templates/serviceaccount.yaml @@ -0,0 +1,22 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serviceAccount.name }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/vald-benchmark-operator/values.schema.json b/charts/vald-benchmark-operator/values.schema.json new file mode 100644 index 0000000000..8e4ded670d --- /dev/null +++ b/charts/vald-benchmark-operator/values.schema.json @@ -0,0 +1,521 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "Values", + "type": "object", + "properties": { + "affinity": { "type": "object", "description": "affinity" }, + "annotations": { + "type": "object", + "description": "deployment annotations" + }, + "image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string", + "description": "image pull policy", + "enum": ["Always", "Never", "IfNotPresent"] + }, + "repository": { + "type": "string", + "description": "job image repository" + }, + "tag": { + "type": "string", + "description": "image tag for job docker image" + } + } + }, + "job_image": { + "type": "object", + "properties": { + "pullPolicy": { + "type": "string", + "enum": ["Always", "Never", "IfNotPresent"] + }, + "repository": { "type": "string" }, + "tag": { "type": "string" } + } + }, + "logging": { + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "logging format. logging format must be `raw` or `json`", + "enum": ["raw", "json"] + }, + "level": { + "type": "string", + "description": "logging level. logging level must be `debug`, `info`, `warn`, `error` or `fatal`.", + "enum": ["debug", "info", "warn", "error", "fatal"] + }, + "logger": { + "type": "string", + "description": "logger name. currently logger must be `glg` or `zap`.", + "enum": ["glg", "zap"] + } + } + }, + "name": { "type": "string", "description": "name of the deployment" }, + "nodeSelector": { + "type": "object", + "description": "node labels for pod assignment" + }, + "observability": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "otlp": { + "type": "object", + "properties": { + "attribute": { + "type": "object", + "properties": { + "metrics": { + "type": "object", + "properties": { + "enable_cgo": { "type": "boolean" }, + "enable_goroutine": { "type": "boolean" }, + "enable_memory": { "type": "boolean" }, + "enable_version_info": { "type": "boolean" }, + "version_info_labels": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "namespace": { "type": "string" }, + "node_name": { "type": "string" }, + "pod_name": { "type": "string" }, + "service_name": { "type": "string" } + } + }, + "collector_endpoint": { "type": "string" }, + "metrics_export_interval": { "type": "string" }, + "metrics_export_timeout": { "type": "string" }, + "trace_batch_timeout": { "type": "string" }, + "trace_export_timeout": { "type": "string" }, + "trace_max_export_batch_size": { "type": "integer" }, + "trace_max_queue_size": { "type": "integer" } + } + }, + "trace": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "sampling_rate": { "type": "integer" } + } + } + } + }, + "podAnnotations": { "type": "object", "description": "pod annotations" }, + "podSecurityContext": { + "type": "object", + "description": "security context for pod" + }, + "rbac": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "required roles and rolebindings will be created" + }, + "name": { + "type": "string", + "description": "name of roles and rolebindings" + } + } + }, + "replicas": { + "type": "integer", + "description": "the number of replica for deployment" + }, + "resources": { + "type": "object", + "description": "kubernetes resources of pod", + "properties": { + "limits": { "type": "object" }, + "requests": { "type": "object" } + } + }, + "securityContext": { + "type": "object", + "description": "security context for container" + }, + "server_config": { + "type": "object", + "properties": { + "full_shutdown_duration": { "type": "string" }, + "healths": { + "type": "object", + "properties": { + "liveness": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "host": { "type": "string" }, + "livenessProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer", + "description": "liveness probe failure threshold" + }, + "httpGet": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "readiness probe path" + }, + "port": { + "type": "string", + "description": "readiness probe port" + }, + "scheme": { + "type": "string", + "description": "readiness probe scheme" + } + } + }, + "initialDelaySeconds": { + "type": "integer", + "description": "liveness probe initial delay seconds" + }, + "periodSeconds": { + "type": "integer", + "description": "liveness probe period seconds" + }, + "successThreshold": { + "type": "integer", + "description": "liveness probe success threshold" + }, + "timeoutSeconds": { + "type": "integer", + "description": "liveness probe timeout seconds" + } + } + }, + "port": { "type": "integer" }, + "server": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "idle_timeout": { "type": "string" }, + "read_header_timeout": { "type": "string" }, + "read_timeout": { "type": "string" }, + "shutdown_duration": { "type": "string" }, + "timeout": { "type": "string" }, + "write_timeout": { "type": "string" } + } + }, + "mode": { "type": "string" }, + "network": { "type": "string" }, + "probe_wait_time": { "type": "string" }, + "socket_path": { "type": "string" } + } + }, + "servicePort": { "type": "integer" } + } + }, + "readiness": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "host": { "type": "string" }, + "port": { "type": "integer" }, + "readinessProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer", + "description": "readiness probe failure threshold" + }, + "httpGet": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "readiness probe path" + }, + "port": { + "type": "string", + "description": "readiness probe port" + }, + "scheme": { + "type": "string", + "description": "readiness probe scheme" + } + } + }, + "initialDelaySeconds": { + "type": "integer", + "description": "readiness probe initial delay seconds" + }, + "periodSeconds": { + "type": "integer", + "description": "readiness probe period seconds" + }, + "successThreshold": { + "type": "integer", + "description": "readiness probe success threshold" + }, + "timeoutSeconds": { + "type": "integer", + "description": "readiness probe timeout seconds" + } + } + }, + "server": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "handler_timeout": { "type": "string" }, + "idle_timeout": { "type": "string" }, + "read_header_timeout": { "type": "string" }, + "read_timeout": { "type": "string" }, + "shutdown_duration": { "type": "string" }, + "write_timeout": { "type": "string" } + } + }, + "mode": { "type": "string" }, + "network": { "type": "string" }, + "probe_wait_time": { "type": "string" }, + "socket_path": { "type": "string" } + } + }, + "servicePort": { "type": "integer" } + } + }, + "startup": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "enable startup probe." + } + } + }, + "startupProbe": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "integer", + "description": "startupProbe probe failure threshold" + }, + "httpGet": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "startup probe path" + }, + "port": { + "type": "string", + "description": "startup probe port" + }, + "scheme": { + "type": "string", + "description": "startup probe scheme" + } + } + }, + "initialDelaySeconds": { + "type": "integer", + "description": "startup probe initial delay seconds" + }, + "periodSeconds": { + "type": "integer", + "description": "startup probe period seconds" + }, + "successThreshold": { + "type": "integer", + "description": "startup probe success threshold" + }, + "timeoutSeconds": { + "type": "integer", + "description": "startup probe timeout seconds" + } + } + } + } + }, + "metrics": { + "type": "object", + "properties": { + "pprof": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "host": { "type": "string" }, + "port": { "type": "integer" }, + "server": { + "type": "object", + "properties": { + "http": { + "type": "object", + "properties": { + "handler_timeout": { "type": "string" }, + "idle_timeout": { "type": "string" }, + "read_header_timeout": { "type": "string" }, + "read_timeout": { "type": "string" }, + "shutdown_duration": { "type": "string" }, + "write_timeout": { "type": "string" } + } + }, + "mode": { "type": "string" }, + "network": { "type": "string" }, + "probe_wait_time": { "type": "string" }, + "socket_path": { "type": "string" } + } + } + } + } + } + }, + "servers": { + "type": "object", + "properties": { + "grpc": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "host": { "type": "string" }, + "name": { "type": "string" }, + "port": { "type": "integer" }, + "server": { + "type": "object", + "properties": { + "grpc": { + "type": "object", + "properties": { + "bidirectional_stream_concurrency": { + "type": "integer" + }, + "connection_timeout": { "type": "string" }, + "enable_reflection": { "type": "boolean" }, + "header_table_size": { "type": "integer" }, + "initial_conn_window_size": { "type": "integer" }, + "initial_window_size": { "type": "integer" }, + "interceptors": { + "type": "array", + "items": { "type": "string" } + }, + "keepalive": { + "type": "object", + "properties": { + "max_conn_age": { + "type": "string", + "description": "gRPC server keep alive max connection age" + }, + "max_conn_age_grace": { + "type": "string", + "description": "gRPC server keep alive max connection age grace" + }, + "max_conn_idle": { + "type": "string", + "description": "gRPC server keep alive max connection idle" + }, + "min_time": { + "type": "string", + "description": "gRPC server keep alive min_time" + }, + "permit_without_stream": { + "type": "boolean", + "description": "gRPC server keep alive permit_without_stream" + }, + "time": { + "type": "string", + "description": "gRPC server keep alive time" + }, + "timeout": { + "type": "string", + "description": "gRPC server keep alive timeout" + } + } + }, + "max_header_list_size": { "type": "integer" }, + "max_receive_message_size": { "type": "integer" }, + "max_send_msg_size": { "type": "integer" }, + "read_buffer_size": { "type": "integer" }, + "write_buffer_size": { "type": "integer" } + } + }, + "mode": { "type": "string" }, + "network": { "type": "string" }, + "probe_wait_time": { "type": "string" }, + "restart": { "type": "boolean" }, + "socket_path": { "type": "string" } + } + }, + "servicePort": { "type": "integer" } + } + }, + "rest": { + "type": "object", + "properties": { "enabled": { "type": "boolean" } } + } + } + }, + "tls": { + "type": "object", + "properties": { + "ca": { "type": "string" }, + "cert": { "type": "string" }, + "enabled": { "type": "boolean" }, + "insecure_skip_verify": { + "type": "boolean", + "description": "enable/disable skip SSL certificate verification" + }, + "key": { "type": "string" } + } + } + } + }, + "service": { + "type": "object", + "properties": { + "annotations": { + "type": "object", + "description": "service annotations" + }, + "enabled": { "type": "boolean", "description": "service enabled" }, + "externalTrafficPolicy": { + "type": "string", + "description": "external traffic policy (can be specified when service type is LoadBalancer or NodePort) : Cluster or Local" + }, + "labels": { "type": "object", "description": "service labels" }, + "type": { + "type": "string", + "description": "service type: ClusterIP, LoadBalancer or NodePort", + "enum": ["ClusterIP", "LoadBalancer", "NodePort"] + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "service account will be created" + }, + "name": { "type": "string", "description": "name of service account" } + } + }, + "time_zone": { "type": "string", "description": "time_zone" }, + "tolerations": { + "type": "array", + "description": "tolerations", + "items": { "type": "object" } + }, + "version": { + "type": "string", + "description": "version of benchmark-operator config" + } + } +} diff --git a/charts/vald-benchmark-operator/values.yaml b/charts/vald-benchmark-operator/values.yaml new file mode 100644 index 0000000000..587ac3fc50 --- /dev/null +++ b/charts/vald-benchmark-operator/values.yaml @@ -0,0 +1,508 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# + +# @schema {"name": "name", "type": "string"} +# name -- name of the deployment +name: vald-benchmark-operator + +# @schema {"name": "replicas", "type": "integer"} +# replicas -- the number of replica for deployment +replicas: 1 + +# @schema {"name": "version", "type": "string"} +# version -- version of benchmark-operator config +version: v0.0.0 + +# @schema {"name": "time_zone", "type": "string"} +# time_zone -- time_zone +time_zone: "" + +# @schema {"name": "image", "type": "object"} +image: + # @schema {"name": "image.repository", "type": "string"} + # image.repository -- image repository + repository: vdaas/vald-benchmark-operator + # @schema {"name": "image.tag", "type": "string"} + # image.tag -- image tag + tag: v1.7.5 + # @schema {"name": "image.pullPolicy", "type": "string", "enum": ["Always", "Never", "IfNotPresent"]} + # image.pullPolicy -- image pull policy + pullPolicy: Always + +# @schema {"name": "job_image", "type": "object"} +job_image: + # @schema {"name": "job_image.repository", "type": "string"} + # image.repository -- job image repository + repository: vdaas/vald-benchmark-job + # @schema {"name": "job_image.tag", "type": "string"} + # image.tag -- image tag for job docker image + tag: v1.7.5 + # @schema {"name": "job_image.pullPolicy", "type": "string", "enum": ["Always", "Never", "IfNotPresent"]} + # image.pullPolicy -- image pull policy + pullPolicy: Always + +# @schema {"name": "rbac", "type": "object"} +rbac: + # @schema {"name": "rbac.create", "type": "boolean"} + # rbac.create -- required roles and rolebindings will be created + create: true + # @schema {"name": "rbac.name", "type": "string"} + # rbac.name -- name of roles and rolebindings + name: vald-benchmark-operator + +# @schema {"name": "serviceAccount", "type": "object"} +serviceAccount: + # @schema {"name": "serviceAccount.create", "type": "boolean"} + # serviceAccount.create -- service account will be created + create: true + # @schema {"name": "serviceAccount.name", "type": "string"} + # serviceAccount.name -- name of service account + name: vald-benchmark-operator + +# @schema {"name": "service", "type": "object"} +service: + # @schema {"name": "service.enabled", "type": "boolean"} + # service.enabled -- service enabled + enabled: true + # @schema {"name": "service.annotations", "type": "object"} + # service.annotations -- service annotations + annotations: {} + # @schema {"name": "service.labels", "type": "object"} + # service.labels -- service labels + labels: {} + # @schema {"name": "service.type", "type": "string", "enum": ["ClusterIP", "LoadBalancer", "NodePort"]} + # service.type -- service type: ClusterIP, LoadBalancer or NodePort + type: ClusterIP + # @schema {"name": "service.externalTrafficPolicy", "type": "string"} + # service.externalTrafficPolicy -- external traffic policy (can be specified when service type is LoadBalancer or NodePort) : Cluster or Local + externalTrafficPolicy: "" + +# @schema {"name": "annotations", "type": "object"} +# annotations -- deployment annotations +annotations: {} + +# @schema {"name": "podAnnotations", "type": "object"} +# podAnnotations -- pod annotations +podAnnotations: {} + +# @schema {"name": "securityContext", "type": "object"} +# securityContext -- security context for container +securityContext: + runAsUser: 65532 + runAsNonRoot: true + runAsGroup: 65532 + privileged: false + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + +# @schema {"name": "podSecurityContext", "type": "object"} +# podSecurityContext -- security context for pod +podSecurityContext: + runAsUser: 65532 + runAsNonRoot: true + runAsGroup: 65532 + fsGroup: 65532 + fsGroupChangePolicy: "OnRootMismatch" + +# @schema {"name": "resources", "type": "object"} +# resources -- kubernetes resources of pod +resources: + # @schema {"name": "resources.limits", "type": "object"} + limits: + cpu: 300m + memory: 300Mi + # @schema {"name": "resources.requests", "type": "object"} + requests: + cpu: 200m + memory: 200Mi + +# @schema {"name": "nodeSelector", "type": "object"} +# nodeSelector -- node labels for pod assignment +nodeSelector: {} + +# @schema {"name": "tolerations", "type": "array", "items": {"type": "object"}} +# tolerations -- tolerations +tolerations: [] + +# @schema {"name": "affinity", "type": "object"} +# affinity -- affinity +affinity: {} + +# @schema {"name": "logging", "type": "object"} +logging: + # @schema {"name": "logging.logger", "type": "string", "enum": ["glg", "zap"]} + # logging.logger -- logger name. + # currently logger must be `glg` or `zap`. + logger: glg + # @schema {"name": "logging.level", "type": "string", "enum": ["debug", "info", "warn", "error", "fatal"]} + # logging.level -- logging level. + # logging level must be `debug`, `info`, `warn`, `error` or `fatal`. + level: debug + # @schema {"name": "logging.format", "type": "string", "enum": ["raw", "json"]} + # logging.format -- logging format. + # logging format must be `raw` or `json` + format: raw + +# @schema {"name": "server_config", "type": "object"} +server_config: + # @schema {"name": "server_config.servers", "type": "object"} + servers: + # @schema {"name": "server_config.servers.rest", "type": "object"} + rest: + # @schema {"name": "server_config.servers.rest.enabled", "type": "boolean"} + enabled: false + # @schema {"name": "server_config.servers.grpc", "type": "object"} + grpc: + # @schema {"name": "server_config.servers.grpc.enabled", "type": "boolean"} + enabled: true + # @schema {"name": "server_config.servers.grpc.name", "type": "string"} + name: grpc + # @schema {"name": "server_config.servers.grpc.host", "type": "string"} + host: 0.0.0.0 + # @schema {"name": "server_config.servers.grpc.port", "type": "integer"} + port: 8081 + # @schema {"name": "server_config.servers.grpc.servicePort", "type": "integer"} + serviecPort: 8081 + # @schema {"name": "server_config.servers.grpc.server", "type": "object"} + server: + # @schema {"name": "server_config.servers.grpc.server.mode", "type": "string"} + mode: GRPC + # @schema {"name": "server_config.servers.grpc.server.probe_wait_time", "type": "string"} + probe_wait_time: 3s + # @schema {"name": "server_config.servers.grpc.server.network", "type": "string"} + network: tcp + # @schema {"name": "server_config.servers.grpc.server.socket_path", "type": "string"} + socket_path: "" + # @schema {"name": "server_config.servers.grpc.server.grpc", "type": "object"} + grpc: + # @schema {"name": "server_config.servers.grpc.server.grpc.bidirectional_stream_concurrency", "type": "integer"} + bidirectional_stream_concurrency: 20 + # @schema {"name": "server_config.servers.grpc.server.grpc.connection_timeout", "type": "string"} + connection_timeout: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.header_table_size", "type": "integer"} + header_table_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.initial_conn_window_size", "type": "integer"} + initial_conn_window_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.initial_window_size", "type": "integer"} + initial_window_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.interceptors", "type": "array", "items": {"type": "string"}} + interceptors: [] + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive", "type": "object"} + keepalive: + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.max_conn_idle", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.max_conn_idle -- gRPC server keep alive max connection idle + max_conn_idle: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.max_conn_age", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.max_conn_age -- gRPC server keep alive max connection age + max_conn_age: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.max_conn_age_grace", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.max_conn_age_grace -- gRPC server keep alive max connection age grace + max_conn_age_grace: "" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.time", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.time -- gRPC server keep alive time + time: "120s" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.timeout", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.timeout -- gRPC server keep alive timeout + timeout: "30s" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.min_time", "type": "string"} + # server_config.servers.grpc.server.grpc.keepalive.min_time -- gRPC server keep alive min_time + min_time: "60s" + # @schema {"name": "server_config.servers.grpc.server.grpc.keepalive.permit_without_stream", "type": "boolean"} + # server_config.servers.grpc.server.grpc.keepalive.permit_without_stream -- gRPC server keep alive permit_without_stream + permit_without_stream: true + # @schema {"name": "server_config.servers.grpc.server.grpc.max_header_list_size", "type": "integer"} + max_header_list_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.max_receive_message_size", "type": "integer"} + max_receive_message_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.max_send_msg_size", "type": "integer"} + max_send_message_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.read_buffer_size", "type": "integer"} + read_buffer_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.write_buffer_size", "type": "integer"} + write_buffer_size: 0 + # @schema {"name": "server_config.servers.grpc.server.grpc.enable_reflection", "type": "boolean"} + enable_reflection: true + # @schema {"name": "server_config.servers.grpc.server.restart", "type": "boolean"} + restart: true + # @schema {"name": "server_config.healths", "type": "object"} + # health_check_servers: + healths: + # @schema {"name": "server_config.healths.liveness", "type": "object"} + liveness: + # @schema {"name": "server_config.healths.liveness.enabled", "type": "boolean"} + enabled: true + # @schema {"name": "server_config.healths.liveness.host", "type": "string"} + host: 0.0.0.0 + # @schema {"name": "server_config.healths.liveness.port", "type": "integer"} + port: 3000 + # @schema {"name": "server_config.healths.liveness.servicePort", "type": "integer"} + servicePort: 3000 + # @schema {"name": "server_config.healths.liveness.livenessProbe", "type": "object"} + livenessProbe: + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet", "type": "object"} + httpGet: + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet.path", "type": "string"} + # server_config.healths.liveness.livenessProbe.httpGet.path -- readiness probe path + path: /liveness + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet.port", "type": "string"} + # server_config.healths.liveness.livenessProbe.httpGet.port -- readiness probe port + port: liveness + # @schema {"name": "server_config.healths.liveness.livenessProbe.httpGet.scheme", "type": "string"} + # server_config.healths.liveness.livenessProbe.httpGet.scheme -- readiness probe scheme + scheme: HTTP + # @schema {"name": "server_config.healths.liveness.livenessProbe.initialDelaySeconds", "type": "integer"} + # server_config.healths.liveness.livenessProbe.initialDelaySeconds -- liveness probe initial delay seconds + initialDelaySeconds: 15 + # @schema {"name": "server_config.healths.liveness.livenessProbe.periodSeconds", "type": "integer"} + # server_config.healths.liveness.livenessProbe.periodSeconds -- liveness probe period seconds + periodSeconds: 20 + # @schema {"name": "server_config.healths.liveness.livenessProbe.successThreshold", "type": "integer"} + # server_config.healths.liveness.livenessProbe.successThreshold -- liveness probe success threshold + successThreshold: 1 + # @schema {"name": "server_config.healths.liveness.livenessProbe.failureThreshold", "type": "integer"} + # server_config.healths.liveness.livenessProbe.failureThreshold -- liveness probe failure threshold + failureThreshold: 2 + # @schema {"name": "server_config.healths.liveness.livenessProbe.timeoutSeconds", "type": "integer"} + # server_config.healths.liveness.livenessProbe.timeoutSeconds -- liveness probe timeout seconds + timeoutSeconds: 5 + # @schema {"name": "server_config.healths.liveness.server", "type": "object"} + server: + # @schema {"name": "server_config.healths.liveness.server.mode", "type": "string"} + mode: "" + # @schema {"name": "server_config.healths.liveness.server.probe_wait_time", "type": "string"} + probe_wait_time: 3s + # @schema {"name": "server_config.healths.liveness.server.network", "type": "string"} + network: tcp + # @schema {"name": "server_config.healths.liveness.server.socket_path", "type": "string"} + socket_path: "" + # @schema {"name": "server_config.healths.liveness.server.http", "type": "object"} + http: + # @schema {"name": "server_config.healths.liveness.server.http.timeout", "type": "string"} + handler_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.idle_timeout", "type": "string"} + idle_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.read_header_timeout", "type": "string"} + read_header_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.read_timeout", "type": "string"} + read_timeout: "" + # @schema {"name": "server_config.healths.liveness.server.http.shutdown_duration", "type": "string"} + shutdown_duration: 5s + # @schema {"name": "server_config.healths.liveness.server.http.write_timeout", "type": "string"} + write_timeout: "" + # @schema {"name": "server_config.healths.readiness", "type": "object"} + readiness: + # @schema {"name": "server_config.healths.readiness.enabled", "type": "boolean"} + enabled: true + # @schema {"name": "server_config.healths.readiness.host", "type": "string"} + host: 0.0.0.0 + # @schema {"name": "server_config.healths.readiness.port", "type": "integer"} + port: 3001 + # @schema {"name": "server_config.healths.readiness.servicePort", "type": "integer"} + servicePort: 3001 + # @schema {"name": "server_config.healths.readiness.readinessProbe", "type": "object"} + readinessProbe: + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet", "type": "object"} + httpGet: + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet.path", "type": "string"} + # server_config.healths.readiness.readinessProbe.httpGet.path -- readiness probe path + path: /readiness + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet.port", "type": "string"} + # server_config.healths.readiness.readinessProbe.httpGet.port -- readiness probe port + port: readiness + # @schema {"name": "server_config.healths.readiness.readinessProbe.httpGet.scheme", "type": "string"} + # server_config.healths.readiness.readinessProbe.httpGet.scheme -- readiness probe scheme + scheme: HTTP + # @schema {"name": "server_config.healths.readiness.readinessProbe.initialDelaySeconds", "type": "integer"} + # server_config.healths.readiness.readinessProbe.initialDelaySeconds -- readiness probe initial delay seconds + initialDelaySeconds: 10 + # @schema {"name": "server_config.healths.readiness.readinessProbe.periodSeconds", "type": "integer"} + # server_config.healths.readiness.readinessProbe.periodSeconds -- readiness probe period seconds + periodSeconds: 3 + # @schema {"name": "server_config.healths.readiness.readinessProbe.successThreshold", "type": "integer"} + # server_config.healths.readiness.readinessProbe.successThreshold -- readiness probe success threshold + successThreshold: 1 + # @schema {"name": "server_config.healths.readiness.readinessProbe.failureThreshold", "type": "integer"} + # server_config.healths.readiness.readinessProbe.failureThreshold --readiness probe failure threshold + failureThreshold: 2 + # @schema {"name": "server_config.healths.readiness.readinessProbe.timeoutSeconds", "type": "integer"} + # server_config.healths.readiness.readinessProbe.timeoutSeconds -- readiness probe timeout seconds + timeoutSeconds: 2 + # @schema {"name": "server_config.healths.readiness.server", "type": "object"} + server: + # @schema {"name": "server_config.healths.readiness.server.mode", "type": "string"} + mode: "" + # @schema {"name": "server_config.healths.readiness.server.probe_wait_time", "type": "string"} + probe_wait_time: 3s + # @schema {"name": "server_config.healths.readiness.server.network", "type": "string"} + network: tcp + # @schema {"name": "server_config.healths.readiness.server.socket_path", "type": "string"} + socket_path: "" + # @schema {"name": "server_config.healths.readiness.server.http", "type": "object"} + http: + # @schema {"name": "server_config.healths.readiness.server.http.handler_timeout", "type": "string"} + handler_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.idle_timeout", "type": "string"} + idle_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.read_header_timeout", "type": "string"} + read_header_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.read_timeout", "type": "string"} + read_timeout: "" + # @schema {"name": "server_config.healths.readiness.server.http.shutdown_duration", "type": "string"} + shutdown_duration: 0s + # @schema {"name": "server_config.healths.readiness.server.http.write_timeout", "type": "string"} + write_timeout: "" + # @schema {"name": "server_config.healths.startup", "type": "object"} + startup: + # @schema {"name": "server_config.healths.startup.enabled", "type": "boolean"} + # server_config.healths.startup.enabled -- enable startup probe. + enabled: true + # @schema {"name": "server_config.healths.startupProbe", "type": "object"} + startupProbe: + # @schema {"name": "server_config.healths.startupProbe.httpGet", "type": "object"} + httpGet: + # @schema {"name": "server_config.healths.startupProbe.httpGet.path", "type": "string"} + # server_config.healths.startupProbe.httpGet.path -- startup probe path + path: /liveness + # @schema {"name": "server_config.healths.startupProbe.httpGet.port", "type": "string"} + # server_config.healths.startupProbe.httpGet.port -- startup probe port + port: liveness + # @schema {"name": "server_config.healths.startupProbe.httpGet.scheme", "type": "string"} + # server_config.healths.startupProbe.httpGet.scheme -- startup probe scheme + scheme: HTTP + # @schema {"name": "server_config.healths.startupProbe.initialDelaySeconds", "type": "integer"} + # server_config.healths.startupProbe.initialDelaySeconds -- startup probe initial delay seconds + initialDelaySeconds: 5 + # @schema {"name": "server_config.healths.startupProbe.periodSeconds", "type": "integer"} + # server_config.healths.startupProbe.periodSeconds -- startup probe period seconds + periodSeconds: 5 + # @schema {"name": "server_config.healths.startupProbe.successThreshold", "type": "integer"} + # server_config.healths.startupProbe.successThreshold -- startup probe success threshold + successThreshold: 1 + # @schema {"name": "server_config.healths.startupProbe.failureThreshold", "type": "integer"} + # server_config.healths.startupProbe.failureThreshold -- startupProbe probe failure threshold + failureThreshold: 30 + # @schema {"name": "server_config.healths.startupProbe.timeoutSeconds", "type": "integer"} + # server_config.healths.startupProbe.timeoutSeconds -- startup probe timeout seconds + timeoutSeconds: 2 + # @schema {"name": "server_config.metrics", "type": "object"} + metrics: + # @schema {"name": "server_config.metrics.pprof", "type": "object"} + pprof: + # @schema {"name": "server_config.metrics.pprof.enabled", "type": "boolean"} + enabled: false + # @schema {"name": "server_config.metrics.pprof.host", "type": "string"} + host: 0.0.0.0 + # @schema {"name": "server_config.metrics.pprof.port", "type": "integer"} + port: 6060 + # @schema {"name": "server_config.metrics.pprof.server", "type": "object"} + server: + # @schema {"name": "server_config.metrics.pprof.server.mode", "type": "string"} + mode: REST + # @schema {"name": "server_config.metrics.pprof.server.probe_wait_time", "type": "string"} + probe_wait_time: 3s + # @schema {"name": "server_config.metrics.pprof.server.network", "type": "string"} + network: tcp + # @schema {"name": "server_config.metrics.pprof.server.socket_path", "type": "string"} + socket_path: "" + # @schema {"name": "server_config.metrics.pprof.server.http", "type": "object"} + http: + # @schema {"name": "server_config.metrics.pprof.server.http.handler_timeout", "type": "string"} + handler_timeout: 5s + # @schema {"name": "server_config.metrics.pprof.server.http.idle_timeout", "type": "string"} + idle_timeout: 2s + # @schema {"name": "server_config.metrics.pprof.server.http.read_header_timeout", "type": "string"} + read_header_timeout: 1s + # @schema {"name": "server_config.metrics.pprof.server.http.read_timeout", "type": "string"} + read_timeout: 1s + # @schema {"name": "server_config.metrics.pprof.server.http.shutdown_duration", "type": "string"} + shutdown_duration: 5s + # @schema {"name": "server_config.metrics.pprof.server.http.write_timeout", "type": "string"} + write_timeout: 1m + # @schema {"name": "server_config.full_shutdown_duration", "type": "string"} + full_shutdown_duration: 600s + # @schema {"name": "server_config.tls", "type": "object"} + tls: + # @schema {"name": "server_config.tls.enabled", "type": "boolean"} + enabled: false + # @schema {"name": "server_config.tls.ca", "type": "string"} + ca: /path/to/ca + # @schema {"name": "server_config.tls.cert", "type": "string"} + cert: /path/to/cert + # @schema {"name": "server_config.tls.key", "type": "string"} + key: /path/to/key + # @schema {"name": "server_config.tls.insecure_skip_verify", "type": "boolean"} + # server_config.tls.insecure_skip_verify -- enable/disable skip SSL certificate verification + insecure_skip_verify: false + +# @schema {"name": "observability", "type": "object"} +observability: + # @schema {"name": "observability.enabled", "type": "boolean"} + enabled: false + # @schema {"name": "observability.otlp", "type": "object"} + otlp: + # @schema {"name": "observability.otlp.collector_endpoint", "type": "string"} + collector_endpoint: "" + # @schema {"name": "observability.otlp.trace_batch_timeout", "type": "string"} + trace_batch_timeout: "1s" + # @schema {"name": "observability.otlp.trace_export_timeout", "type": "string"} + trace_export_timeout: "1m" + # @schema {"name": "observability.otlp.trace_max_export_batch_size", "type": "integer"} + trace_max_export_batch_size: 1024 + # @schema {"name": "observability.otlp.trace_max_queue_size", "type": "integer"} + trace_max_queue_size: 256 + # @schema {"name": "observability.otlp.metrics_export_interval", "type": "string"} + metrics_export_interval: "1s" + # @schema {"name": "observability.otlp.metrics_export_timeout", "type": "string"} + metrics_export_timeout: "1m" + # @schema {"name": "observability.otlp.attribute", "type": "object"} + attribute: + # @schema {"name": "observability.otlp.attribute.namespace", "type": "string"} + namespace: "_MY_POD_NAMESPACE_" + # @schema {"name": "observability.otlp.attribute.pod_name", "type": "string"} + pod_name: "_MY_POD_NAME_" + # @schema {"name": "observability.otlp.attribute.node_name", "type": "string"} + node_name: "_MY_NODE_NAME_" + # @schema {"name": "observability.otlp.attribute.service_name", "type": "string"} + service_name: "vald-benchmark-operator" + # @schema {"name": "observability.otlp.attribute.metrics", "type": "object"} + metrics: + # @schema {"name": "observability.otlp.attribute.metrics.enable_cgo", "type": "boolean"} + enable_cgo: true + # @schema {"name": "observability.otlp.attribute.metrics.enable_goroutine", "type": "boolean"} + enable_goroutine: true + # @schema {"name": "observability.otlp.attribute.metrics.enable_memory", "type": "boolean"} + enable_memory: true + # @schema {"name": "observability.otlp.attribute.metrics.enable_version_info", "type": "boolean"} + enable_version_info: true + # @schema {"name": "observability.otlp.attribute.metrics.version_info_labels", "type": "array", "items": {"type": "string"}} + version_info_labels: + - vald_version + - server_name + - git_commit + - build_time + - go_version + - go_os + - go_arch + - ngt_version + # @schema {"name": "observability.trace", "type": "object"} + trace: + # @schema {"name": "observability.trace.enabled", "type": "boolean"} + enabled: false + # @schema {"name": "observability.trace.sampling_rate", "type": "integer"} + sampling_rate: 1 diff --git a/charts/vald-benchmark-operator/values/benchmark-job.yaml b/charts/vald-benchmark-operator/values/benchmark-job.yaml new file mode 100644 index 0000000000..b11d8dfafa --- /dev/null +++ b/charts/vald-benchmark-operator/values/benchmark-job.yaml @@ -0,0 +1,46 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: vald.vdaas.org/v1 +kind: ValdBenchmarkJob +metadata: + name: sample-search-job + namespace: default +spec: + dataset: + name: "fashion-mnist" + indexes: 1000 + group: "test" + range: + start: 1 + end: 1000 + job_type: "search" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + client_config: + health_check_duration: "10s" + rps: 1000 + search_config: + epsilon: 0.1 + radius: -1 + num: 10 + min_num: 10 + timeout: "1m" + enable_linear_search: true + target: + host: "vald-lb-gateway.default.svc.cluster.local" + port: 8081 diff --git a/charts/vald-benchmark-operator/values/benchmark-scenario.yaml b/charts/vald-benchmark-operator/values/benchmark-scenario.yaml new file mode 100644 index 0000000000..f3885cc824 --- /dev/null +++ b/charts/vald-benchmark-operator/values/benchmark-scenario.yaml @@ -0,0 +1,187 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: vald.vdaas.org/v1 +kind: ValdBenchmarkScenario +metadata: + name: sample-scenario + namespace: default +spec: + # @schema {"name": "dataset", "type": "object"} + # dataset -- dataset information + dataset: + # @schema {"name": "dataset.name", "type": "string" } + # dataset.name -- the name of dataset + name: "fashion-mnist" + # @schema {"name": "dataset.indexes", "type": "integer"} + # dataset.indexes -- the amount of indexes + indexes: 1000 + # @schema {"name": "dataset.group", "type": "string"} + # dataset.group -- the hdf5 group name of dataset + group: "test" + # @schema {"name": "dataset.range", "type": "object"} + # dataset.range -- the data range of indexes + range: + # @schema {"name": "dataset.range.start", "type": "integer"} + # dataset.range.start -- start index number + start: 1 + # @schema {"name": "dataset.range.end", "type": "integer"} + # dataset.range.end -- end index number + end: 1000 + # @schema {"name": "jobs", "type": "array", "items": {"type": "object"}} + # jobs -- benchmark jobs + jobs: + # @schema {"name": "jobs.items.dataset", "type": "object"} + - job_type: "insert" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + dataset: + name: "fashion-mnist" + indexes: 10000 + group: "train" + range: + start: 1 + end: 10000 + insert_config: + skip_strict_exist_check: true + client_config: + health_check_duration: "10s" + rps: 500 + - job_type: "update" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + dataset: + name: "fashion-mnist" + indexes: 10000 + group: "train" + range: + start: 10001 + end: 20000 + update_config: + skip_strict_exist_check: true + client_config: + health_check_duration: "10s" + rps: 500 + - job_type: "search" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + search_config: + epsilon: 0.1 + radius: -1 + num: 10 + min_num: 10 + timeout: "1m" + enable_linear_search: true + client_config: + health_check_duration: "10s" + rps: 2000 + - job_type: "upsert" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + dataset: + name: "fashion-mnist" + indexes: 30000 + group: "train" + range: + start: 10001 + end: 40000 + upsert_config: + skip_strict_exist_check: true + client_config: + health_check_duration: "10s" + rps: 1000 + - job_type: "search" + dimension: 784 + repetition: 2 + replica: 1 + rules: [] + dataset: + name: "fashion-mnist" + indexes: 20000 + group: "test" + range: + start: 1 + end: 20000 + search_config: + epsilon: 0.1 + radius: -1 + num: 10 + min_num: 10 + timeout: "1m" + enable_linear_search: false + client_config: + health_check_duration: "10s" + rps: 4000 + - job_type: "exists" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + dataset: + name: "fashion-mnist" + indexes: 20000 + group: "train" + range: + start: 1 + end: 20000 + client_config: + health_check_duration: "10s" + rps: 1000 + - job_type: "getobject" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + dataset: + name: "fashion-mnist" + indexes: 20000 + group: "train" + range: + start: 1 + end: 20000 + client_config: + health_check_duration: "10s" + rps: 1000 + - job_type: "remove" + dimension: 784 + repetition: 1 + replica: 1 + rules: [] + dataset: + name: "fashion-mnist" + indexes: 30000 + group: "train" + range: + start: 1 + end: 30000 + remove_config: + skip_strict_exist_check: true + client_config: + health_check_duration: "10s" + rps: 1000 + + # @schema {"name": "target", "type": "array", "items": {"type": "object"}} + # target -- target cluster host&port + target: + host: "vald-lb-gateway.default.svc.cluster.local" + port: 8081 diff --git a/cmd/tools/benchmark/job/main.go b/cmd/tools/benchmark/job/main.go new file mode 100644 index 0000000000..c7badde186 --- /dev/null +++ b/cmd/tools/benchmark/job/main.go @@ -0,0 +1,60 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Package main provides program main +package main + +import ( + "context" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/info" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/runner" + "github.com/vdaas/vald/internal/safety" + "github.com/vdaas/vald/pkg/tools/benchmark/job/config" + "github.com/vdaas/vald/pkg/tools/benchmark/job/usecase" +) + +const ( + maxVersion = "v0.0.10" + minVersion = "v0.0.0" + name = "benchmark job" +) + +func main() { + if err := safety.RecoverFunc(func() error { + ctx := context.Background() + return runner.Do( + ctx, + runner.WithName(name), + runner.WithVersion(info.Version, maxVersion, minVersion), + runner.WithConfigLoader(func(path string) (interface{}, *config.GlobalConfig, error) { + cfg, err := config.NewConfig(ctx, path) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to load "+name+"'s configuration") + } + return cfg, &cfg.GlobalConfig, nil + }), + runner.WithDaemonInitializer(func(cfg interface{}) (runner.Runner, error) { + return usecase.New(cfg.(*config.Config)) + }), + ) + })(); err != nil { + log.Fatal(err, info.Get()) + return + } +} diff --git a/cmd/tools/benchmark/job/sample.yaml b/cmd/tools/benchmark/job/sample.yaml new file mode 100644 index 0000000000..2ca6336ddb --- /dev/null +++ b/cmd/tools/benchmark/job/sample.yaml @@ -0,0 +1,260 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +version: v0.0.0 +time_zone: JST +logging: + format: raw + level: debug + logger: glg +server_config: + servers: + - name: grpc + host: 0.0.0.0 + port: 8081 + probe_wait_time: 3s + socket_path: "" + mode: GRPC + grpc: + bidirectional_stream_concurrency: 20 + max_receive_message_size: 0 + max_send_message_size: 0 + initial_window_size: 1048576 + initial_conn_window_size: 2097152 + keepalive: + max_conn_idle: "" + max_conn_age: "" + max_conn_age_grace: "" + time: "3h" + timeout: "60s" + min_time: "10m" + permit_without_stream: true + write_buffer_size: 0 + read_buffer_size: 0 + connection_timeout: "" + max_header_list_size: 0 + header_table_size: 0 + interceptors: + - "RecoverInterceptor" + enable_reflection: true + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: false + tcp_no_delay: false + tcp_cork: false + tcp_quick_ack: false + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + restart: true + health_check_servers: + - name: liveness + host: 0.0.0.0 + port: 3000 + mode: "" + probe_wait_time: "3s" + network: tcp + socket_path: "" + http: + shutdown_duration: "5s" + handler_timeout: "" + idle_timeout: "" + read_header_timeout: "" + read_timeout: "" + write_timeout: "" + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: true + tcp_no_delay: true + tcp_cork: false + tcp_quick_ack: true + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + - name: readiness + host: 0.0.0.0 + port: 3001 + mode: "" + probe_wait_time: "3s" + network: tcp + socket_path: "" + http: + shutdown_duration: "0s" + handler_timeout: "" + idle_timeout: "" + read_header_timeout: "" + read_timeout: "" + write_timeout: "" + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: true + tcp_no_delay: true + tcp_cork: false + tcp_quick_ack: true + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + metrics_servers: + - name: pprof + host: 0.0.0.0 + port: 8081 + probe_wait_time: "3s" + socket_path: "" + mode: REST + network: tcp + http: + handler_timeout: "5s" + idle_timeout: "2s" + read_header_timeout: "1s" + read_timeout: "1s" + shutdown_duration: "5s" + write_timeout: "1m" + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: false + tcp_no_delay: false + tcp_cork: false + tcp_quick_ack: false + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + startup_strategy: + - liveness + - readiness + - grpc + full_shutdown_duration: 30s + tls: + ca: /path/to/ca + cert: /path/to/cert + enabled: false + key: /path/to/key +observability: + enabled: false + otlp: + collector_endpoint: "" + attribute: + namespace: _MY_POD_NAMESPACE_ + pod_name: _MY_POD_NAME_ + node_name: _MY_NODE_NAME_ + service_name: vald-benchmark + trace_batch_timeout: "1s" + trace_export_timeout: "1m" + trace_max_export_batch_size: 1024 + trace_max_queue_size: 256 + metrics: + enable_cgo: true + enable_goroutine: true + enable_memory: true + enable_version_info: true + version_info_labels: + - vald_version + - server_name + - git_commit + - build_time + - go_version + - go_os + - go_arch + - ngt_version + trace: + enabled: false +job: + replica: 1 + repetition: 1 + before_job_name: "" + before_job_namespace: "" + rps: 200 + concurrency_limit: 200 + client_config: + health_check_duration: "1s" + connection_pool: + enable_dns_resolver: true + enable_rebalance: true + rebalance_duration: "30m" + size: 3 + old_conn_close_duration: "2m" + backoff: + initial_duration: "5ms" + backoff_time_limit: "5s" + maximum_duration: "5s" + jitter_limit: "100ms" + backoff_factor: 1.1 + retry_count: 100 + enable_error_log: true + circuit_breaker: + closed_error_rate: 0.7 + half_open_error_rate: 0.5 + min_samples: 1000 + open_timeout: "1s" + closed_refresh_timeout: "10s" + call_option: + wait_for_ready: true + max_retry_rpc_buffer_size: 0 + max_recv_msg_size: 0 + max_send_msg_size: 0 + dial_option: + write_buffer_size: 0 + read_buffer_size: 0 + initial_window_size: 1048576 + initial_connection_window_size: 2097152 + max_msg_size: 0 + backoff_max_delay: "120s" + backoff_base_delay: "1s" + backoff_multiplier: 1.6 + backoff_jitter: 0.2 + min_connection_timeout: "20s" + enable_backoff: false + insecure: true + timeout: "" + interceptors: [] + net: + dns: + cache_enabled: true + refresh_duration: "30m" + cache_expiration: "1h" + dialer: + timeout: "" + keepalive: "" + fallback_delay: "" + dual_stack_enabled: true + tls: + enabled: false + cert: /path/to/cert + key: /path/to/key + ca: /path/to/ca + insecure_skip_verify: false + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: false + tcp_no_delay: false + tcp_cork: false + tcp_quick_ack: false + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + keepalive: + time: "120s" + timeout: "30s" + permit_without_stream: false + tls: + enabled: false + cert: /path/to/cert + key: /path/to/key + ca: /path/to/ca + insecure_skip_verify: false diff --git a/cmd/tools/benchmark/operator/main.go b/cmd/tools/benchmark/operator/main.go new file mode 100644 index 0000000000..6d3e2d0d12 --- /dev/null +++ b/cmd/tools/benchmark/operator/main.go @@ -0,0 +1,59 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Package main provides program main +package main + +import ( + "context" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/info" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/runner" + "github.com/vdaas/vald/internal/safety" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/config" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/usecase" +) + +const ( + maxVersion = "v0.0.10" + minVersion = "v0.0.0" + name = "benchmark operator" +) + +func main() { + if err := safety.RecoverFunc(func() error { + return runner.Do( + context.Background(), + runner.WithName(name), + runner.WithVersion(info.Version, maxVersion, minVersion), + runner.WithConfigLoader(func(path string) (interface{}, *config.GlobalConfig, error) { + cfg, err := config.NewConfig(path) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to load "+name+"'s configuration") + } + return cfg, &cfg.GlobalConfig, nil + }), + runner.WithDaemonInitializer(func(cfg interface{}) (runner.Runner, error) { + return usecase.New(cfg.(*config.Config)) + }), + ) + })(); err != nil { + log.Fatal(err, info.Get()) + return + } +} diff --git a/cmd/tools/benchmark/operator/sample.yaml b/cmd/tools/benchmark/operator/sample.yaml new file mode 100644 index 0000000000..2495d67294 --- /dev/null +++ b/cmd/tools/benchmark/operator/sample.yaml @@ -0,0 +1,175 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +version: v0.0.0 +time_zone: JST +logging: + format: raw + level: debug + logger: glg +server_config: + servers: + - name: grpc + host: 0.0.0.0 + port: 8081 + probe_wait_time: 3s + socket_path: "" + mode: GRPC + grpc: + bidirectional_stream_concurrency: 20 + max_receive_message_size: 0 + max_send_message_size: 0 + initial_window_size: 1048576 + initial_conn_window_size: 2097152 + keepalive: + max_conn_idle: "" + max_conn_age: "" + max_conn_age_grace: "" + time: "3h" + timeout: "60s" + min_time: "10m" + permit_without_stream: true + write_buffer_size: 0 + read_buffer_size: 0 + connection_timeout: "" + max_header_list_size: 0 + header_table_size: 0 + interceptors: + - "RecoverInterceptor" + enable_reflection: true + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: false + tcp_no_delay: false + tcp_cork: false + tcp_quick_ack: false + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + restart: true + health_check_servers: + - name: liveness + host: 0.0.0.0 + port: 3000 + mode: "" + probe_wait_time: "3s" + network: tcp + socket_path: "" + http: + shutdown_duration: "5s" + handler_timeout: "" + idle_timeout: "" + read_header_timeout: "" + read_timeout: "" + write_timeout: "" + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: true + tcp_no_delay: true + tcp_cork: false + tcp_quick_ack: true + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + - name: readiness + host: 0.0.0.0 + port: 3001 + mode: "" + probe_wait_time: "3s" + network: tcp + socket_path: "" + http: + shutdown_duration: "0s" + handler_timeout: "" + idle_timeout: "" + read_header_timeout: "" + read_timeout: "" + write_timeout: "" + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: true + tcp_no_delay: true + tcp_cork: false + tcp_quick_ack: true + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + metrics_servers: + - name: pprof + host: 0.0.0.0 + port: 8081 + probe_wait_time: "3s" + socket_path: "" + mode: REST + network: tcp + http: + handler_timeout: "5s" + idle_timeout: "2s" + read_header_timeout: "1s" + read_timeout: "1s" + shutdown_duration: "5s" + write_timeout: "1m" + socket_option: + reuse_port: true + reuse_addr: true + tcp_fast_open: false + tcp_no_delay: false + tcp_cork: false + tcp_quick_ack: false + tcp_defer_accept: false + ip_transparent: false + ip_recover_destination_addr: false + startup_strategy: + - liveness + - readiness + - grpc + full_shutdown_duration: 30s + tls: + ca: /path/to/ca + cert: /path/to/cert + enabled: false + key: /path/to/key +observability: + enabled: false + otlp: + collector_endpoint: "" + attribute: + namespace: _MY_POD_NAMESPACE_ + pod_name: _MY_POD_NAME_ + node_name: _MY_NODE_NAME_ + service_name: vald-benchmark + trace_batch_timeout: "1s" + trace_export_timeout: "1m" + trace_max_export_batch_size: 1024 + trace_max_queue_size: 256 + metrics: + enable_cgo: true + enable_goroutine: true + enable_memory: true + enable_version_info: true + version_info_labels: + - vald_version + - server_name + - git_commit + - build_time + - go_version + - go_os + - go_arch + - ngt_version + trace: + enabled: false diff --git a/dockers/tools/benchmark/job/Dockerfile b/dockers/tools/benchmark/job/Dockerfile new file mode 100644 index 0000000000..6a3111f26d --- /dev/null +++ b/dockers/tools/benchmark/job/Dockerfile @@ -0,0 +1,125 @@ +# syntax = docker/dockerfile:latest +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# + +ARG GO_VERSION=latest +ARG ZLIB_VERSION +ARG HDF5_VERSION +ARG DISTROLESS_IMAGE=gcr.io/distroless/static +ARG DISTROLESS_IMAGE_TAG=nonroot +ARG UPX_OPTIONS=-9 +ARG MAINTAINER="vdaas.org vald team " + +FROM golang:${GO_VERSION} AS golang + +FROM ubuntu:devel AS builder + +ARG UPX_OPTIONS +ARG ZLIB_VERSION +ARG HDF5_VERSION + +ENV GO111MODULE on +ENV DEBIAN_FRONTEND noninteractive +ENV INITRD No +ENV LANG en_US.UTF-8 +ENV GOROOT /opt/go +ENV GOPATH /go +ENV PATH ${PATH}:${GOROOT}/bin:${GOPATH}/bin +ENV ORG vdaas +ENV REPO vald +ENV APP_NAME job +ENV PKG tools/benchmark/${APP_NAME} +ENV BUILD_DIR=/usr/local +ENV LIB_DIR=/usr/local/lib +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${LIB_DIR}:/lib + +# skipcq: DOK-DL3008, DOK-DL3003 +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + build-essential \ + curl \ + upx \ + g++ \ + git \ + && ldconfig \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p ${LIB_DIR} \ + && curl -sOL https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}/zlib-${ZLIB_VERSION}.tar.gz \ + && mkdir -p zlib \ + && tar -xzvf zlib-${ZLIB_VERSION}.tar.gz -C zlib --strip-components 1 \ + && cd zlib \ + && ./configure --prefix=${LIB_DIR} --static \ + && make \ + && make test \ + && make install \ + && cd / \ + && mkdir -p hdf5 \ + && curl -sOL https://github.com/HDFGroup/hdf5/releases/download/${HDF5_VERSION}/${HDF5_VERSION}.tar.gz \ + && tar -xzvf ${HDF5_VERSION}.tar.gz -C hdf5 --strip-components 2 \ + && cd hdf5 \ + && ./configure --enable-build-mode=production --enable-static-exec --disable-shared --prefix=${BUILD_DIR} --with-zlib=${BUILD_DIR}/include,${LIB_DIR} LDFLAGS="-Wl,-rpath,${LIB_DIR}" \ + && make check \ + && make install \ + && ldconfig + +COPY --from=golang /usr/local/go $GOROOT +RUN mkdir -p "$GOPATH/src" + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/Makefile.d +COPY Makefile.d . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO} +COPY Makefile . +COPY .git . +COPY go.mod . +COPY go.sum . + +RUN make go/download + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/internal +COPY internal . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/apis/grpc +COPY apis/grpc . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/pkg/${PKG} +COPY pkg/${PKG} . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/cmd/${PKG} +COPY cmd/${PKG} . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/versions +COPY versions . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO} +RUN make REPO=${ORG} NAME=${REPO} cmd/${PKG}/${APP_NAME} \ + && mv "cmd/${PKG}/${APP_NAME}" "/usr/bin/${APP_NAME}" + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/cmd/${PKG} +RUN cp sample.yaml /tmp/config.yaml + +FROM ${DISTROLESS_IMAGE}:${DISTROLESS_IMAGE_TAG} +LABEL maintainer="${MAINTAINER}" + +ENV APP_NAME job + +COPY --from=builder /usr/bin/${APP_NAME} /go/bin/${APP_NAME} +COPY --from=builder /tmp/config.yaml /etc/server/config.yaml + +USER nonroot:nonroot + +ENTRYPOINT ["/go/bin/job"] diff --git a/dockers/tools/benchmark/operator/Dockerfile b/dockers/tools/benchmark/operator/Dockerfile new file mode 100644 index 0000000000..ba3dc24266 --- /dev/null +++ b/dockers/tools/benchmark/operator/Dockerfile @@ -0,0 +1,98 @@ +# syntax = docker/dockerfile:latest +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# + +ARG GO_VERSION=latest +ARG DISTROLESS_IMAGE=gcr.io/distroless/static +ARG DISTROLESS_IMAGE_TAG=nonroot +ARG UPX_OPTIONS=-9 +ARG MAINTAINER="vdaas.org vald team " + +FROM golang:${GO_VERSION} AS golang + +FROM ubuntu:devel AS builder + +ENV GO111MODULE on +ENV DEBIAN_FRONTEND noninteractive +ENV INITRD No +ENV LANG en_US.UTF-8 +ENV GOROOT /opt/go +ENV GOPATH /go +ENV PATH ${PATH}:${GOROOT}/bin:${GOPATH}/bin +# ARG UPX_OPTIONS +ENV ORG vdaas +ENV REPO vald +ENV APP_NAME operator +ENV PKG tools/benchmark/${APP_NAME} + +# skipcq: DOK-DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + build-essential \ + curl \ + upx \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p ${GOPATH}/src + +COPY --from=golang /usr/local/go $GOROOT +RUN mkdir -p "$GOPATH/src" + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/Makefile.d +COPY Makefile.d . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO} +COPY Makefile . +COPY .git . +COPY go.mod . +COPY go.sum . + +RUN go mod download + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/internal +COPY internal . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/apis/grpc +COPY apis/grpc . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/pkg/${PKG} +COPY pkg/${PKG} . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/cmd/${PKG} +COPY cmd/${PKG} . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/versions +COPY versions . + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO} +RUN make REPO=${ORG} NAME=${REPO} cmd/${PKG}/${APP_NAME} \ + && mv "cmd/${PKG}/${APP_NAME}" "/usr/bin/${APP_NAME}" + +WORKDIR ${GOPATH}/src/github.com/${ORG}/${REPO}/cmd/${PKG} +RUN cp sample.yaml /tmp/config.yaml + +FROM ${DISTROLESS_IMAGE}:${DISTROLESS_IMAGE_TAG} +LABEL maintainer="${MAINTAINER}" +ENV APP_NAME operator + +COPY --from=builder /usr/bin/${APP_NAME} /go/bin/${APP_NAME} +COPY --from=builder /tmp/config.yaml /etc/server/config.yaml + +USER nonroot:nonroot + +ENTRYPOINT ["/go/bin/operator"] diff --git a/go.mod b/go.mod index 8496307df8..a066a0af80 100755 --- a/go.mod +++ b/go.mod @@ -387,6 +387,7 @@ require ( go.opentelemetry.io/otel/trace v1.19.0 go.uber.org/automaxprocs v0.0.0-00010101000000-000000000000 go.uber.org/goleak v1.2.1 + go.uber.org/ratelimit v0.3.0 go.uber.org/zap v1.26.0 gocloud.dev v0.0.0-00010101000000-000000000000 golang.org/x/net v0.19.0 @@ -394,6 +395,7 @@ require ( golang.org/x/sync v0.5.0 golang.org/x/sys v0.15.0 golang.org/x/text v0.14.0 + golang.org/x/time v0.5.0 golang.org/x/tools v0.16.0 gonum.org/v1/hdf5 v0.0.0-00010101000000-000000000000 gonum.org/v1/plot v0.10.1 @@ -419,6 +421,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect + github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/campoy/embedmd v1.0.0 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect @@ -497,7 +500,6 @@ require ( golang.org/x/image v0.14.0 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/term v0.15.0 // indirect - golang.org/x/time v0.5.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/api v0.152.0 // indirect diff --git a/go.sum b/go.sum index cbeeccd22e..5e745c898a 100644 --- a/go.sum +++ b/go.sum @@ -219,6 +219,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNIC github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= @@ -604,12 +606,16 @@ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lI go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.starlark.net v0.0.0-20231121155337-90ade8b19d09 h1:hzy3LFnSN8kuQK8h9tHl4ndF6UruMj47OqwqsS+/Ai4= go.starlark.net v0.0.0-20231121155337-90ade8b19d09/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.3.0 h1:IdZd9wqvFXnvLvSEBo0KPcGfkoBGNkpTHlrE3Rcjkjw= +go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyBaI= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= gocloud.dev v0.35.0 h1:x/Gtt5OJdT4j+ir1AXAIXb7bBnFawXAAaJptCUGk3HU= diff --git a/internal/config/benchmark.go b/internal/config/benchmark.go new file mode 100644 index 0000000000..26fbc27a93 --- /dev/null +++ b/internal/config/benchmark.go @@ -0,0 +1,247 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 config providers configuration type and load configuration logic +package config + +// BenchmarkJob represents the configuration for the internal benchmark search job. +type BenchmarkJob struct { + Target *BenchmarkTarget `json:"target,omitempty" yaml:"target"` + Dataset *BenchmarkDataset `json:"dataset,omitempty" yaml:"dataset"` + Dimension int `json:"dimension,omitempty" yaml:"dimension"` + Replica int `json:"replica,omitempty" yaml:"replica"` + Repetition int `json:"repetition,omitempty" yaml:"repetition"` + JobType string `json:"job_type,omitempty" yaml:"job_type"` + InsertConfig *InsertConfig `json:"insert_config,omitempty" yaml:"insert_config"` + UpdateConfig *UpdateConfig `json:"update_config,omitempty" yaml:"update_config"` + UpsertConfig *UpsertConfig `json:"upsert_config,omitempty" yaml:"upsert_config"` + SearchConfig *SearchConfig `json:"search_config,omitempty" yaml:"search_config"` + RemoveConfig *RemoveConfig `json:"remove_config,omitempty" yaml:"remove_config"` + ObjectConfig *ObjectConfig `json:"object_config,omitempty" yaml:"object_config"` + ClientConfig *GRPCClient `json:"client_config,omitempty" yaml:"client_config"` + Rules []*BenchmarkJobRule `json:"rules,omitempty" yaml:"rules"` + BeforeJobName string `json:"before_job_name,omitempty" yaml:"before_job_name"` + BeforeJobNamespace string `json:"before_job_namespace,omitempty" yaml:"before_job_namespace"` + RPS int `json:"rps,omitempty" yaml:"rps"` + ConcurrencyLimit int `json:"concurrency_limit,omitempty" yaml:"concurrency_limit"` +} + +// BenchmarkScenario represents the configuration for the internal benchmark scenario. +type BenchmarkScenario struct { + Target *BenchmarkTarget `json:"target,omitempty" yaml:"target"` + Dataset *BenchmarkDataset `json:"dataset,omitempty" yaml:"dataset"` + Jobs []*BenchmarkJob `json:"jobs,omitempty" yaml:"jobs"` +} + +// BenchmarkTarget defines the desired state of BenchmarkTarget +type BenchmarkTarget struct { + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` +} + +func (t *BenchmarkTarget) Bind() *BenchmarkTarget { + t.Host = GetActualValue(t.Host) + return t +} + +// BenchmarkDataset defines the desired state of BenchmarkDateset +type BenchmarkDataset struct { + Name string `json:"name,omitempty"` + Group string `json:"group,omitempty"` + Indexes int `json:"indexes,omitempty"` + Range *BenchmarkDatasetRange `json:"range,omitempty"` + URL string `json:"url,omitempty"` +} + +func (d *BenchmarkDataset) Bind() *BenchmarkDataset { + d.Name = GetActualValue(d.Name) + d.Group = GetActualValue(d.Group) + d.URL = GetActualValue(d.URL) + return d +} + +// BenchmarkDatasetRange defines the desired state of BenchmarkDatesetRange +type BenchmarkDatasetRange struct { + Start int `json:"start,omitempty"` + End int `json:"end,omitempty"` +} + +// BenchmarkJobRule defines the desired state of BenchmarkJobRule +type BenchmarkJobRule struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} + +func (r *BenchmarkJobRule) Bind() *BenchmarkJobRule { + r.Name = GetActualValue(r.Name) + r.Type = GetActualValue(r.Type) + return r +} + +// InsertConfig defines the desired state of insert config +type InsertConfig struct { + SkipStrictExistCheck bool `json:"skip_strict_exist_check,omitempty"` + Timestamp string `json:"timestamp,omitempty"` +} + +func (cfg *InsertConfig) Bind() *InsertConfig { + cfg.Timestamp = GetActualValue(cfg.Timestamp) + return cfg +} + +// UpdateConfig defines the desired state of update config +type UpdateConfig struct { + SkipStrictExistCheck bool `json:"skip_strict_exist_check,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + DisableBalancedUpdate bool `json:"disable_balanced_update,omitempty"` +} + +func (cfg *UpdateConfig) Bind() *UpdateConfig { + cfg.Timestamp = GetActualValue(cfg.Timestamp) + return cfg +} + +// UpsertConfig defines the desired state of upsert config +type UpsertConfig struct { + SkipStrictExistCheck bool `json:"skip_strict_exist_check,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + DisableBalancedUpdate bool `json:"disable_balanced_update,omitempty"` +} + +func (cfg *UpsertConfig) Bind() *UpsertConfig { + cfg.Timestamp = GetActualValue(cfg.Timestamp) + return cfg +} + +// SearchConfig defines the desired state of search config +type SearchConfig struct { + Epsilon float32 `json:"epsilon,omitempty"` + Radius float32 `json:"radius,omitempty"` + Num int32 `json:"num,omitempty"` + MinNum int32 `json:"min_num,omitempty"` + Timeout string `json:"timeout,omitempty"` + EnableLinearSearch bool `json:"enable_linear_search,omitempty"` + AggregationAlgorithm string `json:"aggregation_algorithm,omitempty"` +} + +func (cfg *SearchConfig) Bind() *SearchConfig { + cfg.Timeout = GetActualValue(cfg.Timeout) + cfg.AggregationAlgorithm = GetActualValue(cfg.AggregationAlgorithm) + return cfg +} + +// RemoveConfig defines the desired state of remove config +type RemoveConfig struct { + SkipStrictExistCheck bool `json:"skip_strict_exist_check,omitempty"` + Timestamp string `json:"timestamp,omitempty"` +} + +func (cfg *RemoveConfig) Bind() *RemoveConfig { + cfg.Timestamp = GetActualValue(cfg.Timestamp) + return cfg +} + +// ObjectConfig defines the desired state of object config +type ObjectConfig struct { + FilterConfig FilterConfig `json:"filter_config,omitempty" yaml:"filter_config"` +} + +func (cfg *ObjectConfig) Bind() *ObjectConfig { + cfg.FilterConfig = *cfg.FilterConfig.Bind() + return cfg +} + +// FilterTarget defines the desired state of filter target +type FilterTarget struct { + Host string `json:"host,omitempty" yaml:"host"` + Port int32 `json:"port,omitempty" yaml:"port"` +} + +func (cfg *FilterTarget) Bind() *FilterTarget { + cfg.Host = GetActualValue(cfg.Host) + return cfg +} + +// FilterConfig defines the desired state of filter config +type FilterConfig struct { + Targets []*FilterTarget `json:"target,omitempty" yaml:"target"` +} + +func (cfg *FilterConfig) Bind() *FilterConfig { + for i := 0; i < len(cfg.Targets); i++ { + cfg.Targets[i] = cfg.Targets[i].Bind() + } + return cfg +} + +// Bind binds the actual data from the Job receiver fields. +func (b *BenchmarkJob) Bind() *BenchmarkJob { + b.JobType = GetActualValue(b.JobType) + b.BeforeJobName = GetActualValue(b.BeforeJobName) + b.BeforeJobNamespace = GetActualValue(b.BeforeJobNamespace) + + if b.Target != nil { + b.Target = b.Target.Bind() + } + if b.Dataset != nil { + b.Dataset = b.Dataset.Bind() + } + if b.InsertConfig != nil { + b.InsertConfig = b.InsertConfig.Bind() + } + if b.UpdateConfig != nil { + b.UpdateConfig = b.UpdateConfig.Bind() + } + if b.UpsertConfig != nil { + b.UpsertConfig = b.UpsertConfig.Bind() + } + if b.SearchConfig != nil { + b.SearchConfig = b.SearchConfig.Bind() + } + if b.RemoveConfig != nil { + b.RemoveConfig = b.RemoveConfig.Bind() + } + if b.ObjectConfig != nil { + b.ObjectConfig = b.ObjectConfig.Bind() + } + if b.ClientConfig != nil { + b.ClientConfig = b.ClientConfig.Bind() + } + if len(b.Rules) > 0 { + for i := 0; i < len(b.Rules); i++ { + b.Rules[i] = b.Rules[i].Bind() + } + } + return b +} + +// Bind binds the actual data from the BenchmarkScenario receiver fields. +func (b *BenchmarkScenario) Bind() *BenchmarkScenario { + return b +} + +// BenchmarkJobImageInfo represents the docker image information for benchmark job. +type BenchmarkJobImageInfo struct { + Image string `json:"image,omitempty" yaml:"image"` + PullPolicy string `json:"pull_policy,omitempty" yaml:"pull_policy"` +} + +// Bind binds the actual data from the BenchmarkJobImageInfo receiver fields. +func (b *BenchmarkJobImageInfo) Bind() *BenchmarkJobImageInfo { + b.Image = GetActualValue(b.Image) + b.PullPolicy = GetActualValue(b.PullPolicy) + return b +} diff --git a/internal/config/config.go b/internal/config/config.go index 5e435a7f79..4d1769df89 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,9 +19,11 @@ package config import ( "bytes" + "fmt" "io/fs" "os" "path/filepath" + "reflect" "github.com/vdaas/vald/internal/conv" "github.com/vdaas/vald/internal/encoding/json" @@ -131,3 +133,132 @@ func ToRawYaml(data interface{}) string { } return buf.String() } + +// Merge merges multiple objects to one object. +// the value of each field is prioritized the value of last index of `objs`. +// if the length of `objs` is zero, it returns initial value of type T. +func Merge[T any](objs ...T) (dst T, err error) { + switch len(objs) { + case 0: + return dst, nil + case 1: + dst = objs[0] + return dst, nil + default: + dst = objs[0] + visited := make(map[uintptr]bool) + rdst := reflect.ValueOf(&dst) + for _, src := range objs[1:] { + err = deepMerge(rdst, reflect.ValueOf(&src), visited, "") + if err != nil { + return dst, err + } + } + } + return dst, err +} + +// skipcq: GO-R1005 +func deepMerge(dst, src reflect.Value, visited map[uintptr]bool, fieldPath string) (err error) { + if !src.IsValid() || src.IsZero() { + return nil + } else if !dst.IsValid() { + dst = src + log.Info(dst.Type(), dst, src) + } + dType := dst.Type() + sType := src.Type() + if dType != sType { + return errors.ErrNotMatchFieldType(fieldPath, dType, sType) + } + sKind := src.Kind() + if sKind == reflect.Ptr { + src = src.Elem() + } + if sKind == reflect.Struct && src.CanAddr() { + addr := src.Addr().Pointer() + if visited[addr] { + return nil + } + if src.NumField() > 1 { + visited[addr] = true + } + } + switch dst.Kind() { + case reflect.Ptr: + if dst.IsNil() { + dst.Set(reflect.New(dst.Type().Elem())) + } + return deepMerge(dst.Elem(), src, visited, fieldPath) + case reflect.Struct: + dnum := dst.NumField() + snum := src.NumField() + if dnum != snum { + return errors.ErrNotMatchFieldNum(fieldPath, dnum, snum) + } + for i := 0; i < dnum; i++ { + dstField := dst.Field(i) + if dstField.CanSet() { + nf := fmt.Sprintf("%s.%s(%d)", fieldPath, dType.Field(i).Name, i) + if err = deepMerge(dstField, src.Field(i), visited, nf); err != nil { + return errors.ErrDeepMergeKind(dst.Kind().String(), nf, err) + } + } + } + case reflect.Slice: + srcLen := src.Len() + if srcLen > 0 { + if dst.IsNil() { + dst.Set(reflect.MakeSlice(dType, srcLen, srcLen)) + } else { + diffLen := srcLen - dst.Len() + if diffLen > 0 { + dst.Set(reflect.AppendSlice(dst, reflect.MakeSlice(dType, diffLen, diffLen))) + } + } + for i := 0; i < srcLen; i++ { + nf := fmt.Sprintf("%s[%d]", fieldPath, i) + if err = deepMerge(dst.Index(i), src.Index(i), visited, nf); err != nil { + return errors.ErrDeepMergeKind(dst.Kind().String(), nf, err) + } + } + } + case reflect.Array: + srcLen := src.Len() + if srcLen != dst.Len() { + return errors.ErrNotMatchArrayLength(fieldPath, dst.Len(), srcLen) + } + for i := 0; i < srcLen; i++ { + nf := fmt.Sprintf("%s[%d]", fieldPath, i) + if err = deepMerge(dst.Index(i), src.Index(i), visited, nf); err != nil { + return errors.ErrDeepMergeKind(dst.Kind().String(), nf, err) + } + } + case reflect.Map: + if dst.IsNil() { + dst.Set(reflect.MakeMapWithSize(dType, src.Len())) + } + dElem := dType.Elem() + for _, key := range src.MapKeys() { + vdst := dst.MapIndex(key) + // fmt.Println(vdst.IsValid(), key, vdst) + if !vdst.IsValid() { + vdst = reflect.New(dElem).Elem() + } + nf := fmt.Sprintf("%s[%s]", fieldPath, key) + if vdst.CanSet() { + if err = deepMerge(vdst, src.MapIndex(key), visited, nf); err != nil { + return errors.Errorf("error in array at %s: %w", nf, err) + } + dst.SetMapIndex(key, vdst) + } else { + dst.SetMapIndex(key, src.MapIndex(key)) + } + } + default: + if dst.CanSet() { + dst.Set(src) + } + } + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7009da1c48..2b577cb14f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -18,6 +18,7 @@ package config import ( + "encoding/json" "io/fs" "os" "reflect" @@ -1431,4 +1432,461 @@ func TestToRawYaml(t *testing.T) { } } -// NOT IMPLEMENTED BELOW +func TestMerge(t *testing.T) { + t.Parallel() + type config struct { + Discoverer *Discoverer + } + + type args struct { + objs []*config + } + type want struct { + wantDst *config + err error + } + type test struct { + name string + args args + want want + checkFunc func(want, *config, error) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, gotDst *config, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if !reflect.DeepEqual(gotDst, w.wantDst) { + gb, _ := json.Marshal(gotDst) + wb, _ := json.Marshal(w.wantDst) + return errors.Errorf("got: \"%s\",\n\t\t\t\twant: \"%s\"", string(gb), string(wb)) + } + return nil + } + defaultBeforeFunc := func(t *testing.T, _ args) { + t.Helper() + } + defaultAfterFunc := func(t *testing.T, _ args) { + t.Helper() + } + + // dst + dst := &config{ + Discoverer: &Discoverer{ + Name: "dst", + Namespace: "dst", + DiscoveryDuration: "1m", + Net: &Net{ + DNS: &DNS{ + RefreshDuration: "2s", + CacheExpiration: "10s", + }, + Dialer: &Dialer{ + Timeout: "2s", + Keepalive: "1m", + FallbackDelay: "2s", + DualStackEnabled: true, + }, + SocketOption: &SocketOption{ + ReusePort: true, + ReuseAddr: true, + TCPFastOpen: false, + TCPNoDelay: true, + TCPCork: true, + TCPQuickAck: false, + TCPDeferAccept: true, + IPTransparent: true, + IPRecoverDestinationAddr: true, + }, + TLS: &TLS{ + Enabled: false, + Cert: "/path/to/cert", + Key: "/path/to/key", + CA: "/path/to/ca", + InsecureSkipVerify: false, + }, + }, + Selectors: &Selectors{ + Pod: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/pod": "dst", + }, + Fields: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/pod": "dst", + }, + }, + Node: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/node": "dst", + }, + Fields: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/node": "dst", + }, + }, + NodeMetrics: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/node": "dst", + }, + Fields: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/node": "dst", + }, + }, + PodMetrics: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/pod": "dst", + }, + Fields: map[string]string{ + "vald.vdaas.org": "dst", + "vald.vdaas.org/pod": "dst", + }, + }, + }, + }, + } + // src + src := &config{ + Discoverer: &Discoverer{ + Name: "src", + Namespace: "src", + DiscoveryDuration: "10m", + Net: &Net{ + DNS: &DNS{ + RefreshDuration: "20s", + CacheExpiration: "1s", + }, + Dialer: &Dialer{ + Timeout: "20s", + Keepalive: "10m", + FallbackDelay: "20s", + DualStackEnabled: true, + }, + SocketOption: &SocketOption{ + TCPFastOpen: true, + }, + TLS: &TLS{ + Cert: "/path/to/cert", + Key: "/path/to/key", + CA: "/path/to/ca", + InsecureSkipVerify: false, + }, + }, + Selectors: &Selectors{ + Pod: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + }, + Node: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/src": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/node": "src", + }, + }, + NodeMetrics: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/node": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/node": "src", + }, + }, + PodMetrics: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + }, + }, + }, + } + w := &config{ + Discoverer: &Discoverer{ + Name: "src", + Namespace: "src", + DiscoveryDuration: "10m", + Net: &Net{ + DNS: &DNS{ + CacheEnabled: false, + RefreshDuration: "20s", + CacheExpiration: "1s", + }, + Dialer: &Dialer{ + Timeout: "20s", + Keepalive: "10m", + FallbackDelay: "20s", + DualStackEnabled: true, + }, + SocketOption: &SocketOption{ + ReusePort: true, + ReuseAddr: true, + TCPFastOpen: true, + TCPNoDelay: true, + TCPCork: true, + TCPQuickAck: false, + TCPDeferAccept: true, + IPTransparent: true, + IPRecoverDestinationAddr: true, + }, + TLS: &TLS{ + Enabled: false, + Cert: "/path/to/cert", + Key: "/path/to/key", + CA: "/path/to/ca", + InsecureSkipVerify: false, + }, + }, + Selectors: &Selectors{ + Pod: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + }, + Node: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/node": "dst", + "vald.vdaas.org/src": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/node": "src", + }, + }, + NodeMetrics: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/node": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/node": "src", + }, + }, + PodMetrics: &Selector{ + Labels: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + Fields: map[string]string{ + "vald.vdaas.org": "src", + "vald.vdaas.org/pod": "src", + }, + }, + }, + }, + } + + tests := []test{ + { + name: "return nil config when len(objs) is 0.", + args: args{ + objs: []*config{}, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: defaultBeforeFunc, + afterFunc: defaultAfterFunc, + }, + { + name: "return dst config when len(objs) is 1.", + args: args{ + objs: []*config{ + dst, + }, + }, + want: want{ + wantDst: dst, + }, + checkFunc: defaultCheckFunc, + beforeFunc: defaultBeforeFunc, + afterFunc: defaultAfterFunc, + }, + { + name: "return merged config when len(objs) is 2.", + args: args{ + objs: []*config{ + dst, + src, + }, + }, + want: want{ + wantDst: w, + }, + checkFunc: defaultCheckFunc, + beforeFunc: defaultBeforeFunc, + afterFunc: defaultAfterFunc, + }, + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + gotDst, err := Merge(test.args.objs...) + t.Log("err: \t", err, "\n\t", gotDst, "\n\t", test.want.wantDst) + if err := checkFunc(test.want, gotDst, err); err != nil { + tt.Errorf("error: \n\t\t\t\t%v", err) + } + }) + } +} + +func Test_deepMerge(t *testing.T) { + t.Parallel() + type config struct { + Slice []int + GlobalConfig + } + type args struct { + dst reflect.Value + src reflect.Value + visited map[uintptr]bool + fieldPath string + } + type want struct { + err error + } + type test struct { + name string + args args + want want + checkFunc func(want, error) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + return nil + } + defaultBeforeFunc := func(t *testing.T, _ args) { + t.Helper() + } + defaultAfterFunc := func(t *testing.T, _ args) { + t.Helper() + } + tests := []test{ + func() test { + dst := &config{ + GlobalConfig: GlobalConfig{ + Version: "v0.0.1", + TZ: "UTC", + Logging: &Logging{ + Logger: "glg", + Level: "debug", + Format: "raw", + }, + }, + } + src := &config{ + GlobalConfig: GlobalConfig{ + Version: "v1.0.1", + TZ: "JST", + Logging: &Logging{ + Logger: "glg", + Format: "json", + }, + }, + } + visited := make(map[uintptr]bool) + return test{ + name: "success merge struct by src", + args: args{ + dst: reflect.ValueOf(dst), + src: reflect.ValueOf(src), + visited: visited, + fieldPath: "", + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: defaultBeforeFunc, + afterFunc: defaultAfterFunc, + } + }(), + func() test { + dst := &config{ + Slice: []int{1, 2, 3}, + } + src := &config{ + Slice: []int{4, 5}, + } + visited := make(map[uintptr]bool) + return test{ + name: "success merge struct by slice", + args: args{ + dst: reflect.ValueOf(dst), + src: reflect.ValueOf(src), + visited: visited, + fieldPath: "", + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: defaultBeforeFunc, + afterFunc: defaultAfterFunc, + } + }(), + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + err := deepMerge(test.args.dst, test.args.src, test.args.visited, test.args.fieldPath) + if err := checkFunc(test.want, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} diff --git a/internal/config/server.go b/internal/config/server.go index 9ecc70a9d7..7ecb88ce7e 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -39,7 +39,7 @@ type Servers struct { // StartUpStrategy represent starting order of server name StartUpStrategy []string `json:"startup_strategy" yaml:"startup_strategy"` - // ShutdownStrategy represent shutdonw order of server name + // ShutdownStrategy represent shutdown order of server name ShutdownStrategy []string `json:"shutdown_strategy" yaml:"shutdown_strategy"` // FullShutdownDuration represent summary duration of shutdown time diff --git a/internal/errors/benchmark.go b/internal/errors/benchmark.go index 77328c4240..e73f888009 100644 --- a/internal/errors/benchmark.go +++ b/internal/errors/benchmark.go @@ -17,4 +17,21 @@ // Package errors provides benchmark error package errors -var ErrInvalidCoreMode = New("invalid core mode") +var ( + ErrInvalidCoreMode = New("invalid core mode") + + // ErrFailedToCreateBenchmarkJob represents a function to generate an error that failed to create benchmark job crd. + ErrFailedToCreateBenchmarkJob = func(err error, jn string) error { + return Wrapf(err, "could not create benchmark job resource: %s ", jn) + } + + // ErrFailedToCreateJob represents a function to generate an error that failed to create job resource. + ErrFailedToCreateJob = func(err error, jn string) error { + return Wrapf(err, "could not create job: %s ", jn) + } + + // ErrMismatchBenchmarkAtomics represents a function to generate an error that mismatch each atomic.Pointer stored corresponding to benchmark tasks. + ErrMismatchBenchmarkAtomics = func(job, benchjob, benchscenario interface{}) error { + return Errorf("mismatch atomics: job=%v\tbenchjob=%v\tbenchscenario=%v", job, benchjob, benchscenario) + } +) diff --git a/internal/errors/config.go b/internal/errors/config.go index a9f52a40a2..a80f43b5fb 100644 --- a/internal/errors/config.go +++ b/internal/errors/config.go @@ -17,10 +17,28 @@ // Package errors provides error types and function package errors +import "reflect" + var ( ErrInvalidConfig = New("component config is invalid") ErrUnsupportedConfigFileType = func(ext string) error { return Errorf("unsupported file type: %s", ext) } + + ErrNotMatchFieldType = func(path string, dType, sType reflect.Type) error { + return Errorf("types do not match at %s: %v vs %v", path, dType, sType) + } + + ErrNotMatchFieldNum = func(path string, dNum, sNum int) error { + return Errorf("number of fields do not match at %s, dst: %d, src: %d", path, dNum, sNum) + } + + ErrNotMatchArrayLength = func(path string, dLen, sLen int) error { + return Errorf("array length do not match at %s, dst: %d, src: %d", path, dLen, sLen) + } + + ErrDeepMergeKind = func(kind string, nf string, err error) error { + return Errorf("error in %s at %s: %w", kind, nf, err) + } ) diff --git a/internal/errors/http.go b/internal/errors/http.go index 4776a9c4ea..eec8b6e7f9 100644 --- a/internal/errors/http.go +++ b/internal/errors/http.go @@ -53,4 +53,9 @@ var ( // ErrTransportRetryable represents an error that the transport is retryable. ErrTransportRetryable = New("transport is retryable") + + // ErrInvalidStatusCode represents a function to generate an error that the http status code is invalid. + ErrInvalidStatusCode = func(code int) error { + return Errorf("invalid status code: %d", code) + } ) diff --git a/internal/errors/http_test.go b/internal/errors/http_test.go index 71e01b0fdf..52628913c8 100644 --- a/internal/errors/http_test.go +++ b/internal/errors/http_test.go @@ -526,4 +526,65 @@ func TestErrTransportRetryable(t *testing.T) { } } +func TestErrInvalidStatusCode(t *testing.T) { + type args struct { + code int + } + type want struct { + want error + } + type test struct { + name string + args args + want want + checkFunc func(want, error) error + beforeFunc func() + afterFunc func() + } + + defaultCheckFunc := func(w want, got error) error { + if !Is(got, w.want) { + return Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + { + name: "return ErrInvalidStatusCode error when code is empty", + want: want{ + want: New("invalid status code: 0"), + }, + }, + { + name: "return ErrInvalidStatusCode error when code is 500", + args: args{ + code: 500, + }, + want: want{ + want: New("invalid status code: 500"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + if test.beforeFunc != nil { + test.beforeFunc() + } + if test.afterFunc != nil { + defer test.afterFunc() + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got := ErrInvalidStatusCode(test.args.code) + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + // NOT IMPLEMENTED BELOW diff --git a/internal/k8s/client/client.go b/internal/k8s/client/client.go index 3063d7e528..65b05c5696 100644 --- a/internal/k8s/client/client.go +++ b/internal/k8s/client/client.go @@ -1,16 +1,20 @@ +// // Copyright (C) 2019-2024 vdaas.org vald team // // 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 // -// https://www.apache.org/licenses/LICENSE-2.0 +// https://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 client is Kubernetes client for getting resource from Kubernetes cluster. package client import ( @@ -89,7 +93,6 @@ func New(opts ...Option) (Client, error) { if c.scheme == nil { c.scheme = runtime.NewScheme() } - for _, opt := range opts { if err := opt(c); err != nil { return nil, err @@ -103,7 +106,6 @@ func New(opts ...Option) (Client, error) { if err := snapshotv1.AddToScheme(c.scheme); err != nil { return nil, err } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), manager.Options{ Scheme: c.scheme, }) @@ -153,7 +155,7 @@ func (c *client) Watch(ctx context.Context, obj cli.ObjectList, opts ...ListOpti return c.withWatch.Watch(ctx, obj, opts...) } -func (c *client) LabelSelector(key string, op selection.Operator, vals []string) (labels.Selector, error) { +func (*client) LabelSelector(key string, op selection.Operator, vals []string) (labels.Selector, error) { requirements, err := labels.NewRequirement(key, op, vals) if err != nil { return nil, fmt.Errorf("failed to create requirement on creating label selector: %w", err) diff --git a/internal/k8s/job/job.go b/internal/k8s/job/job.go new file mode 100644 index 0000000000..b71c386870 --- /dev/null +++ b/internal/k8s/job/job.go @@ -0,0 +1,162 @@ +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 job + +import ( + "context" + "reflect" + "strings" + "sync" + "time" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/k8s" + "github.com/vdaas/vald/internal/log" + batchv1 "k8s.io/api/batch/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// JobWatcher is a type alias for k8s resource controller. +type JobWatcher k8s.ResourceController + +type reconciler struct { + mgr manager.Manager + name string + namespaces []string + onError func(err error) + onReconcile func(ctx context.Context, jobList map[string][]Job) + listOpts []client.ListOption + jobsByAppNamePool sync.Pool // map[app][]Job +} + +// Job is a type alias for the k8s job definition. +type Job = batchv1.Job + +// JobStatus is a type alias for the k8s job status definition. +type JobStatus = batchv1.JobStatus + +// New returns the JobWatcher that implements reconciliation loop, or any errors occurred. +func New(opts ...Option) (JobWatcher, error) { + r := &reconciler{ + jobsByAppNamePool: sync.Pool{ + New: func() interface{} { + return make(map[string][]Job) + }, + }, + } + + for _, opt := range append(defaultOpts, opts...) { + if err := opt(r); err != nil { + return nil, errors.ErrOptionFailed(err, reflect.ValueOf(opt)) + } + } + + if len(r.namespaces) != 0 { + r.listOpts = make([]client.ListOption, 0, len(r.namespaces)) + for _, ns := range r.namespaces { + r.listOpts = append(r.listOpts, client.InNamespace(ns)) + } + } + + return r, nil +} + +// Reconcile implements k8s reconciliation loop to retrieve the Job information from k8s. +func (r *reconciler) Reconcile(ctx context.Context, _ reconcile.Request) (res reconcile.Result, err error) { + js := new(batchv1.JobList) + + err = r.mgr.GetClient().List(ctx, js, r.listOpts...) + if err != nil { + if r.onError != nil { + r.onError(err) + } + res = reconcile.Result{ + Requeue: true, + RequeueAfter: time.Millisecond * 100, + } + if k8serrors.IsNotFound(err) { + log.Error("not found", err) + return reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second, + }, nil + } + return + } + + jobs := r.jobsByAppNamePool.Get().(map[string][]Job) + for idx := range js.Items { + job := js.Items[idx] + name, ok := job.GetObjectMeta().GetLabels()["app"] + if !ok { + jns := strings.Split(job.GetName(), "-") + name = strings.Join(jns[:len(jns)-1], "-") + } + + if _, ok := jobs[name]; !ok { + jobs[name] = make([]Job, 0, len(js.Items)) + } + jobs[name] = append(jobs[name], job) + } + + if r.onReconcile != nil { + r.onReconcile(ctx, jobs) + } + + for name := range jobs { + jobs[name] = jobs[name][:0:len(jobs[name])] + } + + r.jobsByAppNamePool.Put(jobs) + + return +} + +// GetName returns the name of resource controller. +func (r *reconciler) GetName() string { + return r.name +} + +// NewReconciler returns the reconciler for the Job. +func (r *reconciler) NewReconciler(_ context.Context, mgr manager.Manager) reconcile.Reconciler { + if r.mgr == nil && mgr != nil { + r.mgr = mgr + } + + batchv1.AddToScheme(r.mgr.GetScheme()) + return r +} + +// For returns the runtime.Object which is job. +func (*reconciler) For() (client.Object, []builder.ForOption) { + return new(batchv1.Job), nil +} + +// Owns returns the owner of the job watcher. +// It will always return nil. +func (*reconciler) Owns() (client.Object, []builder.OwnsOption) { + return nil, nil +} + +// Watches returns the kind of the job and the event handler. +// It will always return nil. +func (*reconciler) Watches() (client.Object, handler.EventHandler, []builder.WatchesOption) { + // return &source.Kind{Type: new(corev1.Pod)}, &handler.EnqueueRequestForObject{} + return nil, nil, nil +} diff --git a/internal/k8s/job/option.go b/internal/k8s/job/option.go new file mode 100644 index 0000000000..b4849294bb --- /dev/null +++ b/internal/k8s/job/option.go @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 job + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// Option represents functional option for reconciler. +type Option func(*reconciler) error + +var defaultOpts = []Option{} + +// WithControllerName returns Option that sets r.name. +func WithControllerName(name string) Option { + return func(r *reconciler) error { + r.name = name + return nil + } +} + +// WithManager returns Option that sets r.mgr. +func WithManager(mgr manager.Manager) Option { + return func(r *reconciler) error { + r.mgr = mgr + return nil + } +} + +// WithNamespaces returns Option to set the namespace. +func WithNamespaces(nss ...string) Option { + return func(r *reconciler) error { + r.namespaces = nss + return nil + } +} + +// WithOnErrorFunc returns Option that sets r.onError. +func WithOnErrorFunc(f func(err error)) Option { + return func(r *reconciler) error { + r.onError = f + return nil + } +} + +// WithOnReconcileFunc returns Option that sets r.onReconcile. +func WithOnReconcileFunc(f func(ctx context.Context, jobList map[string][]Job)) Option { + return func(r *reconciler) error { + r.onReconcile = f + return nil + } +} diff --git a/internal/k8s/reconciler.go b/internal/k8s/reconciler.go index 0a065b8be0..83a79b660e 100644 --- a/internal/k8s/reconciler.go +++ b/internal/k8s/reconciler.go @@ -25,6 +25,7 @@ import ( "github.com/vdaas/vald/internal/net" "github.com/vdaas/vald/internal/safety" "github.com/vdaas/vald/internal/sync/errgroup" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,8 +35,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +type ( + Manager = manager.Manager + OwnerReference = v1.OwnerReference +) + type Controller interface { Start(ctx context.Context) (<-chan error, error) + GetManager() Manager } type ResourceController interface { @@ -135,3 +142,7 @@ func (c *controller) Start(ctx context.Context) (<-chan error, error) { return ech, nil } + +func (c *controller) GetManager() Manager { + return c.mgr +} diff --git a/internal/k8s/vald/benchmark/api/v1/info.go b/internal/k8s/vald/benchmark/api/v1/info.go new file mode 100644 index 0000000000..d1a4cbf62d --- /dev/null +++ b/internal/k8s/vald/benchmark/api/v1/info.go @@ -0,0 +1,40 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "vald.vdaas.org", Version: "v1"} + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register( + &ValdBenchmarkScenario{}, + &ValdBenchmarkScenarioList{}, + &ValdBenchmarkJob{}, + &ValdBenchmarkJobList{}, + ) +} diff --git a/internal/k8s/vald/benchmark/api/v1/job_types.go b/internal/k8s/vald/benchmark/api/v1/job_types.go new file mode 100644 index 0000000000..b6e07488cd --- /dev/null +++ b/internal/k8s/vald/benchmark/api/v1/job_types.go @@ -0,0 +1,234 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 v1 + +import ( + "github.com/vdaas/vald/internal/config" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type BenchmarkJobSpec struct { + *config.GlobalConfig `json:",omitempty" yaml:""` + ServerConfig *config.Servers `json:"server_config,omitempty" yaml:"server_config"` + Target *BenchmarkTarget `json:"target,omitempty" yaml:"target"` + Dataset *BenchmarkDataset `json:"dataset,omitempty" yaml:"dataset"` + Dimension int `json:"dimension,omitempty" yaml:"dimension"` + Replica int `json:"replica,omitempty" yaml:"replica"` + Repetition int `json:"repetition,omitempty" yaml:"repetition"` + JobType string `json:"job_type,omitempty" yaml:"job_type"` + InsertConfig *config.InsertConfig `json:"insert_config,omitempty" yaml:"insert_config"` + UpdateConfig *config.UpdateConfig `json:"update_config,omitempty" yaml:"update_config"` + UpsertConfig *config.UpsertConfig `json:"upsert_config,omitempty" yaml:"upsert_config"` + SearchConfig *config.SearchConfig `json:"search_config,omitempty" yaml:"search_config"` + RemoveConfig *config.RemoveConfig `json:"remove_config,omitempty" yaml:"remove_config"` + ObjectConfig *config.ObjectConfig `json:"object_config,omitempty" yaml:"object_config"` + ClientConfig *config.GRPCClient `json:"client_config,omitempty" yaml:"client_config"` + Rules []*config.BenchmarkJobRule `json:"rules,omitempty" yaml:"rules"` + RPS int `json:"rps,omitempty" yaml:"rps"` + ConcurrencyLimit int `json:"concurrency_limit,omitempty" yaml:"concurrency_limit"` + TTLSecondsAfterFinished int `json:"ttl_seconds_after_finished,omitempty" yaml:"ttl_seconds_after_finished"` +} + +type BenchmarkJobStatus string + +const ( + BenchmarkJobNotReady = BenchmarkJobStatus("NotReady") + BenchmarkJobCompleted = BenchmarkJobStatus("Completed") + BenchmarkJobAvailable = BenchmarkJobStatus("Available") + BenchmarkJobHealthy = BenchmarkJobStatus("Healthy") +) + +// BenchmarkTarget defines the desired state of BenchmarkTarget +type BenchmarkTarget config.BenchmarkTarget + +// BenchmarkDataset defines the desired state of BenchmarkDateset +type BenchmarkDataset config.BenchmarkDataset + +// BenchmarkDatasetRange defines the desired state of BenchmarkDatesetRange +type BenchmarkDatasetRange config.BenchmarkDatasetRange + +// BenchmarkJobRule defines the desired state of BenchmarkJobRule +type BenchmarkJobRule config.BenchmarkJobRule + +type ValdBenchmarkJob struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec BenchmarkJobSpec `json:"spec,omitempty"` + Status BenchmarkJobStatus `json:"status,omitempty"` +} + +type ValdBenchmarkJobList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Items []ValdBenchmarkJob `json:"items,omitempty"` +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BenchmarkDataset) DeepCopyInto(out *BenchmarkDataset) { + *out = *in + if in.Range != nil { + in, out := &in.Range, &out.Range + *out = new(config.BenchmarkDatasetRange) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkDataset. +func (in *BenchmarkDataset) DeepCopy() *BenchmarkDataset { + if in == nil { + return nil + } + out := new(BenchmarkDataset) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BenchmarkDatasetRange) DeepCopyInto(out *BenchmarkDatasetRange) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkDatasetRange. +func (in *BenchmarkDatasetRange) DeepCopy() *BenchmarkDatasetRange { + if in == nil { + return nil + } + out := new(BenchmarkDatasetRange) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BenchmarkJobRule) DeepCopyInto(out *BenchmarkJobRule) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkJobRule. +func (in *BenchmarkJobRule) DeepCopy() *BenchmarkJobRule { + if in == nil { + return nil + } + out := new(BenchmarkJobRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BenchmarkJobSpec) DeepCopyInto(out *BenchmarkJobSpec) { + *out = *in + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(BenchmarkTarget) + **out = **in + } + if in.Dataset != nil { + in, out := &in.Dataset, &out.Dataset + *out = new(BenchmarkDataset) + (*in).DeepCopyInto(*out) + } + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]*config.BenchmarkJobRule, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(config.BenchmarkJobRule) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkJobSpec. +func (in *BenchmarkJobSpec) DeepCopy() *BenchmarkJobSpec { + if in == nil { + return nil + } + out := new(BenchmarkJobSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BenchmarkTarget) DeepCopyInto(out *BenchmarkTarget) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkTarget. +func (in *BenchmarkTarget) DeepCopy() *BenchmarkTarget { + if in == nil { + return nil + } + out := new(BenchmarkTarget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValdBenchmarkJob) DeepCopyInto(out *ValdBenchmarkJob) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkOperator. +func (in *ValdBenchmarkJob) DeepCopy() *ValdBenchmarkJob { + if in == nil { + return nil + } + out := new(ValdBenchmarkJob) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ValdBenchmarkJob) DeepCopyObject() runtime.Object { + return in.DeepCopy() +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValdBenchmarkJobList) DeepCopyInto(out *ValdBenchmarkJobList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ValdBenchmarkJob, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkOperatorList. +func (in *ValdBenchmarkJobList) DeepCopy() *ValdBenchmarkJobList { + if in == nil { + return nil + } + out := new(ValdBenchmarkJobList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ValdBenchmarkJobList) DeepCopyObject() runtime.Object { + return in.DeepCopy() +} diff --git a/internal/k8s/vald/benchmark/api/v1/scenario_types.go b/internal/k8s/vald/benchmark/api/v1/scenario_types.go new file mode 100644 index 0000000000..54bfb50e3b --- /dev/null +++ b/internal/k8s/vald/benchmark/api/v1/scenario_types.go @@ -0,0 +1,139 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type ValdBenchmarkScenarioSpec struct { + Target *BenchmarkTarget `json:"target,omitempty"` + Dataset *BenchmarkDataset `json:"dataset,omitempty"` + Jobs []*BenchmarkJobSpec `json:"jobs,omitempty"` +} + +type ValdBenchmarkScenarioStatus string + +const ( + BenchmarkScenarioNotReady ValdBenchmarkScenarioStatus = "NotReady" + BenchmarkScenarioCompleted ValdBenchmarkScenarioStatus = "Completed" + BenchmarkScenarioAvailable ValdBenchmarkScenarioStatus = "Available" + BenchmarkScenarioHealthy ValdBenchmarkScenarioStatus = "Healthy" +) + +type ValdBenchmarkScenario struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ValdBenchmarkScenarioSpec `json:"spec,omitempty"` + Status ValdBenchmarkScenarioStatus `json:"status,omitempty"` +} + +type ValdBenchmarkScenarioList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ValdBenchmarkScenario `json:"items"` +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValdBenchmarkScenario) DeepCopyInto(out *ValdBenchmarkScenario) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkScenario. +func (in *ValdBenchmarkScenario) DeepCopy() *ValdBenchmarkScenario { + if in == nil { + return nil + } + out := new(ValdBenchmarkScenario) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ValdBenchmarkScenario) DeepCopyObject() runtime.Object { + return in.DeepCopy() +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValdBenchmarkScenarioList) DeepCopyInto(out *ValdBenchmarkScenarioList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ValdBenchmarkScenario, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkScenarioList. +func (in *ValdBenchmarkScenarioList) DeepCopy() *ValdBenchmarkScenarioList { + if in == nil { + return nil + } + out := new(ValdBenchmarkScenarioList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ValdBenchmarkScenarioList) DeepCopyObject() runtime.Object { + return in.DeepCopy() +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValdBenchmarkScenarioSpec) DeepCopyInto(out *ValdBenchmarkScenarioSpec) { + *out = *in + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(BenchmarkTarget) + **out = **in + } + if in.Dataset != nil { + in, out := &in.Dataset, &out.Dataset + *out = new(BenchmarkDataset) + (*in).DeepCopyInto(*out) + } + if in.Jobs != nil { + in, out := &in.Jobs, &out.Jobs + *out = make([]*BenchmarkJobSpec, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(BenchmarkJobSpec) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BenchmarkScenarioSpec. +func (in *ValdBenchmarkScenarioSpec) DeepCopy() *ValdBenchmarkScenarioSpec { + if in == nil { + return nil + } + out := new(ValdBenchmarkScenarioSpec) + in.DeepCopyInto(out) + return out +} diff --git a/internal/k8s/vald/benchmark/job/doc.go b/internal/k8s/vald/benchmark/job/doc.go new file mode 100644 index 0000000000..d2c7acf429 --- /dev/null +++ b/internal/k8s/vald/benchmark/job/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 job provides benchmark job crd information and preriodically update +package job diff --git a/internal/k8s/vald/benchmark/job/job.go b/internal/k8s/vald/benchmark/job/job.go new file mode 100644 index 0000000000..104184c508 --- /dev/null +++ b/internal/k8s/vald/benchmark/job/job.go @@ -0,0 +1,139 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 job + +import ( + "context" + "reflect" + "time" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/k8s" + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + "github.com/vdaas/vald/internal/log" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +type BenchmarkJobWatcher k8s.ResourceController + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "vald.vdaas.org", Version: "v1"} + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +type reconciler struct { + mgr manager.Manager + name string + namespaces []string + onError func(err error) + onReconcile func(ctx context.Context, jobList map[string]v1.ValdBenchmarkJob) + lopts []client.ListOption +} + +func New(opts ...Option) (BenchmarkJobWatcher, error) { + r := new(reconciler) + for _, opt := range append(defaultOpts, opts...) { + err := opt(r) + if err != nil { + return nil, errors.ErrOptionFailed(err, reflect.ValueOf(opt)) + } + } + return r, nil +} + +func (r *reconciler) AddListOpts(opt client.ListOption) { + if opt == nil { + return + } + if r.lopts == nil { + r.lopts = make([]client.ListOption, 0, 1) + } + r.lopts = append(r.lopts, opt) +} + +func (r *reconciler) Reconcile(ctx context.Context, _ reconcile.Request) (res reconcile.Result, err error) { + bj := new(v1.ValdBenchmarkJobList) + + err = r.mgr.GetClient().List(ctx, bj, r.lopts...) + + if err != nil { + if r.onError != nil { + r.onError(err) + } + res = reconcile.Result{ + Requeue: true, + RequeueAfter: time.Millisecond * 100, + } + if k8serrors.IsNotFound(err) { + log.Errorf("not found: %s", err) + return reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second, + }, nil + } + return + } + + jobs := make(map[string]v1.ValdBenchmarkJob, 0) + for _, item := range bj.Items { + name := item.GetName() + jobs[name] = item + } + + if r.onReconcile != nil { + r.onReconcile(ctx, jobs) + } + return +} + +func (r *reconciler) GetName() string { + return r.name +} + +func (r *reconciler) NewReconciler(_ context.Context, mgr manager.Manager) reconcile.Reconciler { + if r.mgr == nil && mgr != nil { + r.mgr = mgr + } + + v1.AddToScheme(r.mgr.GetScheme()) + + return r +} + +func (*reconciler) For() (client.Object, []builder.ForOption) { + return new(v1.ValdBenchmarkJob), nil +} + +func (*reconciler) Owns() (client.Object, []builder.OwnsOption) { + return nil, nil +} + +func (*reconciler) Watches() (client.Object, handler.EventHandler, []builder.WatchesOption) { + // return &source.Kind{Type: new(corev1.Pod)}, &handler.EnqueueRequestForObject{} + return nil, nil, nil +} diff --git a/internal/k8s/vald/benchmark/job/job_template.go b/internal/k8s/vald/benchmark/job/job_template.go new file mode 100644 index 0000000000..b93866959c --- /dev/null +++ b/internal/k8s/vald/benchmark/job/job_template.go @@ -0,0 +1,145 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 job manages the main logic of benchmark job. +package job + +import ( + jobs "github.com/vdaas/vald/internal/k8s/job" + corev1 "k8s.io/api/core/v1" +) + +type ( + ImagePullPolicy corev1.PullPolicy + RestartPolicy corev1.RestartPolicy +) + +const ( + PullAlways ImagePullPolicy = "Always" + PullNever ImagePullPolicy = "Never" + PullIfNotPresent ImagePullPolicy = "PullIfNotPresent" + + RestartPolicyAlways RestartPolicy = "Always" + RestartPolicyOnFailure RestartPolicy = "OnFailure" + RestartPolicyNever RestartPolicy = "Never" +) + +const ( + svcAccount = "vald-benchmark-operator" +) + +type BenchmarkJobTpl interface { + CreateJobTpl(opts ...BenchmarkJobOption) (jobs.Job, error) +} + +type benchmarkJobTpl struct { + containerName string + containerImageName string + imagePullPolicy ImagePullPolicy + jobTpl jobs.Job +} + +func NewBenchmarkJob(opts ...BenchmarkJobTplOption) (BenchmarkJobTpl, error) { + bjTpl := new(benchmarkJobTpl) + for _, opt := range append(defaultBenchmarkJobTplOpts, opts...) { + err := opt(bjTpl) + if err != nil { + return nil, err + } + } + return bjTpl, nil +} + +func (b *benchmarkJobTpl) CreateJobTpl(opts ...BenchmarkJobOption) (jobs.Job, error) { + for _, opt := range append(defaultBenchmarkJobOpts, opts...) { + err := opt(&b.jobTpl) + if err != nil { + return b.jobTpl, err + } + } + // TODO: check enable pprof flag + b.jobTpl.Spec.Template.Annotations = map[string]string{ + "pyroscope.io/scrape": "true", + "pyroscope.io/application-name": "benchmark-job", + "pyroscope.io/profile-cpu-enabled": "true", + "pyroscope.io/profile-mem-enabled": "true", + "pyroscope.io/port": "6060", + } + b.jobTpl.Spec.Template.Spec.Containers = []corev1.Container{ + { + Name: b.containerName, + Image: b.containerImageName, + ImagePullPolicy: corev1.PullPolicy(b.imagePullPolicy), + LivenessProbe: &corev1.Probe{ + InitialDelaySeconds: int32(60), + PeriodSeconds: int32(10), + TimeoutSeconds: int32(300), + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "/go/bin/job", + "-v", + }, + }, + }, + }, + StartupProbe: &corev1.Probe{ + FailureThreshold: int32(30), + PeriodSeconds: int32(10), + TimeoutSeconds: int32(300), + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "/go/bin/job", + "-v", + }, + }, + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "liveness", + Protocol: corev1.ProtocolTCP, + ContainerPort: int32(3000), + }, + { + Name: "readiness", + Protocol: corev1.ProtocolTCP, + ContainerPort: int32(3001), + }, + }, + Env: []corev1.EnvVar{ + { + Name: "CRD_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + { + Name: "CRD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.labels['job-name']", + }, + }, + }, + }, + }, + } + return b.jobTpl, nil +} diff --git a/internal/k8s/vald/benchmark/job/job_template_option.go b/internal/k8s/vald/benchmark/job/job_template_option.go new file mode 100644 index 0000000000..861c5c6d17 --- /dev/null +++ b/internal/k8s/vald/benchmark/job/job_template_option.go @@ -0,0 +1,168 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 job + +import ( + "github.com/vdaas/vald/internal/k8s" + jobs "github.com/vdaas/vald/internal/k8s/job" + corev1 "k8s.io/api/core/v1" +) + +type BenchmarkJobTplOption func(b *benchmarkJobTpl) error + +var defaultBenchmarkJobTplOpts = []BenchmarkJobTplOption{ + WithContainerName("vald-benchmark-job"), + WithContainerImage("vdaas/vald-benchmark-job"), + WithImagePullPolicy(PullAlways), +} + +// WithContainerName sets the docker container name of benchmark job. +func WithContainerName(name string) BenchmarkJobTplOption { + return func(b *benchmarkJobTpl) error { + if len(name) > 0 { + b.containerName = name + } + return nil + } +} + +// WithContainerImage sets the docker image path for benchmark job. +func WithContainerImage(name string) BenchmarkJobTplOption { + return func(b *benchmarkJobTpl) error { + if len(name) > 0 { + b.containerImageName = name + } + return nil + } +} + +// WithImagePullPolicy sets the docker image pull policy for benchmark job. +func WithImagePullPolicy(p ImagePullPolicy) BenchmarkJobTplOption { + return func(b *benchmarkJobTpl) error { + if len(p) == 0 { + return nil + } + b.imagePullPolicy = p + return nil + } +} + +// BenchmarkJobOption represents the option for create benchmark job template. +type BenchmarkJobOption func(b *jobs.Job) error + +// defaultTTLSeconds represents the default TTLSecondsAfterFinished for benchmark job template. +const defaultTTLSeconds int32 = 600 + +var defaultBenchmarkJobOpts = []BenchmarkJobOption{ + WithSvcAccountName(svcAccount), + WithRestartPolicy(RestartPolicyNever), + WithTTLSecondsAfterFinished(defaultTTLSeconds), +} + +// WithSvcAccountName sets the service account name for benchmark job. +func WithSvcAccountName(name string) BenchmarkJobOption { + return func(b *jobs.Job) error { + if len(name) > 0 { + b.Spec.Template.Spec.ServiceAccountName = name + } + return nil + } +} + +// WithRestartPolicy sets the job restart policy for benchmark job. +func WithRestartPolicy(rp RestartPolicy) BenchmarkJobOption { + return func(b *jobs.Job) error { + if len(rp) > 0 { + b.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicy(rp) + } + return nil + } +} + +// WithBackoffLimit sets the job backoff limit for benchmark job. +func WithBackoffLimit(bo int32) BenchmarkJobOption { + return func(b *jobs.Job) error { + b.Spec.BackoffLimit = &bo + return nil + } +} + +// WithName sets the job name of benchmark job. +func WithName(name string) BenchmarkJobOption { + return func(b *jobs.Job) error { + b.Name = name + return nil + } +} + +// WithNamespace specify namespace where job will execute. +func WithNamespace(ns string) BenchmarkJobOption { + return func(b *jobs.Job) error { + b.Namespace = ns + return nil + } +} + +// WithOwnerRef sets the OwnerReference to the job resource. +func WithOwnerRef(refs []k8s.OwnerReference) BenchmarkJobOption { + return func(b *jobs.Job) error { + if len(refs) > 0 { + b.OwnerReferences = refs + } + return nil + } +} + +// WithCompletions sets the job completion. +func WithCompletions(com int32) BenchmarkJobOption { + return func(b *jobs.Job) error { + if com > 1 { + b.Spec.Completions = &com + } + return nil + } +} + +// WithParallelism sets the job parallelism. +func WithParallelism(parallelism int32) BenchmarkJobOption { + return func(b *jobs.Job) error { + if parallelism > 1 { + b.Spec.Parallelism = ¶llelism + } + return nil + } +} + +// WithLabel sets the label to the job resource. +func WithLabel(label map[string]string) BenchmarkJobOption { + return func(b *jobs.Job) error { + if len(label) > 0 { + b.Labels = label + } + return nil + } +} + +// WithTTLSecondsAfterFinished sets the TTLSecondsAfterFinished to the job template. +func WithTTLSecondsAfterFinished(ttl int32) BenchmarkJobOption { + return func(b *jobs.Job) error { + if ttl > 0 { + b.Spec.TTLSecondsAfterFinished = &ttl + } + return nil + } +} diff --git a/internal/k8s/vald/benchmark/job/option.go b/internal/k8s/vald/benchmark/job/option.go new file mode 100644 index 0000000000..f0d231f153 --- /dev/null +++ b/internal/k8s/vald/benchmark/job/option.go @@ -0,0 +1,68 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 job + +import ( + "context" + + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type Option func(*reconciler) error + +var defaultOpts = []Option{} + +// WithControllerName returns Option that sets r.name. +func WithControllerName(name string) Option { + return func(r *reconciler) error { + r.name = name + return nil + } +} + +// WithManager returns Option that sets r.mgr. +func WithManager(mgr manager.Manager) Option { + return func(r *reconciler) error { + r.mgr = mgr + return nil + } +} + +// WithNamespaces returns Option to set the namespace. +func WithNamespaces(nss ...string) Option { + return func(r *reconciler) error { + r.namespaces = nss + return nil + } +} + +// WithOnErrorFunc returns Option that sets r.onError. +func WithOnErrorFunc(f func(err error)) Option { + return func(r *reconciler) error { + r.onError = f + return nil + } +} + +// WithOnReconcileFunc returns Option that sets r.onReconcile. +func WithOnReconcileFunc(f func(ctx context.Context, jobList map[string]v1.ValdBenchmarkJob)) Option { + return func(r *reconciler) error { + r.onReconcile = f + return nil + } +} diff --git a/internal/k8s/vald/benchmark/scenario/doc.go b/internal/k8s/vald/benchmark/scenario/doc.go new file mode 100644 index 0000000000..78ba29c359 --- /dev/null +++ b/internal/k8s/vald/benchmark/scenario/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 operator provides benchmark operator crd information and preriodically update +package scenario diff --git a/internal/k8s/vald/benchmark/scenario/option.go b/internal/k8s/vald/benchmark/scenario/option.go new file mode 100644 index 0000000000..6a1b7d0e26 --- /dev/null +++ b/internal/k8s/vald/benchmark/scenario/option.go @@ -0,0 +1,68 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 scenario + +import ( + "context" + + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type Option func(*reconciler) error + +var defaultOpts = []Option{} + +// WithControllerName returns Option that sets r.name. +func WithControllerName(name string) Option { + return func(r *reconciler) error { + r.name = name + return nil + } +} + +// WithManager returns Option that sets r.mgr. +func WithManager(mgr manager.Manager) Option { + return func(r *reconciler) error { + r.mgr = mgr + return nil + } +} + +// WithNamespaces returns Option to set the namespace. +func WithNamespaces(nss ...string) Option { + return func(r *reconciler) error { + r.namespaces = nss + return nil + } +} + +// WithOnErrorFunc returns Option that sets r.onError. +func WithOnErrorFunc(f func(err error)) Option { + return func(r *reconciler) error { + r.onError = f + return nil + } +} + +// WithOnReconcileFunc returns Option that sets r.onReconcile. +func WithOnReconcileFunc(f func(ctx context.Context, scenarioList map[string]v1.ValdBenchmarkScenario)) Option { + return func(r *reconciler) error { + r.onReconcile = f + return nil + } +} diff --git a/internal/k8s/vald/benchmark/scenario/scenario.go b/internal/k8s/vald/benchmark/scenario/scenario.go new file mode 100644 index 0000000000..19e9ee655d --- /dev/null +++ b/internal/k8s/vald/benchmark/scenario/scenario.go @@ -0,0 +1,128 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 scenario + +import ( + "context" + "reflect" + "time" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/k8s" + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + "github.com/vdaas/vald/internal/log" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type BenchmarkScenarioWatcher k8s.ResourceController + +type reconciler struct { + mgr manager.Manager + name string + namespaces []string + onError func(err error) + onReconcile func(ctx context.Context, operatorList map[string]v1.ValdBenchmarkScenario) + lopts []client.ListOption +} + +func New(opts ...Option) (BenchmarkScenarioWatcher, error) { + r := new(reconciler) + for _, opt := range append(defaultOpts, opts...) { + err := opt(r) + if err != nil { + return nil, errors.ErrOptionFailed(err, reflect.ValueOf(opt)) + } + } + return r, nil +} + +func (r *reconciler) AddListOpts(opt client.ListOption) { + if opt == nil { + return + } + if r.lopts == nil { + r.lopts = make([]client.ListOption, 0, 1) + } + r.lopts = append(r.lopts, opt) +} + +func (r *reconciler) Reconcile(ctx context.Context, _ reconcile.Request) (res reconcile.Result, err error) { + bs := new(v1.ValdBenchmarkScenarioList) + + err = r.mgr.GetClient().List(ctx, bs, r.lopts...) + + if err != nil { + if r.onError != nil { + r.onError(err) + } + res = reconcile.Result{ + Requeue: true, + RequeueAfter: time.Millisecond * 100, + } + if k8serrors.IsNotFound(err) { + log.Errorf("not found: %s", err) + return reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second, + }, nil + } + return + } + scenarios := make(map[string]v1.ValdBenchmarkScenario, 0) + for _, item := range bs.Items { + name := item.Name + scenarios[name] = item + } + + if r.onReconcile != nil { + r.onReconcile(ctx, scenarios) + } + + return +} + +func (r *reconciler) GetName() string { + return r.name +} + +func (r *reconciler) NewReconciler(_ context.Context, mgr manager.Manager) reconcile.Reconciler { + if r.mgr == nil && mgr != nil { + r.mgr = mgr + } + v1.AddToScheme(r.mgr.GetScheme()) + + return r +} + +func (*reconciler) For() (client.Object, []builder.ForOption) { + return new(v1.ValdBenchmarkScenario), nil +} + +func (*reconciler) Owns() (client.Object, []builder.OwnsOption) { + return nil, nil +} + +func (*reconciler) Watches() (client.Object, handler.EventHandler, []builder.WatchesOption) { + // return &source.Kind{Type: new(corev1.Pod)}, &handler.EnqueueRequestForObject{} + // return &source.Kind{Type: new(v1.ValdBenchmarkScenario)}, &handler.EnqueueRequestForObject{}, nil + return nil, &handler.EnqueueRequestForObject{}, nil +} diff --git a/internal/sync/errgroup/group.go b/internal/sync/errgroup/group.go index bf216454ef..974bf56957 100644 --- a/internal/sync/errgroup/group.go +++ b/internal/sync/errgroup/group.go @@ -21,9 +21,8 @@ import ( "context" "runtime" - "github.com/vdaas/vald/internal/sync" - "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/sync" "github.com/vdaas/vald/internal/sync/semaphore" ) diff --git a/internal/test/data/hdf5/doc.go b/internal/test/data/hdf5/doc.go new file mode 100644 index 0000000000..4266f7a19c --- /dev/null +++ b/internal/test/data/hdf5/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 hdf5 is load hdf5 file +package hdf5 diff --git a/internal/test/data/hdf5/hdf5.go b/internal/test/data/hdf5/hdf5.go new file mode 100644 index 0000000000..1dd21039d2 --- /dev/null +++ b/internal/test/data/hdf5/hdf5.go @@ -0,0 +1,297 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 hdf5 is load hdf5 file +package hdf5 + +import ( + "os" + "reflect" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/io" + "github.com/vdaas/vald/internal/net/http/client" + "gonum.org/v1/hdf5" +) + +type Data interface { + Download(url string) error + Read(key Hdf5Key) error + GetName() DatasetName + GetPath() string + GetByGroupName(name string) [][]float32 + GetTrain() [][]float32 + GetTest() [][]float32 + GetNeighbors() [][]int +} + +type DatasetName int + +const ( + Original DatasetName = iota + FashionMNIST784Euclidean +) + +func (d DatasetName) String() string { + switch d { + case Original: + return "original" + case FashionMNIST784Euclidean: + return "fashion-mnist" + default: + return "" + } +} + +type DatasetUrl int + +const ( + FashionMNIST784EuclideanUrl DatasetUrl = iota +) + +func (d DatasetUrl) String() string { + switch d { + case FashionMNIST784EuclideanUrl: + return "http://ann-benchmarks.com/fashion-mnist-784-euclidean.hdf5" + default: + return "" + } +} + +type Hdf5Key int + +const ( + Train Hdf5Key = iota + 1 + Test + Neighors +) + +func (key Hdf5Key) String() string { + switch key { + case Train: + return "train" + case Test: + return "test" + case Neighors: + return "neighbors" + default: + return "" + } +} + +type data struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int +} + +func New(opts ...Option) (Data, error) { + d := new(data) + for _, opt := range append(defaultOptions, opts...) { + if err := opt(d); err != nil { + return nil, errors.ErrOptionFailed(err, reflect.ValueOf(opt)) + } + } + return d, nil +} + +// Get downloads the hdf5 file. +// https://github.com/erikbern/ann-benchmarks/#data-sets +func (d *data) Download(url string) error { + switch d.name { + case Original: + return downloadFile(url, d.path) + case FashionMNIST784Euclidean: + return downloadFile(FashionMNIST784EuclideanUrl.String(), d.path) + default: + return errors.NewErrInvalidOption("name", d.name) + } +} + +func (d *data) Read(key Hdf5Key) error { + f, err := hdf5.OpenFile(d.path, hdf5.F_ACC_RDONLY) + if err != nil { + return err + } + defer f.Close() + + if key != Test { + // load training data + train, err := ReadDatasetF32(f, Train) + if err != nil { + return err + } + d.train = train + } + if key != Train { + // load test data + test, err := ReadDatasetF32(f, Test) + if err != nil { + return err + } + d.test = test + } + + // load neighbors + neighbors32, err := ReadDatasetI32(f, Neighors) + if err != nil { + return err + } + neighbors := make([][]int, len(neighbors32)) + for i, ns := range neighbors32 { + neighbors[i] = make([]int, len(ns)) + for j, n := range ns { + neighbors[i][j] = int(n) + } + } + d.neighbors = neighbors + + return nil +} + +func (d *data) GetName() DatasetName { + return d.name +} + +func (d *data) GetPath() string { + return d.path +} + +// TODO: Apply generics +func (d *data) GetByGroupName(name string) [][]float32 { + switch name { + case "train": + return d.GetTrain() + case "test": + return d.GetTest() + case "neighbors": + l := d.GetNeighbors() + r := make([][]float32, 0) + for x := range l { + for y, z := range l[x] { + r[x][y] = float32(z) + } + } + return r + default: + return nil + } +} + +func (d *data) GetTrain() [][]float32 { + return d.train +} + +func (d *data) GetTest() [][]float32 { + return d.test +} + +func (d *data) GetNeighbors() [][]int { + return d.neighbors +} + +func downloadFile(url, path string) error { + if len(path) == 0 { + return errors.NewErrInvalidOption("no path is specified", path) + } + cli, err := client.New() + if err != nil { + return err + } + + resp, err := cli.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.ErrInvalidStatusCode(resp.StatusCode) + } + + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + + return nil +} + +func ReadDatasetF32(file *hdf5.File, key Hdf5Key) ([][]float32, error) { + data, err := file.OpenDataset(key.String()) + if err != nil { + return nil, err + } + defer data.Close() + + dataspace := data.Space() + defer dataspace.Close() + + dims, _, err := dataspace.SimpleExtentDims() + if err != nil { + return nil, err + } + height, width := int(dims[0]), int(dims[1]) + + rawFloats := make([]float32, dataspace.SimpleExtentNPoints()) + if err := data.Read(&rawFloats); err != nil { + return nil, err + } + + vecs := make([][]float32, height) + for i := 0; i < height; i++ { + vecs[i] = rawFloats[i*width : i*width+width] + } + + return vecs, nil +} + +func ReadDatasetI32(file *hdf5.File, key Hdf5Key) ([][]int32, error) { + data, err := file.OpenDataset(key.String()) + if err != nil { + return nil, err + } + defer data.Close() + + dataspace := data.Space() + defer dataspace.Close() + + dims, _, err := dataspace.SimpleExtentDims() + if err != nil { + return nil, err + } + height, width := int(dims[0]), int(dims[1]) + + rawFloats := make([]int32, dataspace.SimpleExtentNPoints()) + if err := data.Read(&rawFloats); err != nil { + return nil, err + } + + vecs := make([][]int32, height) + for i := 0; i < height; i++ { + vecs[i] = rawFloats[i*width : i*width+width] + } + + return vecs, nil +} diff --git a/internal/test/data/hdf5/hdf5_test.go b/internal/test/data/hdf5/hdf5_test.go new file mode 100644 index 0000000000..6af704fb51 --- /dev/null +++ b/internal/test/data/hdf5/hdf5_test.go @@ -0,0 +1,1437 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 hdf5 is load hdf5 file +package hdf5 + +import ( + "log" + "os" + "reflect" + "testing" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/test/goleak" + "gonum.org/v1/hdf5" +) + +func TestDatasetName_String(t *testing.T) { + type want struct { + want string + } + type test struct { + name string + d DatasetName + want want + checkFunc func(want, string) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got string) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // { + // name: "set original", + // want: want{ + // want: "original", + // }, + // checkFunc: defaultCheckFunc, + // beforeFunc: func(t *testing.T,) { + // t.Helper() + // }, + // afterFunc: func(t *testing.T,) { + // t.Helper() + // }, + // }, + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got := test.d.String() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func TestDatasetUrl_String(t *testing.T) { + type want struct { + want string + } + type test struct { + name string + d DatasetUrl + want want + checkFunc func(want, string) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got string) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got := test.d.String() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_Hdf5Key_String(t *testing.T) { + type want struct { + want string + } + type test struct { + name string + h Hdf5Key + want want + checkFunc func(want, string) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got string) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got := test.h.String() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func TestNew(t *testing.T) { + type args struct { + opts []Option + } + type want struct { + want Data + err error + } + type test struct { + name string + args args + want want + checkFunc func(want, Data, error) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, got Data, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + args: args { + opts:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + args: args { + opts:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got, err := New(test.args.opts...) + if err := checkFunc(test.want, got, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_Download(t *testing.T) { + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type args struct { + url string + } + type want struct { + err error + } + type test struct { + name string + fields fields + args args + want want + checkFunc func(want, error) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + return nil + } + tests := []test{ + /* + { + name: "test_case_1", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + err := d.Download(test.args.url) + if err := checkFunc(test.want, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_Read(t *testing.T) { + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type args struct { + key Hdf5Key + } + type want struct { + err error + } + type test struct { + name string + fields fields + want want + args args + checkFunc func(want, error) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + err := d.Read(test.args.key) + if err := checkFunc(test.want, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_GetName(t *testing.T) { + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type want struct { + want DatasetName + } + type test struct { + name string + fields fields + want want + checkFunc func(want, DatasetName) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got DatasetName) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + got := d.GetName() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_GetPath(t *testing.T) { + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type want struct { + want string + } + type test struct { + name string + fields fields + want want + checkFunc func(want, string) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got string) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + got := d.GetPath() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_GetByGroupName(t *testing.T) { + type args struct { + name string + } + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type want struct { + want [][]float32 + } + type test struct { + name string + args args + fields fields + want want + checkFunc func(want, [][]float32) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, got [][]float32) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + args: args { + name:"", + }, + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + args: args { + name:"", + }, + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + got := d.GetByGroupName(test.args.name) + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_GetTrain(t *testing.T) { + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type want struct { + want [][]float32 + } + type test struct { + name string + fields fields + want want + checkFunc func(want, [][]float32) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got [][]float32) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + got := d.GetTrain() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_GetTest(t *testing.T) { + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type want struct { + want [][]float32 + } + type test struct { + name string + fields fields + want want + checkFunc func(want, [][]float32) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got [][]float32) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + got := d.GetTest() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_data_GetNeighbors(t *testing.T) { + type fields struct { + name DatasetName + path string + train [][]float32 + test [][]float32 + neighbors [][]int + } + type want struct { + want [][]int + } + type test struct { + name string + fields fields + want want + checkFunc func(want, [][]int) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got [][]int) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + fields: fields { + name:nil, + path:"", + train:nil, + test:nil, + neighbors:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T,) { + t.Helper() + }, + afterFunc: func(t *testing.T,) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + d := &data{ + name: test.fields.name, + path: test.fields.path, + train: test.fields.train, + test: test.fields.test, + neighbors: test.fields.neighbors, + } + + got := d.GetNeighbors() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_downloadFile(t *testing.T) { + type args struct { + url string + path string + } + type want struct { + err error + } + type test struct { + name string + args args + want want + checkFunc func(want, error) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultBeforeFunc := func(t *testing.T, _ args) { + t.Helper() + err := os.Mkdir("tmp", os.ModePerm) + if err != nil { + log.Fatal(err) + } + } + defaultAfterFunc := func(t *testing.T, _ args) { + t.Helper() + err := os.RemoveAll("tmp") + if err != nil { + log.Fatal(err) + } + } + defaultCheckFunc := func(w want, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + return nil + } + tests := []test{ + { + name: "success download hdf5 file", + args: args{ + url: "https://ann-benchmarks.com/fashion-mnist-784-euclidean.hdf5", + path: "./tmp/data", + }, + want: want{ + err: nil, + }, + beforeFunc: defaultBeforeFunc, + afterFunc: defaultAfterFunc, + checkFunc: defaultCheckFunc, + }, + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + err := downloadFile(test.args.url, test.args.path) + if err := checkFunc(test.want, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func TestReadDatasetF32(t *testing.T) { + type args struct { + file *hdf5.File + key Hdf5Key + } + type want struct { + want [][]float32 + err error + } + type test struct { + name string + args args + want want + checkFunc func(want, [][]float32, error) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, got [][]float32, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + args: args { + file:nil, + key:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + args: args { + file:nil, + key:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got, err := ReadDatasetF32(test.args.file, test.args.key) + if err := checkFunc(test.want, got, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func TestReadDatasetI32(t *testing.T) { + type args struct { + file *hdf5.File + key Hdf5Key + } + type want struct { + want [][]int32 + err error + } + type test struct { + name string + args args + want want + checkFunc func(want, [][]int32, error) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, got [][]int32, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + args: args { + file:nil, + key:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + args: args { + file:nil, + key:nil, + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got, err := ReadDatasetI32(test.args.file, test.args.key) + if err := checkFunc(test.want, got, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} diff --git a/internal/test/data/hdf5/option.go b/internal/test/data/hdf5/option.go new file mode 100644 index 0000000000..ac2f9a3a24 --- /dev/null +++ b/internal/test/data/hdf5/option.go @@ -0,0 +1,63 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 hdf5 is load hdf5 file +package hdf5 + +import ( + "github.com/vdaas/vald/internal/errors" +) + +type Option func(d *data) error + +var defaultOptions = []Option{ + WithName(FashionMNIST784Euclidean), + WithFilePath("./data"), +} + +func WithNameByString(n string) Option { + var name DatasetName + switch n { + case Original.String(): + name = Original + case FashionMNIST784Euclidean.String(): + name = FashionMNIST784Euclidean + } + return WithName(name) +} + +func WithName(dn DatasetName) Option { + return func(d *data) error { + switch dn { + case Original: + d.name = dn + case FashionMNIST784Euclidean: + d.name = dn + default: + return errors.NewErrInvalidOption("dataname", dn) + } + return nil + } +} + +func WithFilePath(f string) Option { + return func(d *data) error { + if len(f) != 0 { + d.path = f + } + return nil + } +} diff --git a/internal/test/data/hdf5/option_test.go b/internal/test/data/hdf5/option_test.go new file mode 100644 index 0000000000..5453a71665 --- /dev/null +++ b/internal/test/data/hdf5/option_test.go @@ -0,0 +1,387 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 hdf5 is load hdf5 file +package hdf5 + +import ( + "reflect" + "testing" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/test/goleak" +) + +func TestWithNameByString(t *testing.T) { + // Change interface type to the type of object you are testing + type args struct { + n string + } + type want struct { + obj data + } + type test struct { + name string + args args + want want + checkFunc func(want want, got data) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + + // Uncomment this block if the option returns an error, otherwise delete it + /* + defaultCheckFunc := func(w want, obj *T, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if !reflect.DeepEqual(obj, w.obj) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", obj, w.obj) + } + return nil + } + */ + + defaultCheckFunc := func(w want, obj data) error { + if !reflect.DeepEqual(obj, w.obj) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", obj, w.obj) + } + return nil + } + + tests := []test{ + { + name: "set original", + args: args{ + n: "original", + }, + want: want{ + obj: data{ + name: Original, + }, + }, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + { + name: "set fashion-mnist", + args: args{ + n: "fashion-mnist", + }, + want: want{ + obj: data{ + name: FashionMNIST784Euclidean, + }, + }, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got := WithNameByString(test.args.n) + obj := new(data) + _ = got(obj) + if err := checkFunc(test.want, *obj); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func TestWithName(t *testing.T) { + // Change interface type to the type of object you are testing + type T = interface{} + type args struct { + dn DatasetName + } + type want struct { + obj *T + // Uncomment this line if the option returns an error, otherwise delete it + // err error + } + type test struct { + name string + args args + want want + // Use the first line if the option returns an error. otherwise use the second line + // checkFunc func(want, *T, error) error + // checkFunc func(want, *T) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + + // Uncomment this block if the option returns an error, otherwise delete it + /* + defaultCheckFunc := func(w want, obj *T, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if !reflect.DeepEqual(obj, w.obj) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", obj, w.obj) + } + return nil + } + */ + + // Uncomment this block if the option do not returns an error, otherwise delete it + /* + defaultCheckFunc := func(w want, obj *T) error { + if !reflect.DeepEqual(obj, w.obj) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", obj, w.obj) + } + return nil + } + */ + + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + args: args { + dn:nil, + }, + want: want { + obj: new(T), + }, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + args: args { + dn:nil, + }, + want: want { + obj: new(T), + }, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + + // Uncomment this block if the option returns an error, otherwise delete it + /* + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got := WithName(test.args.dn) + obj := new(T) + if err := checkFunc(test.want, obj, got(obj)); err != nil { + tt.Errorf("error = %v", err) + } + */ + + // Uncomment this block if the option do not return an error, otherwise delete it + /* + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + got := WithName(test.args.dn) + obj := new(T) + got(obj) + if err := checkFunc(test.want, obj); err != nil { + tt.Errorf("error = %v", err) + } + */ + }) + } +} + +func TestWithFilePath(t *testing.T) { + // Change interface type to the type of object you are testing + type T = interface{} + type args struct { + f string + } + type want struct { + obj *T + // Uncomment this line if the option returns an error, otherwise delete it + // err error + } + type test struct { + name string + args args + want want + // Use the first line if the option returns an error. otherwise use the second line + // checkFunc func(want, *T, error) error + // checkFunc func(want, *T) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + + // Uncomment this block if the option returns an error, otherwise delete it + /* + defaultCheckFunc := func(w want, obj *T, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if !reflect.DeepEqual(obj, w.obj) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", obj, w.obj) + } + return nil + } + */ + + // Uncomment this block if the option do not returns an error, otherwise delete it + /* + defaultCheckFunc := func(w want, obj *T) error { + if !reflect.DeepEqual(obj, w.obj) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", obj, w.obj) + } + return nil + } + */ + + tests := []test{ + // TODO test cases + /* + { + name: "test_case_1", + args: args { + f:"", + }, + want: want { + obj: new(T), + }, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + }, + */ + + // TODO test cases + /* + func() test { + return test { + name: "test_case_2", + args: args { + f:"", + }, + want: want { + obj: new(T), + }, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + }, + } + }(), + */ + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + + // Uncomment this block if the option returns an error, otherwise delete it + /* + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + got := WithFilePath(test.args.f) + obj := new(T) + if err := checkFunc(test.want, obj, got(obj)); err != nil { + tt.Errorf("error = %v", err) + } + */ + + // Uncomment this block if the option do not return an error, otherwise delete it + /* + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + got := WithFilePath(test.args.f) + obj := new(T) + got(obj) + if err := checkFunc(test.want, obj); err != nil { + tt.Errorf("error = %v", err) + } + */ + }) + } +} diff --git a/internal/test/mock/controller_runtime.go b/internal/test/mock/controller_runtime.go new file mode 100644 index 0000000000..b8bd60fd1c --- /dev/null +++ b/internal/test/mock/controller_runtime.go @@ -0,0 +1,67 @@ +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 mock + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type MockSubResourceWriter struct { + client.SubResourceWriter +} + +func (*MockSubResourceWriter) Update(context.Context, client.Object, ...client.SubResourceUpdateOption) error { + return nil +} + +type MockClient struct { + client.Client +} + +func (*MockClient) Status() client.SubResourceWriter { + s := MockSubResourceWriter{ + SubResourceWriter: &MockSubResourceWriter{}, + } + return s.SubResourceWriter +} + +func (*MockClient) Get(context.Context, client.ObjectKey, client.Object, ...client.GetOption) error { + return nil +} + +func (*MockClient) Create(context.Context, client.Object, ...client.CreateOption) error { + return nil +} + +func (*MockClient) Delete(context.Context, client.Object, ...client.DeleteOption) error { + return nil +} + +func (*MockClient) DeleteAllOf(context.Context, client.Object, ...client.DeleteAllOfOption) error { + return nil +} + +type MockManager struct { + manager.Manager +} + +func (*MockManager) GetClient() client.Client { + c := &MockClient{ + Client: &MockClient{}, + } + return c.Client +} diff --git a/internal/timeutil/rate/rate.go b/internal/timeutil/rate/rate.go new file mode 100644 index 0000000000..263fe6a0d5 --- /dev/null +++ b/internal/timeutil/rate/rate.go @@ -0,0 +1,61 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 rate + +import ( + "context" + "runtime" + + "go.uber.org/ratelimit" + "golang.org/x/time/rate" +) + +type Limiter interface { + Wait(ctx context.Context) error +} + +type limiter struct { + isStd bool + uber ratelimit.Limiter + std *rate.Limiter +} + +func NewLimiter(cnt int) Limiter { + if runtime.GOMAXPROCS(0) >= 32 { + return &limiter{ + isStd: true, + std: rate.NewLimiter(rate.Limit(cnt), 1), + } + } + return &limiter{ + isStd: false, + uber: ratelimit.New(cnt, ratelimit.WithoutSlack), + } +} + +func (l *limiter) Wait(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + if l.isStd { + return l.std.Wait(ctx) + } + l.uber.Take() + } + return nil +} diff --git a/k8s/tools/benchmark/job/clusterrole.yaml b/k8s/tools/benchmark/job/clusterrole.yaml new file mode 100644 index 0000000000..3812aeb17d --- /dev/null +++ b/k8s/tools/benchmark/job/clusterrole.yaml @@ -0,0 +1,91 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: benchmark-job-role + namespace: default +rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkjobs/finalizers + verbs: + - update + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkjobs/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - update + - apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/k8s/tools/benchmark/job/clusterrolebinding.yaml b/k8s/tools/benchmark/job/clusterrolebinding.yaml new file mode 100644 index 0000000000..db44eb5f88 --- /dev/null +++ b/k8s/tools/benchmark/job/clusterrolebinding.yaml @@ -0,0 +1,27 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: benchmark-job-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: benchmark-job-role +subjects: + - kind: ServiceAccount + name: benchmark-job-service + namespace: default diff --git a/k8s/tools/benchmark/job/serviceaccount.yaml b/k8s/tools/benchmark/job/serviceaccount.yaml new file mode 100644 index 0000000000..6324345bf2 --- /dev/null +++ b/k8s/tools/benchmark/job/serviceaccount.yaml @@ -0,0 +1,20 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersoin: v1 +kind: ServiceAccount +metadata: + name: benchmark-job-service + namespace: default diff --git a/k8s/tools/benchmark/job/svc.yaml b/k8s/tools/benchmark/job/svc.yaml new file mode 100644 index 0000000000..61507e665a --- /dev/null +++ b/k8s/tools/benchmark/job/svc.yaml @@ -0,0 +1,31 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: v1 +kind: Service +metadata: + name: benchmark-job-svc +spec: + ports: + - name: prometheus + port: 4000 # FIX ME + targetPort: 4000 # FIX ME + protocol: TCP + selector: + app.kubernetes.io/name: benchmark-job-svc + app.kubernetes.io/component: benchmark-job + clusterIP: None + type: ClusterIP + externalTrafficPolicy: "" diff --git a/k8s/tools/benchmark/operator/clusterrole.yaml b/k8s/tools/benchmark/operator/clusterrole.yaml new file mode 100644 index 0000000000..9a25bc3d89 --- /dev/null +++ b/k8s/tools/benchmark/operator/clusterrole.yaml @@ -0,0 +1,115 @@ +--- +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: vald-benchmark-operator + namespace: default +rules: + - apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkscenarios + - valdbenchmarkscenario + - valdbenchmarkjob + - valdbenchmarkjobs + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkscenarios/finalizers + - valdbenchmarkscenario/finalizers + - valdbenchmarkjob/finalizers + - valdbenchmarkjobs/finalizers + verbs: + - update + - apiGroups: + - vald.vdaas.org + resources: + - valdbenchmarkscenario/status + - valdbenchmarkscenarios/status + - valdbenchmarkjob/status + - valdbenchmarkjobs/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - update + - apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/k8s/tools/benchmark/operator/clusterrolebinding.yaml b/k8s/tools/benchmark/operator/clusterrolebinding.yaml new file mode 100644 index 0000000000..4cb1cbc833 --- /dev/null +++ b/k8s/tools/benchmark/operator/clusterrolebinding.yaml @@ -0,0 +1,29 @@ +--- +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: vald-benchmark-operator + namespace: default +subjects: + - kind: ServiceAccount + name: vald-benchmark-operator + namespace: default +roleRef: + kind: ClusterRole + name: vald-benchmark-operator + apiGroup: rbac.authorization.k8s.io diff --git a/k8s/tools/benchmark/operator/configmap.yaml b/k8s/tools/benchmark/operator/configmap.yaml new file mode 100644 index 0000000000..7aa2218a81 --- /dev/null +++ b/k8s/tools/benchmark/operator/configmap.yaml @@ -0,0 +1,143 @@ +--- +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: vald-benchmark-operator-config + labels: + app.kubernetes.io/name: vald-benchmark-operator + helm.sh/chart: vald-benchmark-operator-v1.7.5 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: v1.7.5 + app.kubernetes.io/component: benchmark-operator +data: + config.yaml: | + --- + version: v0.0.0 + time_zone: "" + logging: + logger: "glg" + level: "debug" + format: "raw" + server_config: + servers: + - name: grpc + host: 0.0.0.0 + port: 8081 + mode: GRPC + probe_wait_time: "3s" + network: "tcp" + socket_path: "" + grpc: + bidirectional_stream_concurrency: 20 + connection_timeout: "" + max_receive_message_size: 0 + max_send_message_size: 0 + initial_window_size: 0 + initial_conn_window_size: 0 + keepalive: + max_conn_idle: "" + max_conn_age: "" + max_conn_age_grace: "" + time: "120s" + timeout: "30s" + min_time: "60s" + permit_without_stream: true + write_buffer_size: 0 + read_buffer_size: 0 + max_header_list_size: 0 + header_table_size: 0 + interceptors: [] + enable_reflection: true + restart: true + health_check_servers: + - name: liveness + host: 0.0.0.0 + port: 3000 + mode: "" + probe_wait_time: "3s" + network: "tcp" + socket_path: "" + http: + shutdown_duration: "5s" + handler_timeout: "" + idle_timeout: "" + read_header_timeout: "" + read_timeout: "" + write_timeout: "" + - name: readiness + host: 0.0.0.0 + port: 3001 + mode: "" + probe_wait_time: "3s" + network: "tcp" + socket_path: "" + http: + shutdown_duration: "0s" + handler_timeout: "" + idle_timeout: "" + read_header_timeout: "" + read_timeout: "" + write_timeout: "" + metrics_servers: + startup_strategy: + - liveness + - grpc + - readiness + full_shutdown_duration: 600s + tls: + enabled: false + cert: "/path/to/cert" + key: "/path/to/key" + ca: "/path/to/ca" + insecure_skip_verify: false + observability: + enabled: false + otlp: + collector_endpoint: "" + trace_batch_timeout: "1s" + trace_export_timeout: "1m" + trace_max_export_batch_size: 1024 + trace_max_queue_size: 256 + metrics_export_interval: "1s" + metrics_export_timeout: "1m" + attribute: + namespace: "_MY_POD_NAMESPACE_" + pod_name: "_MY_POD_NAME_" + node_name: "_MY_NODE_NAME_" + service_name: "vald-benchmark-operator" + metrics: + enable_version_info: true + version_info_labels: + - vald_version + - server_name + - git_commit + - build_time + - go_version + - go_os + - go_arch + - ngt_version + enable_memory: true + enable_goroutine: true + enable_cgo: true + trace: + enabled: false + sampling_rate: 1 + job_image: + image: "vdaas/vald-benchmark-job:v1.7.5" + pullPolicy: Always diff --git a/k8s/tools/benchmark/operator/crds/valdbenchmarkjob.yaml b/k8s/tools/benchmark/operator/crds/valdbenchmarkjob.yaml new file mode 100644 index 0000000000..53367d67c8 --- /dev/null +++ b/k8s/tools/benchmark/operator/crds/valdbenchmarkjob.yaml @@ -0,0 +1,824 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: valdbenchmarkjobs.vald.vdaas.org +spec: + group: vald.vdaas.org + names: + kind: ValdBenchmarkJob + listKind: ValdBenchmarkJobList + plural: valdbenchmarkjobs + singular: valdbenchmarkjob + shortNames: + - vbj + - vbjs + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .spec.replica + name: REPLICAS + type: integer + - jsonPath: .status + name: STATUS + type: string + schema: + openAPIV3Schema: + description: ValdBenchmarkJob is the Schema for the valdbenchmarkjobs API + type: object + 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 + status: + description: ValdBenchmarkJobStatus defines the observed state of ValdBenchmarkJob + enum: + - NotReady + - Completed + - Available + - Healthy + type: string + spec: + type: object + properties: + client_config: + type: object + properties: + addrs: + type: array + items: + type: string + backoff: + type: object + properties: + backoff_factor: + type: number + backoff_time_limit: + type: string + enable_error_log: + type: boolean + initial_duration: + type: string + jitter_limit: + type: string + maximum_duration: + type: string + retry_count: + type: integer + call_option: + type: object + x-kubernetes-preserve-unknown-fields: true + circuit_breaker: + type: object + properties: + closed_error_rate: + type: number + closed_refresh_timeout: + type: string + half_open_error_rate: + type: number + min_samples: + type: integer + open_timeout: + type: string + connection_pool: + type: object + properties: + enable_dns_resolver: + type: boolean + enable_rebalance: + type: boolean + old_conn_close_duration: + type: string + rebalance_duration: + type: string + size: + type: integer + dial_option: + type: object + properties: + backoff_base_delay: + type: string + backoff_jitter: + type: number + backoff_max_delay: + type: string + backoff_multiplier: + type: number + enable_backoff: + type: boolean + initial_connection_window_size: + type: integer + initial_window_size: + type: integer + insecure: + type: boolean + interceptors: + type: array + items: + type: string + enum: + - TraceInterceptor + keepalive: + type: object + properties: + permit_without_stream: + type: boolean + time: + type: string + timeout: + type: string + max_msg_size: + type: integer + min_connection_timeout: + type: string + net: + type: object + properties: + dialer: + type: object + properties: + dual_stack_enabled: + type: boolean + keepalive: + type: string + timeout: + type: string + dns: + type: object + properties: + cache_enabled: + type: boolean + cache_expiration: + type: string + refresh_duration: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + tls: + type: object + properties: + ca: + type: string + cert: + type: string + enabled: + type: boolean + insecure_skip_verify: + type: boolean + key: + type: string + read_buffer_size: + type: integer + timeout: + type: string + write_buffer_size: + type: integer + health_check_duration: + type: string + max_recv_msg_size: + type: integer + max_retry_rpc_buffer_size: + type: integer + max_send_msg_size: + type: integer + tls: + type: object + properties: + ca: + type: string + cert: + type: string + enabled: + type: boolean + insecure_skip_verify: + type: boolean + key: + type: string + wait_for_ready: + type: boolean + concurrency_limit: + type: integer + maximum: 65535 + minimum: 0 + dataset: + type: object + properties: + group: + type: string + minLength: 1 + indexes: + type: integer + minimum: 0 + name: + type: string + enum: + - original + - fashion-mnist + range: + type: object + properties: + end: + type: integer + minimum: 1 + start: + type: integer + minimum: 1 + required: + - start + - end + url: + type: string + required: + - name + - indexes + - group + - range + dimension: + type: integer + minimum: 1 + global_config: + type: object + properties: + logging: + type: object + properties: + format: + type: string + enum: + - raw + - json + level: + type: string + enum: + - debug + - info + - warn + - error + - fatal + logger: + type: string + enum: + - glg + - zap + time_zone: + type: string + version: + type: string + insert_config: + type: object + properties: + skip_strict_exist_check: + type: boolean + timestamp: + type: string + job_type: + type: string + enum: + - insert + - update + - upsert + - search + - remove + - getobject + - exists + object_config: + type: object + properties: + filter_config: + type: object + properties: + host: + type: string + remove_config: + type: object + properties: + skip_strict_exist_check: + type: boolean + timestamp: + type: string + repetition: + type: integer + minimum: 1 + replica: + type: integer + minimum: 1 + rps: + type: integer + maximum: 65535 + minimum: 0 + rules: + type: array + items: + type: string + search_config: + type: object + properties: + aggregation_algorithm: + type: string + enum: + - Unknown + - ConcurrentQueue + - SortSlice + - SortPoolSlice + - PairingHeap + enable_linear_search: + type: boolean + epsilon: + type: number + min_num: + type: integer + num: + type: integer + radius: + type: number + timeout: + type: string + server_config: + type: object + properties: + healths: + type: object + properties: + liveness: + type: object + properties: + enabled: + type: boolean + host: + type: string + livenessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + port: + type: integer + maximum: 65535 + minimum: 0 + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + readiness: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + maximum: 65535 + minimum: 0 + readinessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + startup: + type: object + properties: + enabled: + type: boolean + port: + type: integer + maximum: 65535 + minimum: 0 + startupProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + servers: + type: object + properties: + grpc: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + maximum: 65535 + minimum: 0 + server: + type: object + properties: + grpc: + type: object + properties: + bidirectional_stream_concurrency: + type: integer + connection_timeout: + type: string + enable_reflection: + type: boolean + header_table_size: + type: integer + initial_conn_window_size: + type: integer + initial_window_size: + type: integer + interceptors: + type: array + items: + type: string + enum: + - RecoverInterceptor + - AccessLogInterceptor + - TraceInterceptor + - MetricInterceptor + keepalive: + type: object + properties: + max_conn_age: + type: string + max_conn_age_grace: + type: string + max_conn_idle: + type: string + min_time: + type: string + permit_without_stream: + type: boolean + time: + type: string + timeout: + type: string + max_header_list_size: + type: integer + max_receive_message_size: + type: integer + max_send_message_size: + type: integer + read_buffer_size: + type: integer + write_buffer_size: + type: integer + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + restart: + type: boolean + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + rest: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + maximum: 65535 + minimum: 0 + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + enum: + - tcp + - tcp4 + - tcp6 + - udp + - udp4 + - udp6 + - unix + - unixgram + - unixpacket + probe_wait_time: + type: string + socket_option: + type: object + properties: + ip_recover_destination_addr: + type: boolean + ip_transparent: + type: boolean + reuse_addr: + type: boolean + reuse_port: + type: boolean + tcp_cork: + type: boolean + tcp_defer_accept: + type: boolean + tcp_fast_open: + type: boolean + tcp_no_delay: + type: boolean + tcp_quick_ack: + type: boolean + socket_path: + type: string + servicePort: + type: integer + maximum: 65535 + minimum: 0 + target: + type: object + properties: + host: + type: string + minLength: 1 + port: + type: integer + maximum: 65535 + minimum: 0 + required: + - host + - port + ttl_seconds_after_finished: + type: integer + maximum: 65535 + minimum: 0 + update_config: + type: object + properties: + disable_balance_update: + type: boolean + skip_strict_exist_check: + type: boolean + timestamp: + type: string + upsert_config: + type: object + properties: + disable_balance_update: + type: boolean + skip_strict_exist_check: + type: boolean + timestamp: + type: string diff --git a/k8s/tools/benchmark/operator/crds/valdbenchmarkoperatorrelease.yaml b/k8s/tools/benchmark/operator/crds/valdbenchmarkoperatorrelease.yaml new file mode 100644 index 0000000000..8886ca72ea --- /dev/null +++ b/k8s/tools/benchmark/operator/crds/valdbenchmarkoperatorrelease.yaml @@ -0,0 +1,523 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: valdbenchmarkoperatorreleases.vald.vdaas.org +spec: + group: vald.vdaas.org + names: + kind: ValdBenchmarkOperatorRelease + listKind: ValdBenchmarkOperatorReleaseList + plural: valdbenchmarkoperatorreleases + singular: valdbenchmarkoperatorrelease + shortNames: + - vbor + - vbors + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .status + name: STATUS + type: string + schema: + openAPIV3Schema: + description: ValdBenchmarkScenario is the Schema for the valdbenchmarkscenarios API + type: object + 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 + status: + description: ValdBenchmarkScenarioStatus defines the observed state of ValdBenchmarkScenario + enum: + - NotReady + - Completed + - Available + - Healthy + type: string + spec: + type: object + properties: + affinity: + type: object + x-kubernetes-preserve-unknown-fields: true + annotations: + type: object + x-kubernetes-preserve-unknown-fields: true + image: + type: object + properties: + pullPolicy: + type: string + enum: + - Always + - Never + - IfNotPresent + repository: + type: string + tag: + type: string + job_image: + type: object + properties: + pullPolicy: + type: string + enum: + - Always + - Never + - IfNotPresent + repository: + type: string + tag: + type: string + logging: + type: object + properties: + format: + type: string + enum: + - raw + - json + level: + type: string + enum: + - debug + - info + - warn + - error + - fatal + logger: + type: string + enum: + - glg + - zap + name: + type: string + nodeSelector: + type: object + x-kubernetes-preserve-unknown-fields: true + observability: + type: object + properties: + enabled: + type: boolean + otlp: + type: object + properties: + attribute: + type: object + properties: + metrics: + type: object + properties: + enable_cgo: + type: boolean + enable_goroutine: + type: boolean + enable_memory: + type: boolean + enable_version_info: + type: boolean + version_info_labels: + type: array + items: + type: string + namespace: + type: string + node_name: + type: string + pod_name: + type: string + service_name: + type: string + collector_endpoint: + type: string + metrics_export_interval: + type: string + metrics_export_timeout: + type: string + trace_batch_timeout: + type: string + trace_export_timeout: + type: string + trace_max_export_batch_size: + type: integer + trace_max_queue_size: + type: integer + trace: + type: object + properties: + enabled: + type: boolean + sampling_rate: + type: integer + podAnnotations: + type: object + x-kubernetes-preserve-unknown-fields: true + podSecurityContext: + type: object + x-kubernetes-preserve-unknown-fields: true + rbac: + type: object + properties: + create: + type: boolean + name: + type: string + replicas: + type: integer + resources: + type: object + properties: + limits: + type: object + x-kubernetes-preserve-unknown-fields: true + requests: + type: object + x-kubernetes-preserve-unknown-fields: true + securityContext: + type: object + x-kubernetes-preserve-unknown-fields: true + server_config: + type: object + properties: + full_shutdown_duration: + type: string + healths: + type: object + properties: + liveness: + type: object + properties: + enabled: + type: boolean + host: + type: string + livenessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + port: + type: integer + server: + type: object + properties: + http: + type: object + properties: + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + timeout: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + probe_wait_time: + type: string + socket_path: + type: string + servicePort: + type: integer + readiness: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + readinessProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + probe_wait_time: + type: string + socket_path: + type: string + servicePort: + type: integer + startup: + type: object + properties: + enabled: + type: boolean + startupProbe: + type: object + properties: + failureThreshold: + type: integer + httpGet: + type: object + properties: + path: + type: string + port: + type: string + scheme: + type: string + initialDelaySeconds: + type: integer + periodSeconds: + type: integer + successThreshold: + type: integer + timeoutSeconds: + type: integer + metrics: + type: object + properties: + pprof: + type: object + properties: + enabled: + type: boolean + host: + type: string + port: + type: integer + server: + type: object + properties: + http: + type: object + properties: + handler_timeout: + type: string + idle_timeout: + type: string + read_header_timeout: + type: string + read_timeout: + type: string + shutdown_duration: + type: string + write_timeout: + type: string + mode: + type: string + network: + type: string + probe_wait_time: + type: string + socket_path: + type: string + servers: + type: object + properties: + grpc: + type: object + properties: + enabled: + type: boolean + host: + type: string + name: + type: string + port: + type: integer + server: + type: object + properties: + grpc: + type: object + properties: + bidirectional_stream_concurrency: + type: integer + connection_timeout: + type: string + enable_reflection: + type: boolean + header_table_size: + type: integer + initial_conn_window_size: + type: integer + initial_window_size: + type: integer + interceptors: + type: array + items: + type: string + keepalive: + type: object + properties: + max_conn_age: + type: string + max_conn_age_grace: + type: string + max_conn_idle: + type: string + min_time: + type: string + permit_without_stream: + type: boolean + time: + type: string + timeout: + type: string + max_header_list_size: + type: integer + max_receive_message_size: + type: integer + max_send_msg_size: + type: integer + read_buffer_size: + type: integer + write_buffer_size: + type: integer + mode: + type: string + network: + type: string + probe_wait_time: + type: string + restart: + type: boolean + socket_path: + type: string + servicePort: + type: integer + rest: + type: object + properties: + enabled: + type: boolean + tls: + type: object + properties: + ca: + type: string + cert: + type: string + enabled: + type: boolean + insecure_skip_verify: + type: boolean + key: + type: string + service: + type: object + properties: + annotations: + type: object + x-kubernetes-preserve-unknown-fields: true + enabled: + type: boolean + externalTrafficPolicy: + type: string + labels: + type: object + x-kubernetes-preserve-unknown-fields: true + type: + type: string + enum: + - ClusterIP + - LoadBalancer + - NodePort + serviceAccount: + type: object + properties: + create: + type: boolean + name: + type: string + time_zone: + type: string + tolerations: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + version: + type: string diff --git a/k8s/tools/benchmark/operator/crds/valdbenchmarkscenario.yaml b/k8s/tools/benchmark/operator/crds/valdbenchmarkscenario.yaml new file mode 100644 index 0000000000..3f7492d8b3 --- /dev/null +++ b/k8s/tools/benchmark/operator/crds/valdbenchmarkscenario.yaml @@ -0,0 +1,115 @@ +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: valdbenchmarkscenarios.vald.vdaas.org +spec: + group: vald.vdaas.org + names: + kind: ValdBenchmarkScenario + listKind: ValdBenchmarkScenarioList + plural: valdbenchmarkscenarios + singular: valdbenchmarkscenario + shortNames: + - vbs + - vbss + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - jsonPath: .status + name: STATUS + type: string + schema: + openAPIV3Schema: + description: ValdBenchmarkScenario is the Schema for the valdbenchmarkscenarios API + type: object + 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 + status: + description: ValdBenchmarkScenarioStatus defines the observed state of ValdBenchmarkScenario + enum: + - NotReady + - Completed + - Available + - Healthy + type: string + spec: + type: object + properties: + dataset: + type: object + properties: + group: + type: string + minLength: 1 + indexes: + type: integer + minimum: 0 + name: + type: string + enum: + - original + - fashion-mnist + range: + type: object + properties: + end: + type: integer + minimum: 1 + start: + type: integer + minimum: 1 + required: + - start + - end + url: + type: string + required: + - name + - indexes + - group + - range + jobs: + type: array + items: + type: object + x-kubernetes-preserve-unknown-fields: true + target: + type: object + properties: + host: + type: string + minLength: 1 + port: + type: integer + maximum: 65535 + minimum: 0 + required: + - host + - port diff --git a/k8s/tools/benchmark/operator/deployment.yaml b/k8s/tools/benchmark/operator/deployment.yaml new file mode 100644 index 0000000000..e2740649c7 --- /dev/null +++ b/k8s/tools/benchmark/operator/deployment.yaml @@ -0,0 +1,113 @@ +--- +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vald-benchmark-operator + namespace: default + labels: + app: vald-benchmark-operator + app.kubernetes.io/name: vald-benchmark-operator + helm.sh/chart: vald-benchmark-operator-v1.7.5 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: v1.7.5 + app.kubernetes.io/component: benchmark-operator +spec: + replicas: 1 + selector: + matchLabels: + name: vald-benchmark-operator + template: + metadata: + labels: + name: vald-benchmark-operator + app.kubernetes.io/name: vald-benchmark-operator + app.kubernetes.io/instance: release-name + app.kubernetes.io/component: benchmark-operator + spec: + serviceAccountName: vald-benchmark-operator + containers: + - name: vald-benchmark-operator + image: "vdaas/vald-benchmark-operator:v1.7.5" + imagePullPolicy: Always + livenessProbe: + failureThreshold: 2 + httpGet: + path: /liveness + port: liveness + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 20 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + failureThreshold: 2 + httpGet: + path: /readiness + port: readiness + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 3 + successThreshold: 1 + timeoutSeconds: 2 + startupProbe: + failureThreshold: 30 + httpGet: + path: /liveness + port: liveness + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 2 + ports: + - name: liveness + protocol: TCP + containerPort: 3000 + - name: readiness + protocol: TCP + containerPort: 3001 + - name: grpc + protocol: TCP + containerPort: 8081 + - name: pprof + protocol: TCP + containerPort: 6060 + resources: + limits: + cpu: 300m + memory: 300Mi + requests: + cpu: 200m + memory: 200Mi + volumeMounts: + - name: vald-benchmark-operator-config + mountPath: /etc/server + env: + - name: JOB_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + restartPolicy: Always + volumes: + - name: vald-benchmark-operator-config + configMap: + defaultMode: 420 + name: vald-benchmark-operator-config diff --git a/k8s/tools/benchmark/operator/service.yaml b/k8s/tools/benchmark/operator/service.yaml new file mode 100644 index 0000000000..aebdd7d328 --- /dev/null +++ b/k8s/tools/benchmark/operator/service.yaml @@ -0,0 +1,38 @@ +--- +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: v1 +kind: Service +metadata: + name: vald-benchmark-operator + labels: + app.kubernetes.io/name: vald-benchmark-operator + helm.sh/chart: vald-benchmark-operator-v1.7.5 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: v1.7.5 + app.kubernetes.io/component: helm-operator +spec: + ports: + - name: prometheus + port: 6060 + targetPort: 6060 + protocol: TCP + selector: + app.kubernetes.io/name: vald-benchmark-operator + app.kubernetes.io/component: helm-operator + clusterIP: None + type: ClusterIP diff --git a/k8s/tools/benchmark/operator/serviceaccount.yaml b/k8s/tools/benchmark/operator/serviceaccount.yaml new file mode 100644 index 0000000000..e9be258daa --- /dev/null +++ b/k8s/tools/benchmark/operator/serviceaccount.yaml @@ -0,0 +1,21 @@ +--- +# +# Copyright (C) 2019-2024 vdaas.org vald team +# +# 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 +# +# https://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. +# +apiVersion: v1 +kind: ServiceAccount +metadata: + name: vald-benchmark-operator + namespace: default diff --git a/pkg/tools/benchmark/job/README.md b/pkg/tools/benchmark/job/README.md new file mode 100644 index 0000000000..75caad4fed --- /dev/null +++ b/pkg/tools/benchmark/job/README.md @@ -0,0 +1 @@ +# vald benchmark job diff --git a/pkg/tools/benchmark/job/config/config.go b/pkg/tools/benchmark/job/config/config.go new file mode 100644 index 0000000000..8b2b5d594a --- /dev/null +++ b/pkg/tools/benchmark/job/config/config.go @@ -0,0 +1,453 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 config stores all server application settings +package config + +import ( + "context" + "encoding/json" + "os" + + "github.com/vdaas/vald/internal/config" + "github.com/vdaas/vald/internal/k8s/client" + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + "github.com/vdaas/vald/internal/log" +) + +// GlobalConfig is type alias for config.GlobalConfig +type GlobalConfig = config.GlobalConfig + +// Config represent a application setting data content (config.yaml). +// In K8s environment, this configuration is stored in K8s ConfigMap. +type Config struct { + config.GlobalConfig `json:",inline" yaml:",inline"` + + // Server represent all server configuration + Server *config.Servers `json:"server_config" yaml:"server_config"` + + // Observability represent observability configurations + Observability *config.Observability `json:"observability" yaml:"observability"` + + // Job represents benchmark job configurations + Job *config.BenchmarkJob `json:"job" yaml:"job"` + + // K8sClient represents kubernetes clients + K8sClient client.Client `json:"k8s_client" yaml:"k8s_client"` +} + +var ( + NAMESPACE = os.Getenv("CRD_NAMESPACE") + NAME = os.Getenv("CRD_NAME") + JOBNAME_ANNOTATION = "before-job-name" + JOBNAMESPACE_ANNOTATION = "before-job-namespace" +) + +// NewConfig represents the set config from the given setting file path. +func NewConfig(ctx context.Context, path string) (cfg *Config, err error) { + err = config.Read(path, &cfg) + if err != nil { + return nil, err + } + if cfg != nil { + cfg.Bind() + } + + if cfg.Server != nil { + cfg.Server = cfg.Server.Bind() + } else { + cfg.Server = new(config.Servers) + } + + if cfg.Observability != nil { + cfg.Observability = cfg.Observability.Bind() + } else { + cfg.Observability = new(config.Observability) + } + + if cfg.Job != nil { + cfg.Job = cfg.Job.Bind() + } else { + cfg.Job = new(config.BenchmarkJob) + } + + if cfg.Job.ClientConfig == nil { + cfg.Job.ClientConfig = new(config.GRPCClient) + } + + // Get config from applied ValdBenchmarkJob custom resource + var jobResource v1.ValdBenchmarkJob + if cfg.K8sClient == nil { + c, err := client.New(client.WithSchemeBuilder(*v1.SchemeBuilder)) + if err != nil { + log.Error(err.Error()) + return nil, err + } + cfg.K8sClient = c + } + err = cfg.K8sClient.Get(ctx, NAME, NAMESPACE, &jobResource) + if err != nil { + log.Warn(err.Error()) + } else { + // create override Config + overrideCfg := new(Config) + if jobResource.Spec.GlobalConfig != nil { + overrideCfg.GlobalConfig = *jobResource.Spec.Bind() + } + if jobResource.Spec.ServerConfig != nil { + overrideCfg.Server = (*jobResource.Spec.ServerConfig).Bind() + } + // jobResource.Spec has another field comparering Config.Job, so json.Marshal and Unmarshal are used for embedding field value of Config.Job from jobResource.Spec + var overrideJobCfg config.BenchmarkJob + b, err := json.Marshal(*jobResource.Spec.DeepCopy()) + if err == nil { + err = json.Unmarshal([]byte(b), &overrideJobCfg) + if err != nil { + log.Warn(err.Error()) + } + overrideCfg.Job = overrideJobCfg.Bind() + } + if annotations := jobResource.GetAnnotations(); annotations != nil { + overrideCfg.Job.BeforeJobName = annotations[JOBNAME_ANNOTATION] + overrideCfg.Job.BeforeJobNamespace = annotations[JOBNAMESPACE_ANNOTATION] + } + return config.Merge(cfg, overrideCfg) + } + return cfg, nil +} + +// func FakeData() { +// d := Config{ +// GlobalConfig: config.GlobalConfig{ +// Version: "v0.0.1", +// TZ: "JST", +// Logging: &config.Logging{ +// Format: "raw", +// Level: "debug", +// Logger: "glg", +// }, +// }, +// Server: &config.Servers{ +// Servers: []*config.Server{ +// { +// Name: "grpc", +// Host: "0.0.0.0", +// Port: 8081, +// Mode: "GRPC", +// ProbeWaitTime: "3s", +// SocketPath: "", +// GRPC: &config.GRPC{ +// BidirectionalStreamConcurrency: 20, +// MaxReceiveMessageSize: 0, +// MaxSendMessageSize: 0, +// InitialWindowSize: 1048576, +// InitialConnWindowSize: 2097152, +// Keepalive: &config.GRPCKeepalive{ +// MaxConnIdle: "", +// MaxConnAge: "", +// MaxConnAgeGrace: "", +// Time: "3h", +// Timeout: "60s", +// MinTime: "10m", +// PermitWithoutStream: true, +// }, +// WriteBufferSize: 0, +// ReadBufferSize: 0, +// ConnectionTimeout: "", +// MaxHeaderListSize: 0, +// HeaderTableSize: 0, +// Interceptors: []string{ +// "RecoverInterceptor", +// }, +// EnableReflection: true, +// }, +// SocketOption: &config.SocketOption{ +// ReusePort: true, +// ReuseAddr: true, +// TCPFastOpen: false, +// TCPNoDelay: false, +// TCPCork: false, +// TCPQuickAck: false, +// TCPDeferAccept: false, +// IPTransparent: false, +// IPRecoverDestinationAddr: false, +// }, +// Restart: true, +// }, +// }, +// HealthCheckServers: []*config.Server{ +// { +// Name: "livenesss", +// Host: "0.0.0.0", +// Port: 3000, +// Mode: "", +// Network: "tcp", +// ProbeWaitTime: "3s", +// SocketPath: "", +// HTTP: &config.HTTP{ +// HandlerTimeout: "", +// IdleTimeout: "", +// ReadHeaderTimeout: "", +// ReadTimeout: "", +// ShutdownDuration: "5s", +// WriteTimeout: "", +// }, +// SocketOption: &config.SocketOption{ +// ReusePort: true, +// ReuseAddr: true, +// TCPFastOpen: true, +// TCPNoDelay: true, +// TCPCork: false, +// TCPQuickAck: true, +// TCPDeferAccept: false, +// IPTransparent: false, +// IPRecoverDestinationAddr: false, +// }, +// }, +// { +// Name: "readiness", +// Host: "0.0.0.0", +// Port: 3001, +// Mode: "", +// Network: "tcp", +// ProbeWaitTime: "3s", +// SocketPath: "", +// HTTP: &config.HTTP{ +// HandlerTimeout: "", +// IdleTimeout: "", +// ReadHeaderTimeout: "", +// ReadTimeout: "", +// ShutdownDuration: "0s", +// WriteTimeout: "", +// }, +// SocketOption: &config.SocketOption{ +// ReusePort: true, +// ReuseAddr: true, +// TCPFastOpen: true, +// TCPNoDelay: true, +// TCPCork: false, +// TCPQuickAck: true, +// TCPDeferAccept: false, +// IPTransparent: false, +// IPRecoverDestinationAddr: false, +// }, +// }, +// }, +// MetricsServers: []*config.Server{ +// { +// Name: "pprof", +// Host: "0.0.0.0", +// Port: 6060, +// Mode: "REST", +// Network: "tcp", +// ProbeWaitTime: "3s", +// SocketPath: "", +// HTTP: &config.HTTP{ +// HandlerTimeout: "5s", +// IdleTimeout: "2s", +// ReadHeaderTimeout: "1s", +// ReadTimeout: "1s", +// ShutdownDuration: "5s", +// WriteTimeout: "1m", +// }, +// SocketOption: &config.SocketOption{ +// ReusePort: true, +// ReuseAddr: true, +// TCPFastOpen: false, +// TCPNoDelay: false, +// TCPCork: false, +// TCPQuickAck: false, +// TCPDeferAccept: false, +// IPTransparent: false, +// IPRecoverDestinationAddr: false, +// }, +// }, +// }, +// StartUpStrategy: []string{ +// "livenesss", +// "readiness", +// "pprof", +// }, +// ShutdownStrategy: []string{ +// "readiness", +// "agent-rest", +// "agent-grpc", +// "pprof", +// "livenesss", +// }, +// FullShutdownDuration: "30s", +// TLS: &config.TLS{ +// Enabled: false, +// Cert: "/path/to/cert", +// Key: "/path/to/key", +// CA: "/path/to/ca", +// }, +// }, +// Observability: &config.Observability{ +// Enabled: true, +// OTLP: &config.OTLP{ +// CollectorEndpoint: "", +// Attribute: &config.OTLPAttribute{ +// Namespace: NAMESPACE, +// PodName: NAME, +// NodeName: "", +// ServiceName: "vald", +// }, +// TraceBatchTimeout: "1s", +// TraceExportTimeout: "1m", +// TraceMaxExportBatchSize: 1024, +// TraceMaxQueueSize: 256, +// MetricsExportInterval: "1s", +// MetricsExportTimeout: "1m", +// }, +// Metrics: &config.Metrics{ +// EnableVersionInfo: true, +// EnableMemory: true, +// EnableGoroutine: true, +// EnableCGO: true, +// VersionInfoLabels: []string{ +// "vald_version", +// "server_name", +// "git_commit", +// "build_time", +// "go_version", +// "go_arch", +// "ngt_version", +// }, +// }, +// Trace: &config.Trace{ +// Enabled: true, +// }, +// }, +// Job: &config.BenchmarkJob{ +// Target: &config.BenchmarkTarget{ +// Host: "vald-lb-gateway.svc.local", +// Port: 8081, +// }, +// Dataset: &config.BenchmarkDataset{ +// Name: "fashion-mnist", +// Group: "train", +// Indexes: 10000, +// Range: &config.BenchmarkDatasetRange{ +// Start: 0, +// End: 10000, +// }, +// }, +// Replica: 1, +// Repetition: 1, +// JobType: "search", +// InsertConfig: &config.InsertConfig{}, +// UpdateConfig: &config.UpdateConfig{}, +// UpsertConfig: &config.UpsertConfig{}, +// SearchConfig: &config.SearchConfig{}, +// RemoveConfig: &config.RemoveConfig{}, +// ClientConfig: &config.GRPCClient{ +// Addrs: []string{}, +// HealthCheckDuration: "1s", +// ConnectionPool: &config.ConnectionPool{ +// ResolveDNS: true, +// EnableRebalance: true, +// RebalanceDuration: "30m", +// Size: 3, +// OldConnCloseDuration: "2m", +// }, +// Backoff: &config.Backoff{ +// InitialDuration: "5ms", +// BackoffTimeLimit: "5s", +// MaximumDuration: "5s", +// JitterLimit: "100ms", +// BackoffFactor: 1.1, +// RetryCount: 100, +// EnableErrorLog: true, +// }, +// CircuitBreaker: &config.CircuitBreaker{ +// ClosedErrorRate: 0.7, +// HalfOpenErrorRate: 0.5, +// MinSamples: 1000, +// OpenTimeout: "1s", +// ClosedRefreshTimeout: "10s", +// }, +// CallOption: &config.CallOption{ +// WaitForReady: true, +// MaxRetryRPCBufferSize: 0, +// MaxRecvMsgSize: 0, +// MaxSendMsgSize: 0, +// }, +// DialOption: &config.DialOption{ +// WriteBufferSize: 0, +// ReadBufferSize: 0, +// InitialWindowSize: 1048576, +// InitialConnectionWindowSize: 2097152, +// MaxMsgSize: 0, +// BackoffMaxDelay: "120s", +// BackoffBaseDelay: "1s", +// BackoffJitter: 0.2, +// BackoffMultiplier: 1.6, +// MinimumConnectionTimeout: "20s", +// EnableBackoff: false, +// Insecure: true, +// Timeout: "", +// Interceptors: []string{}, +// Net: &config.Net{ +// DNS: &config.DNS{ +// CacheEnabled: true, +// RefreshDuration: "30m", +// CacheExpiration: "1h", +// }, +// Dialer: &config.Dialer{ +// Timeout: "", +// Keepalive: "", +// FallbackDelay: "", +// DualStackEnabled: true, +// }, +// TLS: &config.TLS{ +// Enabled: false, +// Cert: "path/to/cert", +// Key: "path/to/key", +// CA: "path/to/ca", +// InsecureSkipVerify: false, +// }, +// SocketOption: &config.SocketOption{ +// ReusePort: true, +// ReuseAddr: true, +// TCPFastOpen: false, +// TCPNoDelay: false, +// TCPQuickAck: false, +// TCPCork: false, +// TCPDeferAccept: false, +// IPTransparent: false, +// IPRecoverDestinationAddr: false, +// }, +// }, +// Keepalive: &config.GRPCClientKeepalive{ +// Time: "120s", +// Timeout: "30s", +// PermitWithoutStream: true, +// }, +// }, +// TLS: &config.TLS{ +// Enabled: false, +// Cert: "path/to/cert", +// Key: "path/to/key", +// CA: "path/to/ca", +// InsecureSkipVerify: false, +// }, +// }, +// Rules: []*config.BenchmarkJobRule{}, +// }, +// } +// fmt.Println(config.ToRawYaml(d)) +// } diff --git a/pkg/tools/benchmark/job/config/config_test.go b/pkg/tools/benchmark/job/config/config_test.go new file mode 100644 index 0000000000..facad92568 --- /dev/null +++ b/pkg/tools/benchmark/job/config/config_test.go @@ -0,0 +1,108 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 setting stores all server application settings +package config + +import ( + "context" + "io/fs" + "testing" + + "github.com/vdaas/vald/internal/config" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/test/comparator" + "github.com/vdaas/vald/internal/test/goleak" +) + +func TestNewConfig(t *testing.T) { + t.Parallel() + type args struct { + path string + } + type want struct { + wantCfg *Config + err error + } + type test struct { + name string + args args + want want + checkFunc func(want, *Config, error) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, gotCfg *Config, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if diff := comparator.Diff(gotCfg, w.wantCfg, + comparator.IgnoreTypes(config.Observability{})); diff != "" { + return errors.New(diff) + } + return nil + } + tests := []test{ + func() test { + var path string + return test{ + name: "return error when can't read file", + args: args{ + path: path, + }, + checkFunc: func(w want, gotCfg *Config, err error) error { + if errors.Is(err, fs.ErrPermission) { + return nil + } + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + if gotCfg != nil { + return errors.Errorf("got cfg: \"%#v\",\n\t\t\t\twant: \"%#v\"", gotCfg, nil) + } + return nil + }, + want: want{ + wantCfg: nil, + err: errors.ErrPathNotSpecified, + }, + } + }(), + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + + gotCfg, err := NewConfig(context.Background(), test.args.path) + if err := checkFunc(test.want, gotCfg, err); err != nil { + tt.Errorf("error = %v, got = %#v", err, gotCfg) + } + }) + } +} diff --git a/pkg/tools/benchmark/job/config/doc.go b/pkg/tools/benchmark/job/config/doc.go new file mode 100644 index 0000000000..aa77bcf2f9 --- /dev/null +++ b/pkg/tools/benchmark/job/config/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 config stores all server application settings +package config diff --git a/pkg/tools/benchmark/job/handler/doc.go b/pkg/tools/benchmark/job/handler/doc.go new file mode 100644 index 0000000000..104280dbd1 --- /dev/null +++ b/pkg/tools/benchmark/job/handler/doc.go @@ -0,0 +1,17 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 handler diff --git a/pkg/tools/benchmark/job/handler/grpc/handler.go b/pkg/tools/benchmark/job/handler/grpc/handler.go new file mode 100644 index 0000000000..c5625079b8 --- /dev/null +++ b/pkg/tools/benchmark/job/handler/grpc/handler.go @@ -0,0 +1,48 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 grpc provides grpc server logic +package grpc + +import ( + "context" + + "github.com/vdaas/vald/pkg/tools/benchmark/job/service" +) + +type Benchmark interface { + Start(context.Context) +} + +type server struct { + job service.Job +} + +func New(opts ...Option) (bm Benchmark, err error) { + b := new(server) + + for _, opt := range append(defaultOpts, opts...) { + err = opt(b) + if err != nil { + return nil, err + } + } + + return b, nil +} + +func (s *server) Start(ctx context.Context) { +} diff --git a/pkg/tools/benchmark/job/handler/grpc/option.go b/pkg/tools/benchmark/job/handler/grpc/option.go new file mode 100644 index 0000000000..ba4449020c --- /dev/null +++ b/pkg/tools/benchmark/job/handler/grpc/option.go @@ -0,0 +1,22 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 grpc provides grpc server logic +package grpc + +type Option func(*server) error + +var defaultOpts = []Option{} diff --git a/pkg/tools/benchmark/job/handler/rest/handler.go b/pkg/tools/benchmark/job/handler/rest/handler.go new file mode 100644 index 0000000000..1de5069055 --- /dev/null +++ b/pkg/tools/benchmark/job/handler/rest/handler.go @@ -0,0 +1,32 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 rest provides rest api logic +package rest + +type Handler interface{} + +type handler struct{} + +func New(opts ...Option) Handler { + h := new(handler) + + for _, opt := range append(defaultOpts, opts...) { + opt(h) + } + + return h +} diff --git a/pkg/tools/benchmark/job/handler/rest/option.go b/pkg/tools/benchmark/job/handler/rest/option.go new file mode 100644 index 0000000000..a7b2465029 --- /dev/null +++ b/pkg/tools/benchmark/job/handler/rest/option.go @@ -0,0 +1,22 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 rest provides rest api logic +package rest + +type Option func(*handler) + +var defaultOpts = []Option{} diff --git a/pkg/tools/benchmark/job/router/doc.go b/pkg/tools/benchmark/job/router/doc.go new file mode 100644 index 0000000000..7a54fc80e3 --- /dev/null +++ b/pkg/tools/benchmark/job/router/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 router provides implementation of Go API for routing http Handler wrapped by rest.Func +package router diff --git a/pkg/tools/benchmark/job/router/option.go b/pkg/tools/benchmark/job/router/option.go new file mode 100644 index 0000000000..739c5d84ab --- /dev/null +++ b/pkg/tools/benchmark/job/router/option.go @@ -0,0 +1,47 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 router provides implementation of Go API for routing http Handler wrapped by rest.Func +package router + +import ( + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/pkg/tools/benchmark/job/handler/rest" +) + +type Option func(*router) + +var defaultOpts = []Option{ + WithTimeout("3s"), +} + +func WithHandler(h rest.Handler) Option { + return func(r *router) { + r.handler = h + } +} + +func WithTimeout(timeout string) Option { + return func(r *router) { + r.timeout = timeout + } +} + +func WithErrGroup(eg errgroup.Group) Option { + return func(r *router) { + r.eg = eg + } +} diff --git a/pkg/tools/benchmark/job/router/router.go b/pkg/tools/benchmark/job/router/router.go new file mode 100644 index 0000000000..07356c8ff2 --- /dev/null +++ b/pkg/tools/benchmark/job/router/router.go @@ -0,0 +1,52 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 router provides implementation of Go API for routing http Handler wrapped by rest.Func +package router + +import ( + "net/http" + + "github.com/vdaas/vald/internal/net/http/middleware" + "github.com/vdaas/vald/internal/net/http/routing" + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/pkg/tools/benchmark/job/handler/rest" +) + +type router struct { + handler rest.Handler + eg errgroup.Group + timeout string +} + +// New returns REST route&method information from handler interface +func New(opts ...Option) http.Handler { + r := new(router) + + for _, opt := range append(defaultOpts, opts...) { + opt(r) + } + + return routing.New( + routing.WithMiddleware( + middleware.NewTimeout( + middleware.WithTimeout(r.timeout), + middleware.WithErrorGroup(r.eg), + )), + routing.WithRoutes([]routing.Route{ + // TODO add REST API interface here + }...)) +} diff --git a/pkg/tools/benchmark/job/service/doc.go b/pkg/tools/benchmark/job/service/doc.go new file mode 100644 index 0000000000..bbd2a56a7c --- /dev/null +++ b/pkg/tools/benchmark/job/service/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service diff --git a/pkg/tools/benchmark/job/service/insert.go b/pkg/tools/benchmark/job/service/insert.go new file mode 100644 index 0000000000..ac1bac0314 --- /dev/null +++ b/pkg/tools/benchmark/job/service/insert.go @@ -0,0 +1,90 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + "strconv" + + "github.com/vdaas/vald/apis/grpc/v1/payload" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +func (j *job) insert(ctx context.Context, ech chan error) error { + log.Info("[benchmark job] Start benchmarking insert") + // create data + vecs := j.hdf5.GetByGroupName(j.dataset.Group) + cfg := &payload.Insert_Config{ + SkipStrictExistCheck: j.insertConfig.SkipStrictExistCheck, + } + if j.timestamp > int64(0) { + cfg.Timestamp = j.timestamp + } + eg, egctx := errgroup.New(ctx) + eg.SetLimit(j.concurrencyLimit) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + iter := i + eg.Go(func() error { + log.Debugf("[benchmark job] Start insert: iter = %d", iter) + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return nil + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + // idx is the modulo, which takes between <0, len(vecs)-1>. + idx := (iter - 1) % len(vecs) + res, err := j.client.Insert(egctx, &payload.Insert_Request{ + Vector: &payload.Object_Vector{ + Id: strconv.Itoa(iter), + Vector: vecs[idx], + }, + Config: cfg, + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return errors.Join(err, egctx.Err()) + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + // TODO: send metrics to the Prometeus + log.Debugf("[benchmark job] Finish insert: iter= %d \n%v\n", iter, res) + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] insert error is detected: err = %s", err.Error()) + return err + } + log.Info("[benchmark job] Finish benchmarking insert") + return nil +} diff --git a/pkg/tools/benchmark/job/service/job.go b/pkg/tools/benchmark/job/service/job.go new file mode 100644 index 0000000000..9f066d8b73 --- /dev/null +++ b/pkg/tools/benchmark/job/service/job.go @@ -0,0 +1,331 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + "os" + "reflect" + "strconv" + "syscall" + "time" + + "github.com/vdaas/vald/apis/grpc/v1/payload" + "github.com/vdaas/vald/internal/client/v1/client/vald" + "github.com/vdaas/vald/internal/config" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/k8s/client" + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/rand" + "github.com/vdaas/vald/internal/safety" + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/internal/test/data/hdf5" + "github.com/vdaas/vald/internal/timeutil/rate" +) + +type Job interface { + PreStart(context.Context) error + Start(context.Context) (<-chan error, error) + Stop(context.Context) error +} + +type jobType int + +const ( + USERDEFINED jobType = iota + INSERT + SEARCH + UPDATE + UPSERT + REMOVE + GETOBJECT + EXISTS +) + +func (jt jobType) String() string { + switch jt { + case USERDEFINED: + return "userdefined" + case INSERT: + return "insert" + case SEARCH: + return "search" + case UPDATE: + return "update" + case UPSERT: + return "upsert" + case REMOVE: + return "remove" + case GETOBJECT: + return "getobject" + case EXISTS: + return "exists" + } + return "" +} + +type job struct { + eg errgroup.Group + dimension int + dataset *config.BenchmarkDataset + jobType jobType + jobFunc func(context.Context, chan error) error + insertConfig *config.InsertConfig + updateConfig *config.UpdateConfig + upsertConfig *config.UpsertConfig + searchConfig *config.SearchConfig + removeConfig *config.RemoveConfig + objectConfig *config.ObjectConfig + client vald.Client + hdf5 hdf5.Data + beforeJobName string + beforeJobNamespace string + k8sClient client.Client + beforeJobDur time.Duration + limiter rate.Limiter + rps int + concurrencyLimit int + timeout time.Duration + timestamp int64 +} + +func New(opts ...Option) (Job, error) { + j := new(job) + for _, opt := range append(defaultOpts, opts...) { + if err := opt(j); err != nil { + return nil, errors.ErrOptionFailed(err, reflect.ValueOf(opt)) + } + } + if j.jobFunc == nil { + switch j.jobType { + case USERDEFINED: + opt := WithJobFunc(j.jobFunc) + err := opt(j) + return nil, errors.ErrOptionFailed(err, reflect.ValueOf(opt)) + case INSERT: + j.jobFunc = j.insert + if j.insertConfig == nil { + return nil, errors.NewErrInvalidOption("insert config", j.insertConfig) + } + ts, err := strconv.Atoi(j.insertConfig.Timestamp) + if err != nil { + log.Warn("[benchmark job]: ", errors.NewErrInvalidOption("insert config timestamp", j.insert, err).Error()) + } else { + j.timestamp = int64(ts) + } + case SEARCH: + j.jobFunc = j.search + if j.searchConfig == nil { + return nil, errors.NewErrInvalidOption("search config", j.searchConfig) + } + to, err := time.ParseDuration(j.searchConfig.Timeout) + if err != nil { + log.Warn("[benchmark job]: ", errors.NewErrInvalidOption("search config timeout", j.searchConfig.Timeout, err).Error()) + } else { + j.timeout = to + } + case UPDATE: + j.jobFunc = j.update + if j.updateConfig == nil { + return nil, errors.NewErrInvalidOption("update config", j.updateConfig) + } + ts, err := strconv.Atoi(j.updateConfig.Timestamp) + if err != nil { + log.Warn("[benchmark job]: ", errors.NewErrInvalidOption("update config timestamp", j.updateConfig.Timestamp, err).Error()) + } else { + j.timestamp = int64(ts) + } + case UPSERT: + j.jobFunc = j.upsert + if j.upsertConfig == nil { + return nil, errors.NewErrInvalidOption("upsert config", j.insertConfig) + } + ts, err := strconv.Atoi(j.upsertConfig.Timestamp) + if err != nil { + log.Warn("[benchmark job]: ", errors.NewErrInvalidOption("upsert config timestamp", j.upsertConfig.Timestamp, err).Error()) + } else { + j.timestamp = int64(ts) + } + case REMOVE: + j.jobFunc = j.remove + if j.removeConfig == nil { + return nil, errors.NewErrInvalidOption("insert config", j.insertConfig) + } + ts, err := strconv.Atoi(j.removeConfig.Timestamp) + if err != nil { + log.Warn("[benchmark job]: ", errors.NewErrInvalidOption("remove config timestamp", j.removeConfig.Timestamp, err).Error()) + } else { + j.timestamp = int64(ts) + } + case GETOBJECT: + j.jobFunc = j.getObject + if j.objectConfig == nil { + log.Warnf("[benchmark job] No get object config is set: %v", j.objectConfig) + } + case EXISTS: + j.jobFunc = j.exists + } + } else if j.jobType != USERDEFINED { + log.Warnf("[benchmark job] userdefined jobFunc is set but jobType is set %s", j.jobType.String()) + } + if j.rps > 0 { + j.limiter = rate.NewLimiter(j.rps) + } + // If (Range.End - Range.Start) is smaller than Indexes, Indexes are prioritized based on Range.Start. + if (j.dataset.Range.End - j.dataset.Range.Start + 1) < j.dataset.Indexes { + j.dataset.Range.End = j.dataset.Range.Start + j.dataset.Indexes + } + + return j, nil +} + +func (j *job) PreStart(ctx context.Context) error { + if j.jobType != GETOBJECT && j.jobType != EXISTS && j.jobType != REMOVE { + log.Infof("[benchmark job] start download dataset of %s", j.hdf5.GetName().String()) + if err := j.hdf5.Download(j.dataset.URL); err != nil { + return err + } + log.Infof("[benchmark job] success download dataset of %s", j.hdf5.GetName().String()) + log.Infof("[benchmark job] start load dataset of %s", j.hdf5.GetName().String()) + var key hdf5.Hdf5Key + switch j.dataset.Group { + case "train": + key = hdf5.Train + case "test": + key = hdf5.Test + case "neighbors": + key = hdf5.Neighors + default: + } + if err := j.hdf5.Read(key); err != nil { + return err + } + log.Infof("[benchmark job] success load dataset of %s", j.hdf5.GetName().String()) + } + // Wait for beforeJob completed if exists + if len(j.beforeJobName) != 0 { + var jobResource v1.ValdBenchmarkJob + log.Info("[benchmark job] check before benchjob is completed or not...") + j.eg.Go(safety.RecoverFunc(func() error { + dt := time.NewTicker(j.beforeJobDur) + defer dt.Stop() + for { + select { + case <-ctx.Done(): + return nil + case <-dt.C: + err := j.k8sClient.Get(ctx, j.beforeJobName, j.beforeJobNamespace, &jobResource) + if err != nil { + return err + } + if jobResource.Status == v1.BenchmarkJobCompleted { + log.Infof("[benchmark job ] before job (%s) is completed, job service will start soon.", j.beforeJobName) + return nil + } + log.Infof("[benchmark job] before job (%s/%s) is not completed...", j.beforeJobName, jobResource.Status) + } + } + })) + if err := j.eg.Wait(); err != nil { + return err + } + } + return nil +} + +func (j *job) Start(ctx context.Context) (<-chan error, error) { + ech := make(chan error, 3) + cech, err := j.client.Start(ctx) + if err != nil { + log.Error("[benchmark job] failed to start connection monitor") + close(ech) + return nil, err + } + j.eg.Go(func() error { + defer close(ech) + for { + select { + case <-ctx.Done(): + return nil + case ech <- <-cech: + } + } + }) + + j.eg.Go(func() (err error) { + defer func() { + p, perr := os.FindProcess(os.Getpid()) + if perr != nil { + log.Error(perr) + return + } + if err != nil { + select { + case <-ctx.Done(): + ech <- errors.Join(err, ctx.Err()) + case ech <- err: + } + } + if err := p.Signal(syscall.SIGTERM); err != nil { + log.Error(err) + } + }() + err = j.jobFunc(ctx, ech) + if err != nil { + log.Errorf("[benchmark job] failed to job: %v", err) + } + return + }) + return ech, nil +} + +func (j *job) Stop(ctx context.Context) (err error) { + err = j.client.Stop(ctx) + return +} + +func calcRecall(linearRes, searchRes *payload.Search_Response) (recall float64) { + if linearRes == nil || searchRes == nil { + return + } + lres := linearRes.Results + sres := searchRes.Results + if len(lres) == 0 || len(sres) == 0 { + return + } + linearIds := map[string]struct{}{} + for _, v := range lres { + linearIds[v.Id] = struct{}{} + } + for _, v := range sres { + if _, ok := linearIds[v.Id]; ok { + recall++ + } + } + return recall / float64(len(lres)) +} + +// TODO: apply many object type +func addNoiseToVec(oVec []float32) []float32 { + noise := rand.Float32() + vec := oVec + idx := rand.LimitedUint32(uint64(len(oVec) - 1)) + vec[idx] += noise + return vec +} diff --git a/pkg/tools/benchmark/job/service/object.go b/pkg/tools/benchmark/job/service/object.go new file mode 100644 index 0000000000..3f644c0239 --- /dev/null +++ b/pkg/tools/benchmark/job/service/object.go @@ -0,0 +1,140 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + "strconv" + + "github.com/vdaas/vald/apis/grpc/v1/payload" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +func (j *job) exists(ctx context.Context, ech chan error) error { + log.Info("[benchmark job] Start benchmarking exists") + eg, egctx := errgroup.New(ctx) + eg.SetLimit(j.concurrencyLimit) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + idx := i + eg.Go(func() error { + log.Debugf("[benchmark job] Start exists: iter = %d", i) + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return nil + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + res, err := j.client.Exists(egctx, &payload.Object_ID{ + Id: strconv.Itoa(idx), + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return nil + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + log.Debugf("[benchmark job] Finish exists: iter= %d \n%v\n", idx, res) + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] exists RPC error is detected: err = %s", err.Error()) + return err + } + log.Info("[benchmark job] Finish benchmarking exists") + return nil +} + +func (j *job) getObject(ctx context.Context, ech chan error) error { + log.Info("[benchmark job] Start benchmarking getObject") + eg, egctx := errgroup.New(ctx) + eg.SetLimit(j.concurrencyLimit) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + log.Infof("[benchmark job] Start get object: iter = %d", i) + ft := []*payload.Filter_Target{} + if j.objectConfig != nil { + for i, target := range j.objectConfig.FilterConfig.Targets { + ft[i] = &payload.Filter_Target{ + Host: target.Host, + Port: uint32(target.Port), + } + } + } + idx := i + eg.Go(func() error { + log.Debugf("[benchmark job] Start get object: iter = %d", idx) + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return nil + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + res, err := j.client.GetObject(egctx, &payload.Object_VectorRequest{ + Id: &payload.Object_ID{ + Id: strconv.Itoa(idx), + }, + Filters: &payload.Filter_Config{ + Targets: ft, + }, + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return nil + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + if res != nil { + log.Infof("[benchmark get object job] iter=%d, Id=%s, Vec=%v", idx, res.GetId(), res.GetVector()) + } + log.Debugf("[benchmark job] Finish get object: iter= %d \n%v\n", idx, res) + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] object error is detected: err = %s", err.Error()) + return err + } + log.Info("[benchmark job] Finish benchmarking getObject") + return nil +} diff --git a/pkg/tools/benchmark/job/service/option.go b/pkg/tools/benchmark/job/service/option.go new file mode 100644 index 0000000000..879b5263e1 --- /dev/null +++ b/pkg/tools/benchmark/job/service/option.go @@ -0,0 +1,268 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + + "github.com/vdaas/vald/internal/client/v1/client/vald" + "github.com/vdaas/vald/internal/config" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/k8s/client" + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/internal/test/data/hdf5" + "github.com/vdaas/vald/internal/timeutil" +) + +type Option func(j *job) error + +var defaultOpts = []Option{ + // TODO: set default config for client + WithDimension(748), + WithBeforeJobDuration("30s"), + WithRPS(1000), + WithConcurencyLimit(200), +} + +// WithDimension sets the vector's dimension for running benchmark job with dataset. +func WithDimension(dim int) Option { + return func(j *job) error { + if dim > 0 { + j.dimension = dim + } + return nil + } +} + +// WithInsertConfig sets the insert API config for running insert request job. +func WithInsertConfig(c *config.InsertConfig) Option { + return func(j *job) error { + if c != nil { + j.insertConfig = c + } + return nil + } +} + +// WithUpdateConfig sets the update API config for running update request job. +func WithUpdateConfig(c *config.UpdateConfig) Option { + return func(j *job) error { + if c != nil { + j.updateConfig = c + } + return nil + } +} + +// WithUpsertConfig sets the upsert API config for running upsert request job. +func WithUpsertConfig(c *config.UpsertConfig) Option { + return func(j *job) error { + if c != nil { + j.upsertConfig = c + } + return nil + } +} + +// WithSearchConfig sets the search API config for running search request job. +func WithSearchConfig(c *config.SearchConfig) Option { + return func(j *job) error { + if c != nil { + j.searchConfig = c + } + return nil + } +} + +// WithRemoveConfig sets the remove API config for running remove request job. +func WithRemoveConfig(c *config.RemoveConfig) Option { + return func(j *job) error { + if c != nil { + j.removeConfig = c + } + return nil + } +} + +// WithObjectConfig sets the get object API config for running get object request job. +func WithObjectConfig(c *config.ObjectConfig) Option { + return func(j *job) error { + if c != nil { + j.objectConfig = c + } + return nil + } +} + +// WithValdClient sets the Vald client for sending request to the target Vald cluster. +func WithValdClient(c vald.Client) Option { + return func(j *job) error { + if c == nil { + return errors.NewErrInvalidOption("client", c) + } + j.client = c + return nil + } +} + +// WithErrGroup sets the errgroup to the job struct to handle errors. +func WithErrGroup(eg errgroup.Group) Option { + return func(j *job) error { + if eg == nil { + return errors.NewErrInvalidOption("error group", eg) + } + j.eg = eg + return nil + } +} + +// WithHdf5 sets the hdf5.Data which is used for benchmark job dataset. +func WithHdf5(d hdf5.Data) Option { + return func(j *job) error { + if d == nil { + return errors.NewErrInvalidOption("hdf5", d) + } + j.hdf5 = d + return nil + } +} + +// WithDataset sets the config.BenchmarkDataset including benchmark dataset name, group name of hdf5.Data, the number of index, start range and end range, and original URL which is used for download user defined hdf5. +func WithDataset(d *config.BenchmarkDataset) Option { + return func(j *job) error { + if d == nil { + return errors.NewErrInvalidOption("dataset", d) + } + if d.Name == hdf5.Original.String() && len(d.URL) == 0 { + return errors.NewErrInvalidOption("dataset", d) + } + j.dataset = d + return nil + } +} + +// WithJobTypeByString converts given string to JobType. +func WithJobTypeByString(t string) Option { + var jt jobType + switch t { + case "userdefined": + jt = USERDEFINED + case "insert": + jt = INSERT + case "search": + jt = SEARCH + case "update": + jt = UPDATE + case "upsert": + jt = UPSERT + case "remove": + jt = REMOVE + case "getobject": + jt = GETOBJECT + case "exists": + jt = EXISTS + } + return WithJobType(jt) +} + +// WithJobType sets the jobType for running benchmark job. +func WithJobType(jt jobType) Option { + return func(j *job) error { + if len(jt.String()) == 0 { + return errors.NewErrInvalidOption("jobType", jt.String()) + } + j.jobType = jt + return nil + } +} + +// WithJobFunc sets the job function. +func WithJobFunc(jf func(context.Context, chan error) error) Option { + return func(j *job) error { + if jf == nil { + return errors.NewErrInvalidOption("jobFunc", jf) + } + j.jobFunc = jf + return nil + } +} + +// WithBeforeJobName sets the beforeJobName which we should wait for until finish before running job. +func WithBeforeJobName(bjn string) Option { + return func(j *job) error { + if len(bjn) > 0 { + j.beforeJobName = bjn + } + return nil + } +} + +// WithBeforeJobNamespace sets the beforeJobNamespace of the beforeJobName which we should wait for until finish before running job. +func WithBeforeJobNamespace(bjns string) Option { + return func(j *job) error { + if len(bjns) > 0 { + j.beforeJobNamespace = bjns + } + return nil + } +} + +// WithBeforeJobDuration sets the duration for watching beforeJobName's status. +func WithBeforeJobDuration(dur string) Option { + return func(j *job) error { + if len(dur) == 0 { + return nil + } + dur, err := timeutil.Parse(dur) + if err != nil { + return err + } + j.beforeJobDur = dur + return nil + } +} + +// WithK8sClient binds the k8s client to the job struct which is used for get BenchmarkJobResource from Kubernetes API server. +func WithK8sClient(cli client.Client) Option { + return func(j *job) error { + if cli != nil { + j.k8sClient = cli + } + return nil + } +} + +// WithRPS sets the rpc for sending request per seconds to the target Vald cluster. +func WithRPS(rps int) Option { + return func(j *job) error { + if rps > 0 { + j.rps = rps + } + return nil + } +} + +// WithConcurencyLimit sets the goroutine limit for sending request to the target cluster. +func WithConcurencyLimit(limit int) Option { + return func(j *job) error { + if limit > 0 { + j.concurrencyLimit = limit + } + return nil + } +} diff --git a/pkg/tools/benchmark/job/service/remove.go b/pkg/tools/benchmark/job/service/remove.go new file mode 100644 index 0000000000..ae6271a57e --- /dev/null +++ b/pkg/tools/benchmark/job/service/remove.go @@ -0,0 +1,84 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + "strconv" + + "github.com/vdaas/vald/apis/grpc/v1/payload" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +func (j *job) remove(ctx context.Context, ech chan error) error { + log.Info("[benchmark job] Start benchmarking remove") + cfg := &payload.Remove_Config{ + SkipStrictExistCheck: j.removeConfig.SkipStrictExistCheck, + } + if j.timestamp > int64(0) { + cfg.Timestamp = j.timestamp + } + eg, egctx := errgroup.New(ctx) + eg.SetLimit(j.concurrencyLimit) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + idx := i + eg.Go(func() error { + log.Debugf("[benchmark job] Start remove: iter = %d", i) + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return nil + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + res, err := j.client.Remove(egctx, &payload.Remove_Request{ + Id: &payload.Object_ID{ + Id: strconv.Itoa(idx), + }, + Config: cfg, + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return errors.Join(err, egctx.Err()) + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + log.Debugf("[benchmark job] Finish remove: iter= %d \n%v", idx, res) + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] remove error is detected: err = %s", err.Error()) + return err + } + log.Info("[benchmark job] Finish benchmarking remove") + return nil +} diff --git a/pkg/tools/benchmark/job/service/search.go b/pkg/tools/benchmark/job/service/search.go new file mode 100644 index 0000000000..4128584344 --- /dev/null +++ b/pkg/tools/benchmark/job/service/search.go @@ -0,0 +1,166 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + + "github.com/vdaas/vald/apis/grpc/v1/payload" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +func (j *job) search(ctx context.Context, ech chan error) error { + log.Info("[benchmark job] Start benchmarking search") + // create data + vecs := j.hdf5.GetByGroupName(j.dataset.Group) + cfg := &payload.Search_Config{ + Num: uint32(j.searchConfig.Num), + MinNum: uint32(j.searchConfig.MinNum), + Radius: float32(j.searchConfig.Radius), + Epsilon: float32(j.searchConfig.Epsilon), + Timeout: j.timeout.Nanoseconds(), + AggregationAlgorithm: func() payload.Search_AggregationAlgorithm { + if len(j.searchConfig.AggregationAlgorithm) > 0 { + if v, ok := payload.Search_AggregationAlgorithm_value[j.searchConfig.AggregationAlgorithm]; ok { + return payload.Search_AggregationAlgorithm(v) + } + } + return 0 + }(), + } + sres := make([]*payload.Search_Response, j.dataset.Indexes) + eg, egctx := errgroup.New(ctx) + eg.SetLimit(j.concurrencyLimit) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + iter := i + eg.Go(func() error { + log.Debugf("[benchmark job] Start search: iter = %d", iter) + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return nil + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + // idx is the modulo, which takes between <0, len(vecs)-1>. + idx := (iter - 1) % len(vecs) + if len(vecs[idx]) != j.dimension { + log.Warn("len(vecs) ", len(vecs[iter]), "is not matched with ", j.dimension) + return nil + } + res, err := j.client.Search(egctx, &payload.Search_Request{ + Vector: vecs[idx], + Config: cfg, + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return nil + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + if res != nil { + if j.searchConfig.EnableLinearSearch { + sres[iter-j.dataset.Range.Start] = res + } + log.Debugf("[benchmark job] Finish search: iter = %d, len = %d", iter, len(res.Results)) + } else { + log.Debugf("[benchmark job] Finish search: iter = %d, res = %v", iter, res) + } + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] search error is detected: err = %s", err.Error()) + return err + } + if j.searchConfig.EnableLinearSearch { + lres := make([]*payload.Search_Response, j.dataset.Indexes) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + iter := i + eg.Go(func() error { + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return nil + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + log.Debugf("[benchmark job] Start linear search: iter = %d", iter) + // idx is the modulo, which takes between <0, len(vecs)-1>. + idx := (iter - 1) % len(vecs) + if len(vecs[idx]) != j.dimension { + log.Warn("len(vecs) ", len(vecs[idx]), "is not matched with ", j.dimension) + return nil + } + res, err := j.client.LinearSearch(egctx, &payload.Search_Request{ + Vector: vecs[idx], + Config: cfg, + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return errors.Join(err, egctx.Err()) + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + if res != nil { + lres[idx-j.dataset.Range.Start] = res + } + log.Debugf("[benchmark job] Finish linear search: iter = %d", iter) + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] linear search error is detected: err = %s", err.Error()) + return err + } + recall := make([]float64, j.dataset.Indexes) + cnt := float64(0) + for i := 0; i < j.dataset.Indexes; i++ { + recall[i] = calcRecall(lres[i], sres[i]) + log.Info("[branch job] search recall: ", recall[i]) + cnt += recall[i] + } + log.Info("[benchmark job] Total search recall: ", (cnt / float64(len(vecs)))) + } + log.Info("[benchmark job] Finish benchmarking search") + return nil +} diff --git a/pkg/tools/benchmark/job/service/update.go b/pkg/tools/benchmark/job/service/update.go new file mode 100644 index 0000000000..cad8b4428c --- /dev/null +++ b/pkg/tools/benchmark/job/service/update.go @@ -0,0 +1,93 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + "strconv" + + "github.com/vdaas/vald/apis/grpc/v1/payload" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +func (j *job) update(ctx context.Context, ech chan error) error { + log.Info("[benchmark job] Start benchmarking update") + // create data + vecs := j.hdf5.GetByGroupName(j.dataset.Group) + cfg := &payload.Update_Config{ + SkipStrictExistCheck: j.updateConfig.SkipStrictExistCheck, + DisableBalancedUpdate: j.updateConfig.DisableBalancedUpdate, + } + if j.timestamp > int64(0) { + cfg.Timestamp = j.timestamp + } + eg, egctx := errgroup.New(ctx) + eg.SetLimit(j.concurrencyLimit) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + iter := i + eg.Go(func() error { + log.Debugf("[benchmark job] Start update: iter = %d", iter) + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return nil + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + // idx is the modulo, which takes between <0, len(vecs)-1>. + idx := (iter - 1) % len(vecs) + res, err := j.client.Update(egctx, &payload.Update_Request{ + Vector: &payload.Object_Vector{ + Id: strconv.Itoa(iter), + Vector: addNoiseToVec(vecs[idx]), + }, + Config: cfg, + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return errors.Join(err, egctx.Err()) + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + if res != nil { + log.Infof("[benchmark job] iter=%d, Name=%s, Uuid=%s, Ips=%v", iter, res.Name, res.Uuid, res.Ips) + } + log.Debugf("[benchmark job] Finish update: iter = %d", iter) + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] update error is detected: err = %s", err.Error()) + return err + } + log.Info("[benchmark job] Finish benchmarking upsert") + return nil +} diff --git a/pkg/tools/benchmark/job/service/upsert.go b/pkg/tools/benchmark/job/service/upsert.go new file mode 100644 index 0000000000..106a32080b --- /dev/null +++ b/pkg/tools/benchmark/job/service/upsert.go @@ -0,0 +1,93 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + "strconv" + + "github.com/vdaas/vald/apis/grpc/v1/payload" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +func (j *job) upsert(ctx context.Context, ech chan error) error { + log.Info("[benchmark job] Start benchmarking upsert") + // create data + vecs := j.hdf5.GetByGroupName(j.dataset.Group) + cfg := &payload.Upsert_Config{ + SkipStrictExistCheck: j.upsertConfig.SkipStrictExistCheck, + DisableBalancedUpdate: j.upsertConfig.DisableBalancedUpdate, + } + if j.timestamp > int64(0) { + cfg.Timestamp = j.timestamp + } + eg, egctx := errgroup.New(ctx) + eg.SetLimit(j.concurrencyLimit) + for i := j.dataset.Range.Start; i <= j.dataset.Range.End; i++ { + iter := i + eg.Go(func() error { + log.Debugf("[benchmark job] Start upsert: iter = %d", iter) + err := j.limiter.Wait(egctx) + if err != nil { + log.Errorf("[benchmark job] limiter error is detected: %s", err.Error()) + if errors.Is(err, context.Canceled) { + return errors.Join(err, context.Canceled) + } + select { + case <-egctx.Done(): + return egctx.Err() + case ech <- err: + } + } + // idx is the modulo, which takes between <0, len(vecs)-1>. + idx := (iter - 1) % len(vecs) + res, err := j.client.Upsert(egctx, &payload.Upsert_Request{ + Vector: &payload.Object_Vector{ + Id: strconv.Itoa(iter), + Vector: addNoiseToVec(vecs[idx]), + }, + Config: cfg, + }) + if err != nil { + select { + case <-egctx.Done(): + log.Errorf("[benchmark job] context error is detected: %s\t%s", err.Error(), egctx.Err()) + return errors.Join(err, egctx.Err()) + default: + // TODO: count up error for observe benchmark job + // We should wait for refactoring internal/o11y. + log.Errorf("[benchmark job] err: %s", err.Error()) + } + } + if res != nil { + log.Infof("[benchmark job] iter=%d, Name=%s, Uuid=%s, Ips=%v", idx, res.Name, res.Uuid, res.Ips) + } + log.Debugf("[benchmark job] Finish upsert: iter = %d", iter) + return nil + }) + } + err := eg.Wait() + if err != nil { + log.Warnf("[benchmark job] upsert error is detected: err = %s", err.Error()) + return err + } + log.Info("[benchmark job] Finish benchmarking upsert") + return nil +} diff --git a/pkg/tools/benchmark/job/usecase/benchmarkd.go b/pkg/tools/benchmark/job/usecase/benchmarkd.go new file mode 100644 index 0000000000..f608d865ef --- /dev/null +++ b/pkg/tools/benchmark/job/usecase/benchmarkd.go @@ -0,0 +1,249 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 usecase provides usecases +package usecase + +import ( + "context" + "strconv" + + "github.com/vdaas/vald/internal/client/v1/client/vald" + iconf "github.com/vdaas/vald/internal/config" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/net/grpc" + "github.com/vdaas/vald/internal/net/grpc/interceptor/server/recover" + "github.com/vdaas/vald/internal/observability" + infometrics "github.com/vdaas/vald/internal/observability/metrics/info" + "github.com/vdaas/vald/internal/runner" + "github.com/vdaas/vald/internal/safety" + "github.com/vdaas/vald/internal/servers/server" + "github.com/vdaas/vald/internal/servers/starter" + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/internal/test/data/hdf5" + "github.com/vdaas/vald/pkg/tools/benchmark/job/config" + handler "github.com/vdaas/vald/pkg/tools/benchmark/job/handler/grpc" + "github.com/vdaas/vald/pkg/tools/benchmark/job/handler/rest" + "github.com/vdaas/vald/pkg/tools/benchmark/job/router" + "github.com/vdaas/vald/pkg/tools/benchmark/job/service" +) + +type run struct { + eg errgroup.Group + cfg *config.Config + job service.Job + h handler.Benchmark + server starter.Server + observability observability.Observability +} + +func New(cfg *config.Config) (r runner.Runner, err error) { + log.Info("pkg/tools/benchmark/job/cmd start") + eg := errgroup.Get() + + if cfg.Job.Target != nil { + addr := cfg.Job.Target.Host + ":" + strconv.Itoa(cfg.Job.Target.Port) + if cfg.Job.ClientConfig.Addrs == nil { + cfg.Job.ClientConfig.Addrs = []string{addr} + } else { + cfg.Job.ClientConfig.Addrs = append(cfg.Job.ClientConfig.Addrs, addr) + } + } + + copts, err := cfg.Job.ClientConfig.Opts() + if err != nil { + return nil, err + } + if cfg.Job.ClientConfig.DialOption == nil { + copts = append(copts, grpc.WithInsecure(true)) + } + gcli := grpc.New(copts...) + vcli, err := vald.New( + vald.WithAddrs(cfg.Job.ClientConfig.Addrs...), + vald.WithClient(gcli), + ) + if err != nil { + return nil, err + } + d, err := hdf5.New( + hdf5.WithNameByString(cfg.Job.Dataset.Name), + ) + if err != nil { + return nil, err + } + log.Info("pkg/tools/benchmark/job/cmd success d") + job, err := service.New( + service.WithErrGroup(eg), + service.WithValdClient(vcli), + service.WithDataset(cfg.Job.Dataset), + service.WithJobTypeByString(cfg.Job.JobType), + service.WithDimension(cfg.Job.Dimension), + service.WithInsertConfig(cfg.Job.InsertConfig), + service.WithUpdateConfig(cfg.Job.UpdateConfig), + service.WithUpsertConfig(cfg.Job.UpsertConfig), + service.WithSearchConfig(cfg.Job.SearchConfig), + service.WithRemoveConfig(cfg.Job.RemoveConfig), + service.WithObjectConfig(cfg.Job.ObjectConfig), + service.WithHdf5(d), + service.WithBeforeJobName(cfg.Job.BeforeJobName), + service.WithBeforeJobNamespace(cfg.Job.BeforeJobNamespace), + service.WithK8sClient(cfg.K8sClient), + service.WithRPS(cfg.Job.RPS), + service.WithConcurencyLimit(cfg.Job.ConcurrencyLimit), + ) + if err != nil { + return nil, err + } + + h, err := handler.New() + if err != nil { + return nil, err + } + + grpcServerOptions := []server.Option{ + server.WithGRPCRegistFunc(func(srv *grpc.Server) { + // TODO register grpc server handler here + }), + server.WithGRPCOption( + grpc.ChainUnaryInterceptor(recover.RecoverInterceptor()), + grpc.ChainStreamInterceptor(recover.RecoverStreamInterceptor()), + ), + server.WithPreStartFunc(func() error { + // TODO check unbackupped upstream + return nil + }), + server.WithPreStopFunction(func() error { + // TODO backup all index data here + return nil + }), + } + + var obs observability.Observability + if cfg.Observability.Enabled { + obs, err = observability.NewWithConfig( + cfg.Observability, + infometrics.New("vald_benchmark_job_info", "Benchmark Job info", *cfg.Job), + ) + if err != nil { + return nil, err + } + } + + srv, err := starter.New( + starter.WithConfig(cfg.Server), + starter.WithREST(func(sc *iconf.Server) []server.Option { + return []server.Option{ + server.WithHTTPHandler( + router.New( + router.WithTimeout(sc.HTTP.HandlerTimeout), + router.WithErrGroup(eg), + router.WithHandler( + rest.New( + // TODO pass grpc handler to REST option + ), + ), + )), + } + }), + starter.WithGRPC(func(sc *iconf.Server) []server.Option { + return grpcServerOptions + }), + ) + if err != nil { + return nil, err + } + log.Info("pkg/tools/benchmark/job/cmd end") + + return &run{ + eg: eg, + cfg: cfg, + job: job, + h: h, + server: srv, + observability: obs, + }, nil +} + +func (r *run) PreStart(ctx context.Context) error { + if r.observability != nil { + if err := r.observability.PreStart(ctx); err != nil { + return err + } + } + if r.job != nil { + return r.job.PreStart(ctx) + } + return nil +} + +func (r *run) Start(ctx context.Context) (<-chan error, error) { + ech := make(chan error, 3) + var oech, dech, sech <-chan error + r.eg.Go(safety.RecoverFunc(func() (err error) { + defer close(ech) + if r.observability != nil { + oech = r.observability.Start(ctx) + } + dech, err = r.job.Start(ctx) + + if err != nil { + ech <- err + return err + } + + r.h.Start(ctx) + + sech = r.server.ListenAndServe(ctx) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case err = <-oech: + case err = <-dech: + case err = <-sech: + } + if err != nil { + select { + case <-ctx.Done(): + log.Error(err) + return errors.Wrap(ctx.Err(), err.Error()) + case ech <- err: + } + } + } + })) + return ech, nil +} + +func (r *run) PreStop(ctx context.Context) error { + return nil +} + +func (r *run) Stop(ctx context.Context) error { + if r.observability != nil { + r.observability.Stop(ctx) + } + if r.job != nil { + r.job.Stop(ctx) + } + return r.server.Shutdown(ctx) +} + +func (r *run) PostStop(ctx context.Context) error { + return nil +} diff --git a/pkg/tools/benchmark/operator/README.md b/pkg/tools/benchmark/operator/README.md new file mode 100644 index 0000000000..3300ab85a9 --- /dev/null +++ b/pkg/tools/benchmark/operator/README.md @@ -0,0 +1 @@ +# vald benchmark operator diff --git a/pkg/tools/benchmark/operator/config/config.go b/pkg/tools/benchmark/operator/config/config.go new file mode 100644 index 0000000000..9bd44965b7 --- /dev/null +++ b/pkg/tools/benchmark/operator/config/config.go @@ -0,0 +1,184 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 config stores all server application settings +package config + +import ( + "github.com/vdaas/vald/internal/config" +) + +// GlobalConfig is type alias for config.GlobalConfig +type GlobalConfig = config.GlobalConfig + +// Config represent a application setting data content (config.yaml). +// In K8s environment, this configuration is stored in K8s ConfigMap. +type Config struct { + config.GlobalConfig `json:",inline" yaml:",inline"` + + // Server represent all server configurations + Server *config.Servers `json:"server_config" yaml:"server_config"` + + // Observability represent observability configurations + Observability *config.Observability `json:"observability" yaml:"observability"` + + // Scenario represents benchmark scenario configurations + Scenario *config.BenchmarkScenario `json:"scenario" yaml:"scenario"` + + // JobImage represents the location of Docker image for benchmark job and its ImagePullPolicy + JobImage *config.BenchmarkJobImageInfo `json:"job_image" yaml:"job_image"` +} + +// NewConfig represents the set config from the given setting file path. +func NewConfig(path string) (cfg *Config, err error) { + err = config.Read(path, &cfg) + if err != nil { + return nil, err + } + + if cfg != nil { + cfg.Bind() + } + + if cfg.Server != nil { + cfg.Server = cfg.Server.Bind() + } + + if cfg.Observability != nil { + cfg.Observability = cfg.Observability.Bind() + } + + if cfg.JobImage != nil { + cfg.JobImage = cfg.JobImage.Bind() + } else { + cfg.JobImage = new(config.BenchmarkJobImageInfo) + } + + if cfg.Scenario != nil { + cfg.Scenario = cfg.Scenario.Bind() + } else { + cfg.Scenario = new(config.BenchmarkScenario) + } + + return cfg, nil +} + +// func FakeData() { +// d := Config{ +// Version: "v0.0.1", +// Server: &config.Servers{ +// Servers: []*config.Server{ +// { +// Name: "agent-rest", +// Host: "127.0.0.1", +// Port: 8080, +// Mode: "REST", +// ProbeWaitTime: "3s", +// ShutdownDuration: "5s", +// HandlerTimeout: "5s", +// IdleTimeout: "2s", +// ReadHeaderTimeout: "1s", +// ReadTimeout: "1s", +// WriteTimeout: "1s", +// }, +// { +// Name: "agent-grpc", +// Host: "127.0.0.1", +// Port: 8082, +// Mode: "GRPC", +// }, +// }, +// MetricsServers: []*config.Server{ +// { +// Name: "pprof", +// Host: "127.0.0.1", +// Port: 6060, +// Mode: "REST", +// ProbeWaitTime: "3s", +// ShutdownDuration: "5s", +// HandlerTimeout: "5s", +// IdleTimeout: "2s", +// ReadHeaderTimeout: "1s", +// ReadTimeout: "1s", +// WriteTimeout: "1s", +// }, +// }, +// HealthCheckServers: []*config.Server{ +// { +// Name: "livenesss", +// Host: "127.0.0.1", +// Port: 3000, +// }, +// { +// Name: "readiness", +// Host: "127.0.0.1", +// Port: 3001, +// }, +// }, +// StartUpStrategy: []string{ +// "livenesss", +// "pprof", +// "agent-grpc", +// "agent-rest", +// "readiness", +// }, +// ShutdownStrategy: []string{ +// "readiness", +// "agent-rest", +// "agent-grpc", +// "pprof", +// "livenesss", +// }, +// FullShutdownDuration: "30s", +// TLS: &config.TLS{ +// Enabled: false, +// Cert: "/path/to/cert", +// Key: "/path/to/key", +// CA: "/path/to/ca", +// }, +// }, +// Scenario: &config.BenchmarkScenario{ +// Target: &config.BenchmarkTarget{ +// Host: "localhost", +// Port: 8081, +// }, +// Dataset: &config.BenchmarkDataset{ +// Name: "fashion-mnist-784-euc", +// Group: "Train", +// Indexes: 10000, +// Range: &config.BenchmarkDatasetRange{ +// Start: 100000, +// End: 200000, +// }, +// }, +// Jobs: []*config.BenchmarkJob{ +// { +// JobType: "search", +// Replica: 1, +// Repetition: 1, +// Dimension: 784, +// Iter: 10000, +// Num: 10, +// MinNum: 100, +// Radius: -1, +// Epsilon: 0.1, +// Timeout: "5s", +// }, +// }, +// }, +// } +// fmt.Println(config.ToRawYaml(d)) +// } diff --git a/pkg/tools/benchmark/operator/config/doc.go b/pkg/tools/benchmark/operator/config/doc.go new file mode 100644 index 0000000000..aa77bcf2f9 --- /dev/null +++ b/pkg/tools/benchmark/operator/config/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 config stores all server application settings +package config diff --git a/pkg/tools/benchmark/operator/handler/doc.go b/pkg/tools/benchmark/operator/handler/doc.go new file mode 100644 index 0000000000..104280dbd1 --- /dev/null +++ b/pkg/tools/benchmark/operator/handler/doc.go @@ -0,0 +1,17 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 handler diff --git a/pkg/tools/benchmark/operator/handler/grpc/handler.go b/pkg/tools/benchmark/operator/handler/grpc/handler.go new file mode 100644 index 0000000000..c91f697ddf --- /dev/null +++ b/pkg/tools/benchmark/operator/handler/grpc/handler.go @@ -0,0 +1,48 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 grpc provides grpc server logic +package grpc + +import ( + "context" + + "github.com/vdaas/vald/pkg/tools/benchmark/operator/service" +) + +type Benchmark interface { + Start(context.Context) +} + +type server struct { + service.Operator +} + +func New(opts ...Option) (bm Benchmark, err error) { + b := new(server) + + for _, opt := range append(defaultOpts, opts...) { + err = opt(b) + if err != nil { + return nil, err + } + } + + return b, nil +} + +func (*server) Start(context.Context) { +} diff --git a/pkg/tools/benchmark/operator/handler/grpc/option.go b/pkg/tools/benchmark/operator/handler/grpc/option.go new file mode 100644 index 0000000000..ba4449020c --- /dev/null +++ b/pkg/tools/benchmark/operator/handler/grpc/option.go @@ -0,0 +1,22 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 grpc provides grpc server logic +package grpc + +type Option func(*server) error + +var defaultOpts = []Option{} diff --git a/pkg/tools/benchmark/operator/handler/rest/handler.go b/pkg/tools/benchmark/operator/handler/rest/handler.go new file mode 100644 index 0000000000..1de5069055 --- /dev/null +++ b/pkg/tools/benchmark/operator/handler/rest/handler.go @@ -0,0 +1,32 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 rest provides rest api logic +package rest + +type Handler interface{} + +type handler struct{} + +func New(opts ...Option) Handler { + h := new(handler) + + for _, opt := range append(defaultOpts, opts...) { + opt(h) + } + + return h +} diff --git a/pkg/tools/benchmark/operator/handler/rest/option.go b/pkg/tools/benchmark/operator/handler/rest/option.go new file mode 100644 index 0000000000..a7b2465029 --- /dev/null +++ b/pkg/tools/benchmark/operator/handler/rest/option.go @@ -0,0 +1,22 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 rest provides rest api logic +package rest + +type Option func(*handler) + +var defaultOpts = []Option{} diff --git a/pkg/tools/benchmark/operator/router/doc.go b/pkg/tools/benchmark/operator/router/doc.go new file mode 100644 index 0000000000..7a54fc80e3 --- /dev/null +++ b/pkg/tools/benchmark/operator/router/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 router provides implementation of Go API for routing http Handler wrapped by rest.Func +package router diff --git a/pkg/tools/benchmark/operator/router/option.go b/pkg/tools/benchmark/operator/router/option.go new file mode 100644 index 0000000000..621b8db7b3 --- /dev/null +++ b/pkg/tools/benchmark/operator/router/option.go @@ -0,0 +1,47 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 router provides implementation of Go API for routing http Handler wrapped by rest.Func +package router + +import ( + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/handler/rest" +) + +type Option func(*router) + +var defaultOpts = []Option{ + WithTimeout("3s"), +} + +func WithHandler(h rest.Handler) Option { + return func(r *router) { + r.handler = h + } +} + +func WithTimeout(timeout string) Option { + return func(r *router) { + r.timeout = timeout + } +} + +func WithErrGroup(eg errgroup.Group) Option { + return func(r *router) { + r.eg = eg + } +} diff --git a/pkg/tools/benchmark/operator/router/router.go b/pkg/tools/benchmark/operator/router/router.go new file mode 100644 index 0000000000..64514c9c35 --- /dev/null +++ b/pkg/tools/benchmark/operator/router/router.go @@ -0,0 +1,52 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 router provides implementation of Go API for routing http Handler wrapped by rest.Func +package router + +import ( + "net/http" + + "github.com/vdaas/vald/internal/net/http/middleware" + "github.com/vdaas/vald/internal/net/http/routing" + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/handler/rest" +) + +type router struct { + handler rest.Handler + eg errgroup.Group + timeout string +} + +// New returns REST route&method information from handler interface +func New(opts ...Option) http.Handler { + r := new(router) + + for _, opt := range append(defaultOpts, opts...) { + opt(r) + } + + return routing.New( + routing.WithMiddleware( + middleware.NewTimeout( + middleware.WithTimeout(r.timeout), + middleware.WithErrorGroup(r.eg), + )), + routing.WithRoutes([]routing.Route{ + // TODO add REST API interface here + }...)) +} diff --git a/pkg/tools/benchmark/operator/service/doc.go b/pkg/tools/benchmark/operator/service/doc.go new file mode 100644 index 0000000000..bbd2a56a7c --- /dev/null +++ b/pkg/tools/benchmark/operator/service/doc.go @@ -0,0 +1,18 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service diff --git a/pkg/tools/benchmark/operator/service/operator.go b/pkg/tools/benchmark/operator/service/operator.go new file mode 100644 index 0000000000..89a6a67b28 --- /dev/null +++ b/pkg/tools/benchmark/operator/service/operator.go @@ -0,0 +1,705 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "context" + "reflect" + "strconv" + "sync/atomic" + "time" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/k8s" + "github.com/vdaas/vald/internal/k8s/client" + "github.com/vdaas/vald/internal/k8s/job" + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + benchjob "github.com/vdaas/vald/internal/k8s/vald/benchmark/job" + benchscenario "github.com/vdaas/vald/internal/k8s/vald/benchmark/scenario" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +type Operator interface { + PreStart(context.Context) error + Start(context.Context) (<-chan error, error) +} + +type scenario struct { + Crd *v1.ValdBenchmarkScenario + BenchJobStatus map[string]v1.BenchmarkJobStatus +} + +const ( + Scenario = "scenario" + ScenarioKind = "ValdBenchmarkScenario" + BenchmarkName = "benchmark-name" + BeforeJobName = "before-job-name" + BeforeJobNamespace = "before-job-namespace" +) + +type operator struct { + jobNamespace string + jobImage string + jobImagePullPolicy string + scenarios *atomic.Pointer[map[string]*scenario] + benchjobs *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] + jobs *atomic.Pointer[map[string]string] + rcd time.Duration // reconcile check duration + eg errgroup.Group + ctrl k8s.Controller +} + +// New creates the new scenario struct to handle vald benchmark job scenario. +// When the input options are invalid, the error will be returned. +func New(opts ...Option) (Operator, error) { + operator := new(operator) + for _, opt := range append(defaultOpts, opts...) { + if err := opt(operator); err != nil { + return nil, errors.ErrOptionFailed(err, reflect.ValueOf(opt)) + } + } + + err := operator.initCtrl() + if err != nil { + return nil, err + } + return operator, nil +} + +// initCtrl creates the controller for reconcile k8s objects. +func (o *operator) initCtrl() error { + // watcher of vald benchmark scenario resource + benchScenario, err := benchscenario.New( + benchscenario.WithControllerName("benchmark scenario resource"), + benchscenario.WithNamespaces(o.jobNamespace), + benchscenario.WithOnErrorFunc(func(err error) { + log.Errorf("failed to reconcile benchmark scenario resource:", err) + }), + benchscenario.WithOnReconcileFunc(o.benchScenarioReconcile), + ) + if err != nil { + return err + } + + // watcher of vald benchmark job resource + benchJob, err := benchjob.New( + benchjob.WithControllerName("benchmark job resource"), + benchjob.WithOnErrorFunc(func(err error) { + log.Errorf("failed to reconcile benchmark job resource:", err) + }), + benchjob.WithNamespaces(o.jobNamespace), + benchjob.WithOnErrorFunc(func(err error) { + log.Error(err) + }), + benchjob.WithOnReconcileFunc(o.benchJobReconcile), + ) + if err != nil { + return err + } + + // watcher of job resource + job, err := job.New( + job.WithControllerName("benchmark job"), + job.WithNamespaces(o.jobNamespace), + job.WithOnErrorFunc(func(err error) { + log.Errorf("failed to reconcile job resource:", err) + }), + job.WithOnReconcileFunc(o.jobReconcile), + ) + if err != nil { + return err + } + + // create reconcile controller which watches valdbenchmarkscenario resource, valdbenchmarkjob resource, and job resource. + o.ctrl, err = k8s.New( + k8s.WithControllerName("vald benchmark scenario operator"), + k8s.WithResourceController(benchScenario), + k8s.WithResourceController(benchJob), + k8s.WithResourceController(job), + ) + return err +} + +func (o *operator) getAtomicScenario() map[string]*scenario { + if o.scenarios == nil { + o.scenarios = &atomic.Pointer[map[string]*scenario]{} + return nil + } + if v := o.scenarios.Load(); v != nil { + return *(v) + } + return nil +} + +func (o *operator) getAtomicBenchJob() map[string]*v1.ValdBenchmarkJob { + if o.benchjobs == nil { + o.benchjobs = &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + return nil + } + if v := o.benchjobs.Load(); v != nil { + return *(v) + } + return nil +} + +func (o *operator) getAtomicJob() map[string]string { + if o.jobs == nil { + o.jobs = &atomic.Pointer[map[string]string]{} + return nil + } + if v := o.jobs.Load(); v != nil { + return *(v) + } + return nil +} + +// jobReconcile gets k8s job list and watches theirs STATUS. +// Then, it processes according STATUS. +// skipcq: GO-R1005 +func (o *operator) jobReconcile(ctx context.Context, jobList map[string][]job.Job) { + log.Debug("[reconcile job] start") + cjobs := o.getAtomicJob() + if cjobs == nil { + cjobs = map[string]string{} + } + if len(jobList) == 0 { + log.Info("[reconcile job] no job is founded") + o.jobs.Store(&(map[string]string{})) + log.Debug("[reconcile job] finish") + return + } + // benchmarkJobStatus is used for update benchmark job resource status + benchmarkJobStatus := make(map[string]v1.BenchmarkJobStatus) + // jobNames is used for check whether cjobs has delted job. + // If cjobs has the delted job, it will be remove the end of jobReconcile function. + jobNames := map[string]struct{}{} + for _, jobs := range jobList { + cnt := len(jobs) + var name string + for idx := range jobs { + job := jobs[idx] + if job.GetNamespace() != o.jobNamespace { + continue + } + jobNames[job.GetName()] = struct{}{} + if _, ok := cjobs[job.Name]; !ok && job.Status.CompletionTime == nil { + cjobs[job.GetName()] = job.Namespace + benchmarkJobStatus[job.GetName()] = v1.BenchmarkJobAvailable + continue + } + name = job.GetName() + if job.Status.Active == 0 && job.Status.Succeeded != 0 { + cnt-- + } + } + if cnt == 0 && name != "" { + benchmarkJobStatus[name] = v1.BenchmarkJobCompleted + } + } + if len(benchmarkJobStatus) != 0 { + _, err := o.updateBenchmarkJobStatus(ctx, benchmarkJobStatus) + if err != nil { + log.Error(err.Error) + } + } + // delete job which is not be in `jobList` from cj. + for k := range cjobs { + if _, ok := jobNames[k]; !ok { + delete(cjobs, k) + } + } + o.jobs.Store(&cjobs) + log.Debug("[reconcile job] finish") +} + +// benchJobReconcile gets the vald benchmark job resource list and create Job for running benchmark job. +// skipcq: GO-R1005 +func (o *operator) benchJobReconcile(ctx context.Context, benchJobList map[string]v1.ValdBenchmarkJob) { + log.Debugf("[reconcile benchmark job resource] job list: %#v", benchJobList) + cbjl := o.getAtomicBenchJob() + if cbjl == nil { + cbjl = make(map[string]*v1.ValdBenchmarkJob, 0) + } + if len(benchJobList) == 0 { + log.Info("[reconcile benchmark job resource] job resource not found") + o.benchjobs.Store(&(map[string]*v1.ValdBenchmarkJob{})) + log.Debug("[reconcile benchmark job resource] finish") + return + } + // jobStatus is used for update benchmarkJob CR status if updating is needed. + jobStatus := make(map[string]v1.BenchmarkJobStatus) + for k := range benchJobList { + // update scenario status + job := benchJobList[k] + hasOwner := false + if len(job.GetOwnerReferences()) > 0 { + hasOwner = true + } + if scenarios := o.getAtomicScenario(); scenarios != nil && hasOwner { + on := job.GetOwnerReferences()[0].Name + if _, ok := scenarios[on]; ok { + if scenarios[on].BenchJobStatus == nil { + scenarios[on].BenchJobStatus = map[string]v1.BenchmarkJobStatus{} + } + scenarios[on].BenchJobStatus[job.Name] = job.Status + } + o.scenarios.Store(&scenarios) + } + if oldJob := cbjl[k]; oldJob != nil { + if oldJob.GetGeneration() != job.GetGeneration() { + if job.Status != "" && oldJob.Status != v1.BenchmarkJobCompleted { + // delete old version job + err := o.deleteJob(ctx, oldJob.GetName()) + if err != nil { + log.Warnf("[reconcile benchmark job resource] failed to delete old version job: job name=%s, version=%d\t%s", oldJob.GetName(), oldJob.GetGeneration(), err.Error()) + } + // create new version job + err = o.createJob(ctx, job) + if err != nil { + log.Errorf("[reconcile benchmark job resource] failed to create new version job: %s", err.Error()) + } + cbjl[k] = &job + } + } else if oldJob.Status == "" { + jobStatus[oldJob.GetName()] = v1.BenchmarkJobAvailable + } + } else if len(job.Status) == 0 || job.Status == v1.BenchmarkJobNotReady { + log.Info("[reconcile benchmark job resource] create job: ", k) + err := o.createJob(ctx, job) + if err != nil { + log.Errorf("[reconcile benchmark job resource] failed to create job: %s", err.Error()) + } + jobStatus[job.Name] = v1.BenchmarkJobAvailable + cbjl[k] = &job + } + } + // delete benchmark job which is not be in `benchJobList` from cbjl. + for k := range cbjl { + if _, ok := benchJobList[k]; !ok { + delete(cbjl, k) + } + } + o.benchjobs.Store(&cbjl) + if len(jobStatus) != 0 { + _, err := o.updateBenchmarkJobStatus(ctx, jobStatus) + if err != nil { + log.Errorf("[reconcile benchmark job resource] failed update job status: %s", err) + } + } + log.Debug("[reconcile benchmark job resource] finish") +} + +// benchScenarioReconcile gets the vald benchmark scenario list and create vald benchmark job resource according to it. +func (o *operator) benchScenarioReconcile(ctx context.Context, scenarioList map[string]v1.ValdBenchmarkScenario) { + log.Debugf("[reconcile benchmark scenario resource] scenario list: %#v", scenarioList) + cbsl := o.getAtomicScenario() + if cbsl == nil { + cbsl = map[string]*scenario{} + } + if len(scenarioList) == 0 { + log.Info("[reconcile benchmark scenario resource]: scenario not found") + o.scenarios.Store(&(map[string]*scenario{})) + log.Debug("[reconcile benchmark scenario resource] finish") + return + } + scenarioStatus := make(map[string]v1.ValdBenchmarkScenarioStatus) + for name := range scenarioList { + sc := scenarioList[name] + if oldScenario := cbsl[name]; oldScenario == nil { + // apply new crd which is not set yet. + jobNames, err := o.createBenchmarkJob(ctx, sc) + if err != nil { + log.Errorf("[reconcile benchmark scenario resource] failed to create benchmark job resource: %s", err.Error()) + } + cbsl[name] = &scenario{ + Crd: &sc, + BenchJobStatus: func() map[string]v1.BenchmarkJobStatus { + s := map[string]v1.BenchmarkJobStatus{} + for _, v := range jobNames { + s[v] = v1.BenchmarkJobNotReady + } + return s + }(), + } + scenarioStatus[sc.GetName()] = v1.BenchmarkScenarioHealthy + } else { + // apply updated crd which is already applied. + if oldScenario.Crd.GetGeneration() < sc.GetGeneration() { + // delete old job resource. If it is succeeded, job pod will be deleted automatically because of OwnerReference. + err := o.deleteBenchmarkJob(ctx, oldScenario.Crd.GetName(), oldScenario.Crd.Generation) + if err != nil { + log.Warnf("[reconcile benchmark scenario resource] failed to delete old version benchmark jobs: scenario name=%s, version=%d\t%s", oldScenario.Crd.GetName(), oldScenario.Crd.Generation, err.Error()) + } + // create new benchmark job resources of new version. + jobNames, err := o.createBenchmarkJob(ctx, sc) + if err != nil { + log.Errorf("[reconcile benchmark scenario resource] failed to create new version benchmark job resource: %s", err.Error()) + } + cbsl[name] = &scenario{ + Crd: &sc, + BenchJobStatus: func() map[string]v1.BenchmarkJobStatus { + s := map[string]v1.BenchmarkJobStatus{} + for _, v := range jobNames { + s[v] = v1.BenchmarkJobNotReady + } + return s + }(), + } + } else if oldScenario.Crd.Status != sc.Status { + // only update status + cbsl[name].Crd.Status = sc.Status + } + } + } + // delete stored crd which is not be in `scenarioList` from cbsl. + for k := range cbsl { + if _, ok := scenarioList[k]; !ok { + delete(cbsl, k) + } + } + o.scenarios.Store(&cbsl) + // Update scenario status + _, err := o.updateBenchmarkScenarioStatus(ctx, scenarioStatus) + if err != nil { + log.Errorf("[reconcile benchmark scenario resource] failed to update benchmark scenario resource status: %s", err.Error()) + } + log.Debug("[reconcile benchmark scenario resource] finish") +} + +// deleteBenchmarkJob deletes benchmark job resource according to given scenario name and generation. +func (o *operator) deleteBenchmarkJob(ctx context.Context, name string, generation int64) error { + opts := new(client.DeleteAllOfOptions) + client.MatchingLabels(map[string]string{ + Scenario: name + strconv.Itoa(int(generation)), + }).ApplyToDeleteAllOf(opts) + client.InNamespace(o.jobNamespace).ApplyToDeleteAllOf(opts) + return o.ctrl.GetManager().GetClient().DeleteAllOf(ctx, &v1.ValdBenchmarkJob{}, opts) +} + +// deleteJob deletes job resource according to given benchmark job name and generation. +func (o *operator) deleteJob(ctx context.Context, name string) error { + cj := new(job.Job) + err := o.ctrl.GetManager().GetClient().Get(ctx, client.ObjectKey{ + Namespace: o.jobNamespace, + Name: name, + }, cj) + if err != nil { + return err + } + opts := new(client.DeleteOptions) + deleteProgation := client.DeletePropagationBackground + opts.PropagationPolicy = &deleteProgation + return o.ctrl.GetManager().GetClient().Delete(ctx, cj, opts) +} + +// createBenchmarkJob creates the ValdBenchmarkJob crd for running job. +func (o *operator) createBenchmarkJob(ctx context.Context, scenario v1.ValdBenchmarkScenario) ([]string, error) { + ownerRef := []k8s.OwnerReference{ + { + APIVersion: scenario.APIVersion, + Kind: scenario.Kind, + Name: scenario.Name, + UID: scenario.UID, + }, + } + jobNames := make([]string, 0) + var beforeJobName string + for _, job := range scenario.Spec.Jobs { + bj := new(v1.ValdBenchmarkJob) + // set metadata.name, metadata.namespace, OwnerReference + bj.Name = scenario.GetName() + "-" + job.JobType + "-" + strconv.FormatInt(time.Now().UnixNano(), 10) + bj.Namespace = scenario.GetNamespace() + bj.SetOwnerReferences(ownerRef) + // set label + labels := map[string]string{ + Scenario: scenario.GetName() + strconv.Itoa(int(scenario.Generation)), + } + bj.SetLabels(labels) + // set annotations for wating before job + annotations := map[string]string{ + BeforeJobName: beforeJobName, + BeforeJobNamespace: o.jobNamespace, + } + bj.SetAnnotations(annotations) + // set specs + bj.Spec = *job + if bj.Spec.Target == nil { + bj.Spec.Target = scenario.Spec.Target + } + if bj.Spec.Dataset == nil { + bj.Spec.Dataset = scenario.Spec.Dataset + } + // set status + bj.Status = v1.BenchmarkJobNotReady + // create benchmark job resource + c := o.ctrl.GetManager().GetClient() + if err := c.Create(ctx, bj); err != nil { + return nil, errors.ErrFailedToCreateBenchmarkJob(err, bj.GetName()) + } + jobNames = append(jobNames, bj.Name) + beforeJobName = bj.Name + } + return jobNames, nil +} + +// createJob creates benchmark job from benchmark job resource. +func (o *operator) createJob(ctx context.Context, bjr v1.ValdBenchmarkJob) error { + label := map[string]string{ + BenchmarkName: bjr.GetName() + strconv.Itoa(int(bjr.GetGeneration())), + } + job, err := benchjob.NewBenchmarkJob( + benchjob.WithContainerName(bjr.GetName()), + benchjob.WithContainerImage(o.jobImage), + benchjob.WithImagePullPolicy(benchjob.ImagePullPolicy(o.jobImagePullPolicy)), + ) + if err != nil { + return err + } + tpl, err := job.CreateJobTpl( + benchjob.WithName(bjr.GetName()), + benchjob.WithNamespace(bjr.Namespace), + benchjob.WithLabel(label), + benchjob.WithCompletions(int32(bjr.Spec.Repetition)), + benchjob.WithParallelism(int32(bjr.Spec.Replica)), + benchjob.WithOwnerRef([]k8s.OwnerReference{ + { + APIVersion: bjr.APIVersion, + Kind: bjr.Kind, + Name: bjr.Name, + UID: bjr.UID, + }, + }), + benchjob.WithTTLSecondsAfterFinished(int32(bjr.Spec.TTLSecondsAfterFinished)), + ) + if err != nil { + return err + } + // create job + c := o.ctrl.GetManager().GetClient() + if err = c.Create(ctx, &tpl); err != nil { + return errors.ErrFailedToCreateJob(err, tpl.GetName()) + } + return nil +} + +// updateBenchmarkScenarioStatus updates status of ValdBenchmarkScenarioResource +func (o *operator) updateBenchmarkScenarioStatus(ctx context.Context, ss map[string]v1.ValdBenchmarkScenarioStatus) ([]string, error) { + var sns []string + if cbsl := o.getAtomicScenario(); cbsl != nil { + for name, status := range ss { + if scenario, ok := cbsl[name]; ok { + if scenario.Crd.Status == status { + continue + } + scenario.Crd.Status = status + cli := o.ctrl.GetManager().GetClient() + err := cli.Status().Update(ctx, scenario.Crd) + if err != nil { + log.Error(err.Error()) + continue + } + sns = append(sns, name) + } + } + } + return sns, nil +} + +// updateBenchmarkJobStatus updates status of ValdBenchmarkJobResource +func (o *operator) updateBenchmarkJobStatus(ctx context.Context, js map[string]v1.BenchmarkJobStatus) ([]string, error) { + var jns []string + if cbjl := o.getAtomicBenchJob(); cbjl != nil { + for name, status := range js { + if bjob, ok := cbjl[name]; ok { + if bjob.Status == status { + continue + } + bjob.Status = status + cli := o.ctrl.GetManager().GetClient() + err := cli.Status().Update(ctx, bjob) + if err != nil { + log.Error(err.Error()) + continue + } + jns = append(jns, name) + } + } + } + return jns, nil +} + +func (o *operator) checkJobsStatus(ctx context.Context, jobs map[string]string) error { + cbjl := o.getAtomicBenchJob() + if jobs == nil || cbjl == nil { + log.Infof("[check job status] no job launched") + return nil + } + job := new(job.Job) + c := o.ctrl.GetManager().GetClient() + jobStatus := map[string]v1.BenchmarkJobStatus{} + for name, ns := range jobs { + err := c.Get(ctx, client.ObjectKey{ + Namespace: ns, + Name: name, + }, job) + if err != nil { + return err + } + if job.Status.Active != 0 || job.Status.Failed != 0 { + continue + } + if job.Status.Succeeded != 0 { + if job, ok := cbjl[name]; ok { + if job.Status != v1.BenchmarkJobCompleted { + jobStatus[name] = v1.BenchmarkJobCompleted + } + } + } + + } + _, err := o.updateBenchmarkJobStatus(ctx, jobStatus) + return err +} + +// checkAtomics checks each atomic keeps consistency. +// skipcq: GO-R1005 +func (o *operator) checkAtomics() error { + cjl := o.getAtomicJob() + cbjl := o.getAtomicBenchJob() + cbsl := o.getAtomicScenario() + bjCompletedCnt := 0 + bjAvailableCnt := 0 + + if len(cbjl) == 0 && len(cbsl) > 0 && len(cjl) > 0 { + log.Errorf("mismatch atomics: job=%v, benchjob=%v, scenario=%v", cjl, cbjl, cbsl) + return errors.ErrMismatchBenchmarkAtomics(cjl, cbjl, cbsl) + } + + for _, bj := range cbjl { + // check bench and job + if bj.Status == v1.BenchmarkJobCompleted { + bjCompletedCnt++ + } else { + bjAvailableCnt++ + if ns, ok := cjl[bj.GetName()]; !ok || ns != bj.GetNamespace() { + log.Errorf("mismatch atomics: job=%v, benchjob=%v, scenario=%v", cjl, cbjl, cbsl) + return errors.ErrMismatchBenchmarkAtomics(cjl, cbjl, cbsl) + } + } + // check scenario and bench + if owners := bj.GetOwnerReferences(); len(owners) > 0 { + var scenarioName string + for _, o := range owners { + if o.Kind == ScenarioKind { + scenarioName = o.Name + } + } + if sc := cbsl[scenarioName]; sc != nil { + if sc.BenchJobStatus[bj.Name] != bj.Status { + log.Errorf("mismatch atomics: job=%v, benchjob=%v, scenario=%v", cjl, cbjl, cbsl) + return errors.ErrMismatchBenchmarkAtomics(cjl, cbjl, cbsl) + } + } else { + log.Errorf("mismatch atomics: job=%v, benchjob=%v, scenario=%v", cjl, cbjl, cbsl) + return errors.ErrMismatchBenchmarkAtomics(cjl, cbjl, cbsl) + } + } + } + // check benchmarkjob status list and scenario benchmark job status list + if len(cbsl) > 0 { + for _, sc := range cbsl { + for _, status := range sc.BenchJobStatus { + if status == v1.BenchmarkJobCompleted { + bjCompletedCnt-- + } else { + bjAvailableCnt-- + } + } + } + if bjAvailableCnt != 0 || bjCompletedCnt != 0 { + log.Errorf("mismatch atomics: job=%v, benchjob=%v, scenario=%v", cjl, cbjl, cbsl) + return errors.ErrMismatchBenchmarkAtomics(cjl, cbjl, cbsl) + } + } + return nil +} + +func (*operator) PreStart(context.Context) error { + log.Infof("[benchmark scenario operator] start vald benchmark scenario operator") + return nil +} + +// skipcq: GO-R1005 +func (o *operator) Start(ctx context.Context) (<-chan error, error) { + scch, err := o.ctrl.Start(ctx) + if err != nil { + return nil, err + } + ech := make(chan error, 2) + o.eg.Go(func() error { + defer close(ech) + rcticker := time.NewTicker(o.rcd) + defer rcticker.Stop() + for { + select { + case <-ctx.Done(): + return nil + case <-rcticker.C: + // check mismatch atomic + err = o.checkAtomics() + if err != nil { + ech <- err + } + // determine whether benchmark scenario status should be updated. + if cbsl := o.getAtomicScenario(); cbsl != nil { + scenarioStatus := make(map[string]v1.ValdBenchmarkScenarioStatus) + for name, scenario := range cbsl { + if scenario.Crd.Status != v1.BenchmarkScenarioCompleted { + cnt := len(scenario.BenchJobStatus) + for _, bjob := range scenario.BenchJobStatus { + if bjob == v1.BenchmarkJobCompleted { + cnt-- + } + } + if cnt == 0 { + scenarioStatus[name] = v1.BenchmarkScenarioCompleted + } + } + } + if _, err := o.updateBenchmarkScenarioStatus(ctx, scenarioStatus); err != nil { + log.Errorf("failed to update benchmark scenario to %s\terror: %s", v1.BenchmarkJobCompleted, err.Error()) + } + + } + // get job and check status + if jobs := o.getAtomicJob(); jobs != nil { + err = o.checkJobsStatus(ctx, jobs) + if err != nil { + log.Error(err.Error()) + } + } + case err = <-scch: + if err != nil { + ech <- err + } + } + } + }) + return ech, nil +} diff --git a/pkg/tools/benchmark/operator/service/operator_test.go b/pkg/tools/benchmark/operator/service/operator_test.go new file mode 100644 index 0000000000..6de4237312 --- /dev/null +++ b/pkg/tools/benchmark/operator/service/operator_test.go @@ -0,0 +1,4516 @@ +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service + +import ( + "context" + "reflect" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/vdaas/vald/internal/config" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/k8s" + "github.com/vdaas/vald/internal/k8s/job" + v1 "github.com/vdaas/vald/internal/k8s/vald/benchmark/api/v1" + "github.com/vdaas/vald/internal/test/goleak" + "github.com/vdaas/vald/internal/test/mock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// mockCtrl is used for mock the request to the Kubernetes API. +type mockCtrl struct { + StartFunc func(ctx context.Context) (<-chan error, error) + GetManagerFunc func() k8s.Manager +} + +func (m *mockCtrl) Start(ctx context.Context) (<-chan error, error) { + return m.StartFunc(ctx) +} + +func (m *mockCtrl) GetManager() k8s.Manager { + return m.GetManagerFunc() +} + +func Test_operator_getAtomicScenario(t *testing.T) { + t.Parallel() + type fields struct { + scenarios *atomic.Pointer[map[string]*scenario] + } + type want struct { + want map[string]*scenario + } + type test struct { + name string + fields fields + want want + checkFunc func(want, map[string]*scenario) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got map[string]*scenario) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + { + name: "get nil when atomic has no resource", + fields: fields{}, + want: want{ + want: nil, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + { + name: "get scenarios when scenario list is stored", + fields: fields{ + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + ap.Store(&map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 10, + Timeout: "10s", + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioHealthy, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scneario-insert": v1.BenchmarkJobAvailable, + "scneario-search": v1.BenchmarkJobAvailable, + }, + }, + }) + return &ap + }(), + }, + want: want{ + want: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 10, + Timeout: "10s", + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioHealthy, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scneario-insert": v1.BenchmarkJobAvailable, + "scneario-search": v1.BenchmarkJobAvailable, + }, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + o := &operator{ + scenarios: test.fields.scenarios, + } + + got := o.getAtomicScenario() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_operator_getAtomicBenchJob(t *testing.T) { + t.Parallel() + type fields struct { + benchjobs *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] + } + type want struct { + want map[string]*v1.ValdBenchmarkJob + } + type test struct { + name string + fields fields + want want + checkFunc func(want, map[string]*v1.ValdBenchmarkJob) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got map[string]*v1.ValdBenchmarkJob) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + { + name: "get nil when atomic has no resource", + fields: fields{}, + want: want{ + want: nil, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + { + name: "get benchjobs when job list is stored", + fields: fields{ + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + m := map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + "scenario-search": { + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 10, + Timeout: "10s", + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + } + ap.Store(&m) + return &ap + }(), + }, + want: want{ + want: map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + "scenario-search": { + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 10, + Timeout: "10s", + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + o := &operator{ + benchjobs: test.fields.benchjobs, + } + + got := o.getAtomicBenchJob() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_operator_getAtomicJob(t *testing.T) { + t.Parallel() + type fields struct { + jobs *atomic.Pointer[map[string]string] + } + type want struct { + want map[string]string + } + type test struct { + name string + fields fields + want want + checkFunc func(want, map[string]string) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, got map[string]string) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + { + name: "get nil when atomic has no resource", + fields: fields{}, + want: want{ + want: nil, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + { + name: "get jobs when jobs has resource", + fields: fields{ + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + m := map[string]string{ + "scenario-insert": "default", + "scenario-search": "default", + } + ap.Store(&m) + return &ap + }(), + }, + want: want{ + want: map[string]string{ + "scenario-insert": "default", + "scenario-search": "default", + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + }, + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + o := &operator{ + jobs: test.fields.jobs, + } + + got := o.getAtomicJob() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_operator_jobReconcile(t *testing.T) { + t.Parallel() + type args struct { + ctx context.Context + jobList map[string][]job.Job + } + type fields struct { + jobNamespace string + jobImage string + jobImagePullPolicy string + scenarios *atomic.Pointer[map[string]*scenario] + benchjobs *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] + jobs *atomic.Pointer[map[string]string] + ctrl k8s.Controller + } + type want struct { + want map[string]string + } + type test struct { + name string + args args + fields fields + want want + checkFunc func(want, map[string]string) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, got map[string]string) error { + if !reflect.DeepEqual(got, w.want) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + return nil + } + tests := []test{ + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success when the length of jobList is 0.", + args: args{ + ctx: ctx, + jobList: map[string][]job.Job{}, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: &atomic.Pointer[map[string]*scenario]{}, + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + }, + want: want{ + want: map[string]string{}, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with new job whose namespace is same as jobNamespace and deleted job by etcd", + args: args{ + ctx: ctx, + jobList: map[string][]job.Job{ + "scenario-insert": { + { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + }, + Status: job.JobStatus{ + Active: 1, + }, + }, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: &atomic.Pointer[map[string]*scenario]{}, + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + m := map[string]string{ + "scenario-completed-insert": "default", + } + ap.Store(&m) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + m := map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + } + ap.Store(&m) + return &ap + }(), + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + want: map[string]string{ + "scenario-insert": "default", + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with completed job whose namespace is same as jobNamespace", + args: args{ + ctx: ctx, + jobList: map[string][]job.Job{ + "scenario-insert": { + { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-completed-insert", + Namespace: "default", + }, + Status: job.JobStatus{ + Active: 0, + Succeeded: 1, + CompletionTime: func() *metav1.Time { + t := &metav1.Time{ + Time: time.Now(), + } + return t + }(), + }, + }, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: &atomic.Pointer[map[string]*scenario]{}, + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + m := map[string]string{ + "scenario-completed-insert": "default", + } + ap.Store(&m) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + m := map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + } + ap.Store(&m) + return &ap + }(), + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + want: map[string]string{ + "scenario-completed-insert": "default", + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with job whose namespace is not same as jobNamespace", + args: args{ + ctx: ctx, + jobList: map[string][]job.Job{ + "scenario-insert": { + { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "benchmark", + }, + Status: job.JobStatus{ + Active: 1, + }, + }, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: &atomic.Pointer[map[string]*scenario]{}, + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: nil, + }, + want: want{ + want: map[string]string{}, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + o := &operator{ + jobNamespace: test.fields.jobNamespace, + jobImage: test.fields.jobImage, + jobImagePullPolicy: test.fields.jobImagePullPolicy, + benchjobs: test.fields.benchjobs, + jobs: test.fields.jobs, + ctrl: test.fields.ctrl, + } + + o.jobReconcile(test.args.ctx, test.args.jobList) + got := o.getAtomicJob() + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_operator_benchJobReconcile(t *testing.T) { + t.Parallel() + type args struct { + ctx context.Context + benchJobList map[string]v1.ValdBenchmarkJob + } + type fields struct { + jobNamespace string + jobImage string + jobImagePullPolicy string + scenarios *atomic.Pointer[map[string]*scenario] + benchjobs *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] + jobs *atomic.Pointer[map[string]string] + ctrl k8s.Controller + } + type want struct { + scenarios map[string]*scenario + benchjobs map[string]*v1.ValdBenchmarkJob + } + type test struct { + name string + args args + fields fields + want want + checkFunc func(want, map[string]*scenario, map[string]*v1.ValdBenchmarkJob) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, gotS map[string]*scenario, gotJ map[string]*v1.ValdBenchmarkJob) error { + if !reflect.DeepEqual(w.scenarios, gotS) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", gotS, w.scenarios) + } + if !reflect.DeepEqual(w.benchjobs, gotJ) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", gotJ, w.benchjobs) + } + return nil + } + tests := []test{ + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success when benchJobList is empty", + args: args{ + ctx: ctx, + benchJobList: map[string]v1.ValdBenchmarkJob{}, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: &atomic.Pointer[map[string]*scenario]{}, + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: nil, + }, + want: want{ + benchjobs: map[string]*v1.ValdBenchmarkJob{}, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success when benchJobList has new benchmark Job with owner reference (reconcile after submitted scenario)", + args: args{ + ctx: ctx, + benchJobList: map[string]v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + m := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + }, + } + ap.Store(&m) + return &ap + }(), + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + scenarios: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": "", + }, + }, + }, + benchjobs: map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success when benchJobList has updated benchmark Job with owner reference (reconcile after updated scenario)", + args: args{ + ctx: ctx, + benchJobList: map[string]v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 2, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 20000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 20000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + m := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 2, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 20000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 20000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobAvailable, + }, + }, + } + ap.Store(&m) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + m := map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + } + ap.Store(&m) + return &ap + }(), + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + scenarios: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 2, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 20000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 20000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobAvailable, + }, + }, + }, + benchjobs: map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 2, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 20000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 20000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success when benchJobList has updated benchmark Job with owner reference (reconcile after updated job status)", + args: args{ + ctx: ctx, + benchJobList: map[string]v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + m := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": "", + }, + }, + } + ap.Store(&m) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + m := map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: "", + }, + } + ap.Store(&m) + return &ap + }(), + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + scenarios: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobAvailable, + }, + }, + }, + benchjobs: map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success when benchJobList has new benchmark Job with owner reference and benchJob has deleted job", + args: args{ + ctx: ctx, + benchJobList: map[string]v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + m := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": "", + }, + }, + } + ap.Store(&m) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + m := map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: "", + }, + "scenario-deleted-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-deleted-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario-deleted", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobCompleted, + }, + } + ap.Store(&m) + return &ap + }(), + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + scenarios: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobAvailable, + }, + }, + }, + benchjobs: map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Name: "scenario", + }, + }, + Generation: 1, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + o := &operator{ + jobNamespace: test.fields.jobNamespace, + jobImage: test.fields.jobImage, + jobImagePullPolicy: test.fields.jobImagePullPolicy, + scenarios: test.fields.scenarios, + benchjobs: test.fields.benchjobs, + jobs: test.fields.jobs, + ctrl: test.fields.ctrl, + } + + o.benchJobReconcile(test.args.ctx, test.args.benchJobList) + gotS := o.getAtomicScenario() + gotJ := o.getAtomicBenchJob() + if err := checkFunc(test.want, gotS, gotJ); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_operator_benchScenarioReconcile(t *testing.T) { + t.Parallel() + type args struct { + ctx context.Context + scenarioList map[string]v1.ValdBenchmarkScenario + } + type fields struct { + jobNamespace string + jobImage string + jobImagePullPolicy string + scenarios *atomic.Pointer[map[string]*scenario] + benchjobs *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] + jobs *atomic.Pointer[map[string]string] + ctrl k8s.Controller + } + type want struct { + want map[string]*scenario + } + type test struct { + name string + args args + fields fields + want want + checkFunc func(want, map[string]*scenario) error + beforeFunc func(*testing.T, args) + afterFunc func(*testing.T, args) + } + defaultCheckFunc := func(w want, got map[string]*scenario) error { + if len(w.want) != len(got) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + for k, ws := range w.want { + gs, ok := got[k] + if !ok { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + // check CRD + if !reflect.DeepEqual(ws.Crd, gs.Crd) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + // check benchJobStatus + if len(ws.BenchJobStatus) != len(gs.BenchJobStatus) { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + for k, v := range gs.BenchJobStatus { + sk := strings.Split(k, "-") + wk := strings.Join(sk[:len(sk)-1], "-") + if v != ws.BenchJobStatus[wk] { + return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) + } + } + } + return nil + } + tests := []test{ + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with scenarioList is empty", + args: args{ + ctx: ctx, + scenarioList: map[string]v1.ValdBenchmarkScenario{}, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: &atomic.Pointer[map[string]*scenario]{}, + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: nil, + }, + want: want{ + want: map[string]*scenario{}, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with scenarioList has new scenario with no scenario has been applied yet.", + args: args{ + ctx: ctx, + scenarioList: map[string]v1.ValdBenchmarkScenario{ + "scenario": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: &atomic.Pointer[map[string]*scenario]{}, + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + want: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioHealthy, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobNotReady, + "scenario-search": v1.BenchmarkJobNotReady, + }, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with scenarioList has only status updated scenario.", + args: args{ + ctx: ctx, + scenarioList: map[string]v1.ValdBenchmarkScenario{ + "scenario": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + m := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioHealthy, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert-1234567890": v1.BenchmarkJobNotReady, + "scenario-search-1234567891": v1.BenchmarkJobNotReady, + }, + }, + } + ap.Store(&m) + return &ap + }(), + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + want: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobNotReady, + "scenario-search": v1.BenchmarkJobNotReady, + }, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with scenarioList has updated scenario when job is already running", + args: args{ + ctx: ctx, + scenarioList: map[string]v1.ValdBenchmarkScenario{ + "scenario": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 2, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 20000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 20000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + m := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobAvailable, + "scenario-search": v1.BenchmarkJobAvailable, + }, + }, + } + ap.Store(&m) + return &ap + }(), + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + want: map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 2, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 20000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 20000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobNotReady, + "scenario-search": v1.BenchmarkJobNotReady, + }, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + func() test { + ctx, cancel := context.WithCancel(context.Background()) + return test{ + name: "success with scenarioList has another scenario when scenario is already running", + args: args{ + ctx: ctx, + scenarioList: map[string]v1.ValdBenchmarkScenario{ + "scenario-v2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-v2", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + }, + }, + fields: fields{ + jobNamespace: "default", + jobImage: "vdaas/vald-benchmark-job", + jobImagePullPolicy: "Always", + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + m := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioAvailable, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobAvailable, + "scenario-search": v1.BenchmarkJobAvailable, + }, + }, + } + ap.Store(&m) + return &ap + }(), + benchjobs: &atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{}, + jobs: &atomic.Pointer[map[string]string]{}, + ctrl: &mockCtrl{ + StartFunc: func(ctx context.Context) (<-chan error, error) { + return nil, nil + }, + GetManagerFunc: func() k8s.Manager { + m := &mock.MockManager{ + Manager: &mock.MockManager{}, + } + return m + }, + }, + }, + want: want{ + want: map[string]*scenario{ + "scenario-v2": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-v2", + Namespace: "default", + Generation: 1, + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 5, + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioHealthy, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-v2-insert": v1.BenchmarkJobNotReady, + "scenario-v2-search": v1.BenchmarkJobNotReady, + }, + }, + }, + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T, args args) { + t.Helper() + }, + afterFunc: func(t *testing.T, args args) { + t.Helper() + cancel() + }, + } + }(), + } + + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt, test.args) + } + if test.afterFunc != nil { + defer test.afterFunc(tt, test.args) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + o := &operator{ + jobNamespace: test.fields.jobNamespace, + jobImage: test.fields.jobImage, + jobImagePullPolicy: test.fields.jobImagePullPolicy, + scenarios: test.fields.scenarios, + benchjobs: test.fields.benchjobs, + jobs: test.fields.jobs, + ctrl: test.fields.ctrl, + } + + o.benchScenarioReconcile(test.args.ctx, test.args.scenarioList) + got := o.getAtomicScenario() + t.Log(got["scenario"]) + if err := checkFunc(test.want, got); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +func Test_operator_checkAtomics(t *testing.T) { + t.Parallel() + type fields struct { + scenarios *atomic.Pointer[map[string]*scenario] + benchjobs *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] + jobs *atomic.Pointer[map[string]string] + } + type want struct { + err error + } + type test struct { + name string + fields fields + want want + checkFunc func(want, error) error + beforeFunc func(*testing.T) + afterFunc func(*testing.T) + } + defaultCheckFunc := func(w want, err error) error { + if !errors.Is(err, w.err) { + return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) + } + return nil + } + defaultScenarioMap := map[string]*scenario{ + "scenario": { + Crd: &v1.ValdBenchmarkScenario{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario", + Namespace: "default", + }, + Spec: v1.ValdBenchmarkScenarioSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + Jobs: []*v1.BenchmarkJobSpec{ + { + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + { + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 10, + Timeout: "10s", + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + { + JobType: "update", + UpdateConfig: &config.UpdateConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + }, + }, + Status: v1.BenchmarkScenarioHealthy, + }, + BenchJobStatus: map[string]v1.BenchmarkJobStatus{ + "scenario-insert": v1.BenchmarkJobCompleted, + "scenario-search": v1.BenchmarkJobAvailable, + "scenario-update": v1.BenchmarkJobAvailable, + }, + }, + } + defaultBenchJobMap := map[string]*v1.ValdBenchmarkJob{ + "scenario-insert": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-insert", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: ScenarioKind, + Name: "scenario", + }, + }, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "insert", + InsertConfig: &config.InsertConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobCompleted, + }, + "scenario-search": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-search", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: ScenarioKind, + Name: "scenario", + }, + }, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "search", + SearchConfig: &config.SearchConfig{ + Epsilon: 0.1, + Radius: -1, + Num: 10, + MinNum: 10, + Timeout: "10s", + EnableLinearSearch: false, + AggregationAlgorithm: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + "scenario-update": { + ObjectMeta: metav1.ObjectMeta{ + Name: "scenario-update", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: ScenarioKind, + Name: "scenario", + }, + }, + }, + Spec: v1.BenchmarkJobSpec{ + Target: &v1.BenchmarkTarget{ + Host: "localhost", + Port: 8080, + }, + Dataset: &v1.BenchmarkDataset{ + Name: "fashion-minsit", + Group: "train", + Indexes: 10000, + Range: &config.BenchmarkDatasetRange{ + Start: 0, + End: 10000, + }, + URL: "", + }, + JobType: "update", + UpdateConfig: &config.UpdateConfig{ + SkipStrictExistCheck: false, + Timestamp: "", + }, + }, + Status: v1.BenchmarkJobAvailable, + }, + } + defaultJobMap := map[string]string{ + "scenario-insert": "default", + "scenario-search": "default", + "scenario-update": "default", + } + tests := []test{ + func() test { + return test{ + name: "return nil with no mismatch atmoics", + fields: fields{ + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + ap.Store(&defaultScenarioMap) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + ap.Store(&defaultBenchJobMap) + return &ap + }(), + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + ap.Store(&defaultJobMap) + return &ap + }(), + }, + want: want{}, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + } + }(), + func() test { + return test{ + name: "return mismatch error when scneario and job has atomic and benchJob has no atomic", + fields: fields{ + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + ap.Store(&defaultScenarioMap) + return &ap + }(), + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + ap.Store(&defaultJobMap) + return &ap + }(), + }, + want: want{ + err: errors.ErrMismatchBenchmarkAtomics(defaultJobMap, map[string]*v1.ValdBenchmarkJob{}, defaultScenarioMap), + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + } + }(), + func() test { + benchJobMap := map[string]*v1.ValdBenchmarkJob{} + for k, v := range defaultBenchJobMap { + val := v1.ValdBenchmarkJob{} + val = *v + benchJobMap[k] = &val + } + benchJobMap["scenario-search"].SetNamespace("benchmark") + return test{ + name: "return mismatch error when benchJob with different namespace", + fields: fields{ + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + ap.Store(&defaultScenarioMap) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + ap.Store(&benchJobMap) + return &ap + }(), + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + ap.Store(&defaultJobMap) + return &ap + }(), + }, + want: want{ + err: errors.ErrMismatchBenchmarkAtomics(defaultJobMap, benchJobMap, defaultScenarioMap), + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + } + }(), + func() test { + benchJobMap := map[string]*v1.ValdBenchmarkJob{} + for k, v := range defaultBenchJobMap { + val := v1.ValdBenchmarkJob{} + val = *v + benchJobMap[k] = &val + } + benchJobMap["scenario-search"].Status = v1.BenchmarkJobNotReady + return test{ + name: "return mismatch error when status is not same between benchJob and scenario.BenchJobStatus", + fields: fields{ + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + ap.Store(&defaultScenarioMap) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + ap.Store(&benchJobMap) + return &ap + }(), + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + ap.Store(&defaultJobMap) + return &ap + }(), + }, + want: want{ + err: errors.ErrMismatchBenchmarkAtomics(defaultJobMap, benchJobMap, defaultScenarioMap), + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + } + }(), + func() test { + benchJobMap := map[string]*v1.ValdBenchmarkJob{} + for k, v := range defaultBenchJobMap { + val := v1.ValdBenchmarkJob{} + val = *v + benchJobMap[k] = &val + } + var ors []metav1.OwnerReference + for _, v := range benchJobMap["scenario-search"].OwnerReferences { + or := v.DeepCopy() + or.Name = "incorrectName" + ors = append(ors, *or) + } + benchJobMap["scenario-search"].OwnerReferences = ors + return test{ + name: "return mismatch error when scenario does not have key of bench job owners scenario", + fields: fields{ + scenarios: func() *atomic.Pointer[map[string]*scenario] { + ap := atomic.Pointer[map[string]*scenario]{} + ap.Store(&defaultScenarioMap) + return &ap + }(), + benchjobs: func() *atomic.Pointer[map[string]*v1.ValdBenchmarkJob] { + ap := atomic.Pointer[map[string]*v1.ValdBenchmarkJob]{} + ap.Store(&benchJobMap) + return &ap + }(), + jobs: func() *atomic.Pointer[map[string]string] { + ap := atomic.Pointer[map[string]string]{} + ap.Store(&defaultJobMap) + return &ap + }(), + }, + want: want{ + err: errors.ErrMismatchBenchmarkAtomics(defaultJobMap, benchJobMap, defaultScenarioMap), + }, + checkFunc: defaultCheckFunc, + beforeFunc: func(t *testing.T) { + t.Helper() + }, + afterFunc: func(t *testing.T) { + t.Helper() + }, + } + }(), + } + for _, tc := range tests { + test := tc + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) + if test.beforeFunc != nil { + test.beforeFunc(tt) + } + if test.afterFunc != nil { + defer test.afterFunc(tt) + } + checkFunc := test.checkFunc + if test.checkFunc == nil { + checkFunc = defaultCheckFunc + } + o := &operator{ + scenarios: test.fields.scenarios, + benchjobs: test.fields.benchjobs, + jobs: test.fields.jobs, + } + + err := o.checkAtomics() + if err := checkFunc(test.want, err); err != nil { + tt.Errorf("error = %v", err) + } + }) + } +} + +// NOT IMPLEMENTED BELOW +// func TestNew(t *testing.T) { +// type args struct { +// opts []Option +// } +// type want struct { +// want Operator +// err error +// } +// type test struct { +// name string +// args args +// want want +// checkFunc func(want, Operator, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, got Operator, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// if !reflect.DeepEqual(got, w.want) { +// return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// // { +// // name: "test_case_1", +// // args: args{ +// // opts: nil, +// // }, +// // want: want{ +// // want: func() Operator { +// // o := &operator{ +// // jobNamespace: "default", +// // jobImage: "vdaas/vald-benchmark-job", +// // jobImagePullPolicy: "Always", +// // rcd: 10 * time.Second, +// // } +// // return o +// // }(), +// // }, +// // checkFunc: defaultCheckFunc, +// // beforeFunc: func(t *testing.T, args args) { +// // t.Helper() +// // }, +// // afterFunc: func(t *testing.T, args args) { +// // t.Helper() +// // }, +// // }, +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// opts:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// +// got, err := New(test.args.opts...) +// if err := checkFunc(test.want, got, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } + +// +// func Test_operator_PreStart(t *testing.T) { +// type args struct { +// ctx context.Context +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// err := o.PreStart(test.args.ctx) +// if err := checkFunc(test.want, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// +// }) +// } +// } +// +// func Test_operator_Start(t *testing.T) { +// type args struct { +// ctx context.Context +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// want <-chan error +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, <-chan error, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, got <-chan error, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// if !reflect.DeepEqual(got, w.want) { +// return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// got, err := o.Start(test.args.ctx) +// if err := checkFunc(test.want, got, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// +// }) +// } +// } + +// func Test_operator_initCtrl(t *testing.T) { +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// err error +// } +// type test struct { +// name string +// fields fields +// want want +// checkFunc func(want, error) error +// beforeFunc func(*testing.T) +// afterFunc func(*testing.T) +// } +// defaultCheckFunc := func(w want, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// return nil +// } +// +// tests := []test{ +// // { +// // name: "test_case_1", +// // fields: fields{ +// // jobNamespace: "default", +// // jobImage: "vdaas/vald-benchmark-job", +// // jobImagePullPolicy: "Always", +// // // scenarios:nil, +// // // benchjobs:nil, +// // // jobs:nil, +// // // rcd:nil, +// // eg: nil, +// // ctrl: nil, +// // }, +// // want: want{ +// // err: errors.New("hoge"), +// // }, +// // checkFunc: defaultCheckFunc, +// // beforeFunc: func(t *testing.T) { +// // t.Helper() +// // }, +// // afterFunc: func(t *testing.T) { +// // t.Helper() +// // }, +// // }, +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T,) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T,) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// err := o.initCtrl() +// if err := checkFunc(test.want, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } + +// func Test_operator_deleteBenchmarkJob(t *testing.T) { +// type args struct { +// ctx context.Context +// name string +// generation int64 +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// name:"", +// generation:0, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// name:"", +// generation:0, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// err := o.deleteBenchmarkJob(test.args.ctx, test.args.name, test.args.generation) +// if err := checkFunc(test.want, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } +// +// func Test_operator_deleteJob(t *testing.T) { +// type args struct { +// ctx context.Context +// name string +// generation int64 +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// name:"", +// generation:0, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// name:"", +// generation:0, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// err := o.deleteJob(test.args.ctx, test.args.name, test.args.generation) +// if err := checkFunc(test.want, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } +// +// func Test_operator_createBenchmarkJob(t *testing.T) { +// type args struct { +// ctx context.Context +// scenario v1.ValdBenchmarkScenario +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// want []string +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, []string, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, got []string, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// if !reflect.DeepEqual(got, w.want) { +// return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// scenario:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// scenario:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// got, err := o.createBenchmarkJob(test.args.ctx, test.args.scenario) +// if err := checkFunc(test.want, got, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } +// +// func Test_operator_createJob(t *testing.T) { +// type args struct { +// ctx context.Context +// bjr v1.ValdBenchmarkJob +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// bjr:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// bjr:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// err := o.createJob(test.args.ctx, test.args.bjr) +// if err := checkFunc(test.want, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } +// +// func Test_operator_updateBenchmarkScenarioStatus(t *testing.T) { +// type args struct { +// ctx context.Context +// ss map[string]v1.ValdBenchmarkScenarioStatus +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// want []string +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, []string, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, got []string, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// if !reflect.DeepEqual(got, w.want) { +// return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// ss:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// ss:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// got, err := o.updateBenchmarkScenarioStatus(test.args.ctx, test.args.ss) +// if err := checkFunc(test.want, got, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } +// +// func Test_operator_updateBenchmarkJobStatus(t *testing.T) { +// type args struct { +// ctx context.Context +// js map[string]v1.BenchmarkJobStatus +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// want []string +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, []string, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, got []string, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// if !reflect.DeepEqual(got, w.want) { +// return errors.Errorf("got: \"%#v\",\n\t\t\t\twant: \"%#v\"", got, w.want) +// } +// return nil +// } +// tests := []test{ +// // TODO test cases +// /* +// { +// name: "test_case_1", +// args: args { +// ctx:nil, +// js:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// }, +// */ +// +// // TODO test cases +// /* +// func() test { +// return test { +// name: "test_case_2", +// args: args { +// ctx:nil, +// js:nil, +// }, +// fields: fields { +// jobNamespace:"", +// jobImage:"", +// jobImagePullPolicy:"", +// scenarios:nil, +// benchjobs:nil, +// jobs:nil, +// rcd:nil, +// eg:nil, +// ctrl:nil, +// }, +// want: want{}, +// checkFunc: defaultCheckFunc, +// beforeFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// afterFunc: func(t *testing.T, args args) { +// t.Helper() +// }, +// } +// }(), +// */ +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// got, err := o.updateBenchmarkJobStatus(test.args.ctx, test.args.js) +// if err := checkFunc(test.want, got, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } +// +// func Test_operator_checkJobsStatus(t *testing.T) { +// type args struct { +// ctx context.Context +// jobs map[string]string +// } +// type fields struct { +// jobNamespace string +// jobImage string +// jobImagePullPolicy string +// scenarios atomic.Pointer[map[string]*scenario] +// benchjobs atomic.Pointer[map[string]*v1.ValdBenchmarkJob] +// jobs atomic.Pointer[map[string]string] +// rcd time.Duration +// eg errgroup.Group +// ctrl k8s.Controller +// } +// type want struct { +// err error +// } +// type test struct { +// name string +// args args +// fields fields +// want want +// checkFunc func(want, error) error +// beforeFunc func(*testing.T, args) +// afterFunc func(*testing.T, args) +// } +// defaultCheckFunc := func(w want, err error) error { +// if !errors.Is(err, w.err) { +// return errors.Errorf("got_error: \"%#v\",\n\t\t\t\twant: \"%#v\"", err, w.err) +// } +// return nil +// } +// tests := []test{ +// // func() test { +// // return test{ +// // name: "test_case_2", +// // args: args{ +// // ctx: nil, +// // jobs: nil, +// // }, +// // fields: fields{ +// // jobNamespace: "", +// // jobImage: "", +// // jobImagePullPolicy: "", +// // scenarios: nil, +// // benchjobs: nil, +// // jobs: nil, +// // rcd: nil, +// // eg: nil, +// // ctrl: nil, +// // }, +// // want: want{}, +// // checkFunc: defaultCheckFunc, +// // beforeFunc: func(t *testing.T, args args) { +// // t.Helper() +// // }, +// // afterFunc: func(t *testing.T, args args) { +// // t.Helper() +// // }, +// // } +// // }(), +// } +// +// for _, tc := range tests { +// test := tc +// t.Run(test.name, func(tt *testing.T) { +// tt.Parallel() +// defer goleak.VerifyNone(tt, goleak.IgnoreCurrent()) +// if test.beforeFunc != nil { +// test.beforeFunc(tt, test.args) +// } +// if test.afterFunc != nil { +// defer test.afterFunc(tt, test.args) +// } +// checkFunc := test.checkFunc +// if test.checkFunc == nil { +// checkFunc = defaultCheckFunc +// } +// o := &operator{ +// jobNamespace: test.fields.jobNamespace, +// jobImage: test.fields.jobImage, +// jobImagePullPolicy: test.fields.jobImagePullPolicy, +// scenarios: test.fields.scenarios, +// benchjobs: test.fields.benchjobs, +// jobs: test.fields.jobs, +// rcd: test.fields.rcd, +// eg: test.fields.eg, +// ctrl: test.fields.ctrl, +// } +// +// err := o.checkJobsStatus(test.args.ctx, test.args.jobs) +// if err := checkFunc(test.want, err); err != nil { +// tt.Errorf("error = %v", err) +// } +// }) +// } +// } diff --git a/pkg/tools/benchmark/operator/service/option.go b/pkg/tools/benchmark/operator/service/option.go new file mode 100644 index 0000000000..11cb1a7a79 --- /dev/null +++ b/pkg/tools/benchmark/operator/service/option.go @@ -0,0 +1,90 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 service manages the main logic of benchmark job. +package service + +import ( + "time" + + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/sync/errgroup" +) + +// Option represents the functional option for scenario struct. +type Option func(o *operator) error + +var defaultOpts = []Option{ + WithJobImage("vdaas/vald-benchmark-job"), + WithJobImagePullPolicy("Always"), + WithReconcileCheckDuration("10s"), + WithJobNamespace("default"), +} + +// WithErrGroup sets the error group to scenario. +func WithErrGroup(eg errgroup.Group) Option { + return func(o *operator) error { + if eg == nil { + return errors.NewErrInvalidOption("client", eg) + } + o.eg = eg + return nil + } +} + +// WithReconcileCheckDuration sets the reconcile check duration from input string. +func WithReconcileCheckDuration(ts string) Option { + return func(o *operator) error { + t, err := time.ParseDuration(ts) + if err != nil { + return err + } + o.rcd = t + return nil + } +} + +// WithJobNamespace sets the namespace for running benchmark job. +func WithJobNamespace(ns string) Option { + return func(o *operator) error { + if ns == "" { + o.jobNamespace = "default" + } else { + o.jobNamespace = ns + } + return nil + } +} + +// WithJobImage sets the benchmark job docker image info. +func WithJobImage(image string) Option { + return func(o *operator) error { + if image != "" { + o.jobImage = image + } + return nil + } +} + +// WithJobImagePullPolicy sets the benchmark job docker image pullPolicy. +func WithJobImagePullPolicy(p string) Option { + return func(o *operator) error { + if p != "" { + o.jobImagePullPolicy = p + } + return nil + } +} diff --git a/pkg/tools/benchmark/operator/usecase/benchmarkd.go b/pkg/tools/benchmark/operator/usecase/benchmarkd.go new file mode 100644 index 0000000000..448d99f36a --- /dev/null +++ b/pkg/tools/benchmark/operator/usecase/benchmarkd.go @@ -0,0 +1,205 @@ +// +// Copyright (C) 2019-2024 vdaas.org vald team +// +// 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 +// +// https://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 usecase provides usecases +package usecase + +import ( + "context" + "os" + + iconf "github.com/vdaas/vald/internal/config" + "github.com/vdaas/vald/internal/errors" + "github.com/vdaas/vald/internal/log" + "github.com/vdaas/vald/internal/net/grpc" + "github.com/vdaas/vald/internal/net/grpc/interceptor/server/recover" + "github.com/vdaas/vald/internal/observability" + infometrics "github.com/vdaas/vald/internal/observability/metrics/info" + "github.com/vdaas/vald/internal/runner" + "github.com/vdaas/vald/internal/safety" + "github.com/vdaas/vald/internal/servers/server" + "github.com/vdaas/vald/internal/servers/starter" + "github.com/vdaas/vald/internal/sync/errgroup" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/config" + handler "github.com/vdaas/vald/pkg/tools/benchmark/operator/handler/grpc" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/handler/rest" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/router" + "github.com/vdaas/vald/pkg/tools/benchmark/operator/service" +) + +type run struct { + eg errgroup.Group + cfg *config.Config + operator service.Operator + h handler.Benchmark + server starter.Server + observability observability.Observability +} + +var JOB_NAMESPACE = os.Getenv("JOB_NAMESPACE") + +func New(cfg *config.Config) (r runner.Runner, err error) { + log.Info("pkg/tools/benchmark/scenario/cmd start") + + eg := errgroup.Get() + + log.Info("pkg/tools/benchmark/scenario/cmd success d") + + operator, err := service.New( + service.WithErrGroup(eg), + service.WithJobNamespace(JOB_NAMESPACE), + service.WithJobImage(cfg.JobImage.Image), + service.WithJobImagePullPolicy(cfg.JobImage.PullPolicy), + ) + if err != nil { + return nil, err + } + + h, err := handler.New() + if err != nil { + return nil, err + } + + grpcServerOptions := []server.Option{ + server.WithGRPCRegistFunc(func(srv *grpc.Server) { + // TODO register grpc server handler here + }), + server.WithGRPCOption( + grpc.ChainUnaryInterceptor(recover.RecoverInterceptor()), + grpc.ChainStreamInterceptor(recover.RecoverStreamInterceptor()), + ), + server.WithPreStartFunc(func() error { + // TODO check unbackupped upstream + return nil + }), + server.WithPreStopFunction(func() error { + // TODO backup all index data here + return nil + }), + } + + var obs observability.Observability + if cfg.Observability.Enabled { + obs, err = observability.NewWithConfig( + cfg.Observability, + infometrics.New("vald_benchmark_scenario_info", "Benchmark Scenario info", *cfg.Scenario), + ) + if err != nil { + return nil, err + } + } + + srv, err := starter.New( + starter.WithConfig(cfg.Server), + starter.WithREST(func(sc *iconf.Server) []server.Option { + return []server.Option{ + server.WithHTTPHandler( + router.New( + router.WithTimeout(sc.HTTP.HandlerTimeout), + router.WithErrGroup(eg), + router.WithHandler( + rest.New( + // TODO pass grpc handler to REST option + ), + ), + )), + } + }), + starter.WithGRPC(func(sc *iconf.Server) []server.Option { + return grpcServerOptions + }), + ) + if err != nil { + return nil, err + } + log.Info("pkg/tools/benchmark/scenario/cmd end") + + return &run{ + eg: eg, + cfg: cfg, + operator: operator, + h: h, + server: srv, + observability: obs, + }, nil +} + +func (r *run) PreStart(ctx context.Context) error { + if r.observability != nil { + if err := r.observability.PreStart(ctx); err != nil { + return err + } + } + if r.operator != nil { + return r.operator.PreStart(ctx) + } + return nil +} + +func (r *run) Start(ctx context.Context) (<-chan error, error) { + ech := make(chan error, 3) + var oech, dech, sech <-chan error + r.eg.Go(safety.RecoverFunc(func() (err error) { + defer close(ech) + if r.observability != nil { + oech = r.observability.Start(ctx) + } + + dech, err = r.operator.Start(ctx) + if err != nil { + ech <- err + return err + } + + r.h.Start(ctx) + + sech = r.server.ListenAndServe(ctx) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case err = <-oech: + case err = <-dech: + case err = <-sech: + } + if err != nil { + select { + case <-ctx.Done(): + log.Error(err) + return errors.Wrap(ctx.Err(), err.Error()) + case ech <- err: + } + } + } + })) + return ech, nil +} + +func (*run) PreStop(context.Context) error { + return nil +} + +func (r *run) Stop(ctx context.Context) error { + if r.observability != nil { + r.observability.Stop(ctx) + } + return r.server.Shutdown(ctx) +} + +func (*run) PostStop(context.Context) error { + return nil +} diff --git a/versions/HDF5_VERSION b/versions/HDF5_VERSION new file mode 100644 index 0000000000..e459802679 --- /dev/null +++ b/versions/HDF5_VERSION @@ -0,0 +1 @@ +hdf5-1_14_3 diff --git a/versions/ZLIB_VERSION b/versions/ZLIB_VERSION new file mode 100644 index 0000000000..7e32cd5698 --- /dev/null +++ b/versions/ZLIB_VERSION @@ -0,0 +1 @@ +1.3