From d2da03a695fea1da7dc4afc4d189268ea25326a9 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Thu, 13 Aug 2026 12:36:45 -0700 Subject: [PATCH 1/2] refactor(sidecarapi): extract the sidecar wire contract into its own module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The controller talked to the sidecar through github.com/sei-protocol/seictl, so it depended on the seictl repo for a contract it co-owns. That dependency also made `go mod tidy` fail outright: seictl requires gogo/protobuf v1.3.3, a version only the regen-network fork has, and the replace that redirects it is seictl's, not ours — Go ignores replace directives in dependency modules. sidecarapi is that contract as its own module: the OpenAPI spec, the client generated from it, and the dependency-free wire types. Eight dependencies, no sei-chain, no k8s, and no replace directives of its own (a dependency module's replaces are ignored, so carrying any would be a silent no-op). The root module resolves it through a filesystem `replace`, deliberately, not a tag. A tagged require would need the commit to exist before the commit that adds it, forcing this into two merges; a `go.work` is worse — it promotes a used module's replaces to main-module status, so a local `go build ./...` would succeed where GOWORK=off (Docker, release CI, external consumers) fails. Also here, because each one is load-bearing rather than incidental: - The k8s v0.35.0 replace block is gone, not inherited. Its comment blamed seictl's transitive constraints; that turned out to be true, so with seictl dropped MVS settles on v0.35.1 unaided and controller-runtime v0.23.1 still builds. Re-derived rather than carried forward. - A depguard rule, `contract-stays-light`, denies the chain graph to anything under sidecarapi/ — _test.go files included, which is where this broke before (seictl#238 added such an import to a test and every consumer's `go mod tidy` stopped working). depguard is the conventional mechanism for an import restriction and it already ships in the linter this repo runs; Kubernetes solves the same problem with import-boss. Note the trailing slash on the sidecar/ deny entry: `pkg` is a prefix match, so without it the rule also denies sidecarapi itself. - CI now fans out per module. Go package patterns stop at a nested module boundary, so `go list ./...` in the root never sees sidecarapi/ — a root-only lint and test would have gone green over uncompiled code. Adds a lint matrix, a hygiene job running `go mod tidy -diff` per module, and MODULES-driven make targets. Between them, depguard catches the offending import and tidy-check catches the unresolvable graph it produces. - Both Dockerfiles copy sidecarapi/go.mod before `go mod download`, and .dockerignore re-includes nested module files. `!go.mod` matched the root only, so the replace target was absent from the build context. - openapi.yaml declared no security on /v0/status while the server requires X-Remote-User there. Harmless while nothing read the spec; not harmless once this module is the contract of record, because reconciling the server "down" to it would expose the status snapshot. /v0/healthz gets an explicit empty security block so public-by-design and block-forgotten stay distinguishable. The 27 import sites move group, not just prefix — the path is under goimports' local-prefixes now, so a bare sed would have left a lint-failing tree. Verified: gofmt, goimports, go build, go vet, tidy-check and verify-generated all clean; 14 root and 3 sidecarapi test packages pass; the controller's build closure holds zero sei-chain/cosmos/cometbft/gogo packages. The depguard rule was exercised against a reintroduction of the seictl#238 import and reports it at the file with its reason. Not verified locally: the Docker builds — no daemon available here, so the .dockerignore fix was checked by simulating pattern resolution. Co-Authored-By: Claude Opus 5 (1M context) --- .dockerignore | 7 +- .github/workflows/ci.yml | 23 + .golangci.yml | 34 + Dockerfile | 6 + Makefile | 32 +- cmd/main.go | 3 +- go.mod | 19 +- go.sum | 28 +- .../node/envtest/workflow_lifecycle_test.go | 2 +- .../node/plan_execution_integration_test.go | 2 +- .../controller/node/plan_execution_test.go | 2 +- internal/controller/nodetask/controller.go | 3 +- .../nodetask/controller_gov_test.go | 3 +- .../controller/nodetask/controller_test.go | 2 +- internal/controller/nodetask/govoutputs.go | 3 +- .../controller/seinetwork/envtest/stubs.go | 3 +- .../seinetwork/envtest/suite_test.go | 3 +- internal/peering/resolver_test.go | 2 +- internal/planner/archive_test.go | 2 +- internal/planner/executor_test.go | 2 +- internal/planner/full_test.go | 2 +- internal/planner/group.go | 2 +- internal/planner/group_accounts_test.go | 2 +- internal/planner/group_test.go | 2 +- internal/planner/planner.go | 2 +- internal/planner/sidecar_probe_test.go | 2 +- internal/planner/workflow.go | 2 +- internal/task/config.go | 3 +- internal/task/config_patch.go | 2 +- internal/task/seinodetask_params.go | 3 +- internal/task/seinodetask_params_test.go | 2 +- internal/task/sidecar.go | 3 +- internal/task/task.go | 2 +- internal/task/workflow_registry_test.go | 2 +- sdk/sei/provider/k8s/handle.go | 1 - sdk/sei/provider/k8s/k8s.go | 1 - sdk/sei/provider/k8s/k8s_test.go | 1 - sdk/sei/provider/k8s/k8s_workflow_test.go | 1 - sdk/sei/provider/k8s/render.go | 1 - sdk/sei/provider/k8s/render_task_test.go | 1 - sdk/sei/provider/k8s/render_workflow_test.go | 1 - sdk/sei/provider/registry_drift_test.go | 1 - sidecarapi/api/codegen.yaml | 7 + sidecarapi/api/generate.go | 3 + sidecarapi/api/openapi.yaml | 283 ++++++ sidecarapi/client/client.go | 360 +++++++ sidecarapi/client/client_test.go | 490 +++++++++ sidecarapi/client/gov_param_change_test.go | 110 +++ sidecarapi/client/sidecar.gen.go | 903 +++++++++++++++++ sidecarapi/client/tasks.go | 929 ++++++++++++++++++ .../client/tasks_genesis_accounts_test.go | 109 ++ sidecarapi/client/tasks_test.go | 637 ++++++++++++ sidecarapi/go.mod | 17 + sidecarapi/go.sum | 629 ++++++++++++ sidecarapi/wire/genesis_accounts.go | 27 + sidecarapi/wire/wire.go | 125 +++ sidecarapi/wire/wire_test.go | 58 ++ test/integration/Dockerfile | 4 + test/integration/giga_migration_test.go | 1 + 59 files changed, 4836 insertions(+), 76 deletions(-) create mode 100644 sidecarapi/api/codegen.yaml create mode 100644 sidecarapi/api/generate.go create mode 100644 sidecarapi/api/openapi.yaml create mode 100644 sidecarapi/client/client.go create mode 100644 sidecarapi/client/client_test.go create mode 100644 sidecarapi/client/gov_param_change_test.go create mode 100644 sidecarapi/client/sidecar.gen.go create mode 100644 sidecarapi/client/tasks.go create mode 100644 sidecarapi/client/tasks_genesis_accounts_test.go create mode 100644 sidecarapi/client/tasks_test.go create mode 100644 sidecarapi/go.mod create mode 100644 sidecarapi/go.sum create mode 100644 sidecarapi/wire/genesis_accounts.go create mode 100644 sidecarapi/wire/wire.go create mode 100644 sidecarapi/wire/wire_test.go diff --git a/.dockerignore b/.dockerignore index 285bc19e..c02e37e1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,9 +6,14 @@ !**/*.go **/*_test.go -# Re-include Go module files +# Re-include Go module files. Both patterns are needed: `!go.mod` matches the +# root module only, so a nested module's go.mod (sidecarapi/go.mod, which the +# root module resolves through a filesystem `replace`) stays excluded without +# the recursive form, and `go mod download` fails on the missing replace target. !go.mod !go.sum +!**/go.mod +!**/go.sum # Re-include embedded shell scripts !**/*.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c968208..1882f140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,13 @@ on: jobs: lint: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # One job per Go module. golangci-lint resolves packages with Go + # patterns, which stop at a nested module boundary — a root-only run + # would never lint sidecarapi/ and would pass while it was broken. + module: [".", "sidecarapi"] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 @@ -17,10 +24,26 @@ jobs: - uses: golangci/golangci-lint-action@v8 with: version: v2.12.1 + working-directory: ${{ matrix.module }} # Temporary override — pre-existing lint debt surfaced by the # v2.8.0 → v2.12.1 bump. Tracked in #163; remove once paid down. only-new-issues: true + hygiene: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + # Every module must be tidy. Without this, the failure mode that + # motivated sidecarapi is invisible in CI: `go build` succeeds while + # `go mod tidy` cannot resolve the graph at all. + - run: make tidy-check + test: runs-on: ubuntu-latest steps: diff --git a/.golangci.yml b/.golangci.yml index 2e2b4197..3bf89dfc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,6 +5,7 @@ linters: default: none enable: - bodyclose + - depguard - copyloopvar - dupl - errcheck @@ -25,6 +26,35 @@ linters: - unparam - unused settings: + depguard: + rules: + # sidecarapi is the sidecar's wire contract, imported by the controller, + # the sidecar module, and the seictl CLI. Only the sidecar module may + # carry the sei-chain graph. + # + # `go mod tidy` walks the test closure of every package the main module + # imports, so a test-only import of a heavy package here — even from a + # _test.go file — makes the controller un-tidyable: sei-cosmos requires + # gogo/protobuf v1.3.3, which exists only in the regen-network fork, and + # a dependency module's replace directive is ignored. That is exactly how + # this broke once before (seictl#238 added such an import to a test). + contract-stays-light: + list-mode: lax + files: + - "**/sidecarapi/**" + deny: + - pkg: github.com/sei-protocol/sei-chain + desc: the chain graph belongs to the sidecar module; a test needing both sides of the wire boundary belongs there + # Trailing slash matters: `pkg` is a prefix match, and without it + # this also denies sidecarapi/* itself. + - pkg: github.com/sei-protocol/sei-k8s-controller/sidecar/ + desc: the contract must not import its own implementation; the sidecar module imports this one, never the reverse + - pkg: github.com/cosmos/cosmos-sdk + desc: chain graph + - pkg: github.com/cometbft + desc: chain graph + - pkg: github.com/gogo/protobuf + desc: chain graph, and only resolvable through the regen-network replace revive: rules: - name: comment-spacings @@ -46,6 +76,10 @@ linters: - dupl - lll path: sdk/* + - linters: + - dupl + - lll + path: sidecarapi/* paths: - third_party$ - builtin$ diff --git a/Dockerfile b/Dockerfile index 748d58e7..1cf8a981 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,12 @@ ARG TARGETARCH WORKDIR /workspace COPY go.mod go.mod COPY go.sum go.sum +# The root module resolves sidecarapi through a filesystem `replace`, so its +# go.mod must be present before `go mod download` — otherwise the replace target +# is missing and the prefetch fails. Copy the manifests only, so this layer +# still caches on dependency changes rather than on every source edit. +COPY sidecarapi/go.mod sidecarapi/go.mod +COPY sidecarapi/go.sum sidecarapi/go.sum RUN go mod download COPY . . diff --git a/Makefile b/Makefile index 3772a076..1cf82b6b 100644 --- a/Makefile +++ b/Makefile @@ -17,16 +17,38 @@ CONTROLLER_GEN_VERSION ?= v0.20.1 LOCALBIN ?= $(CURDIR)/bin SETUP_ENVTEST ?= $(LOCALBIN)/setup-envtest -.PHONY: build test test-integration test-all lint manifests generate verify-generated setup-envtest ci docker-build docker-push +# MODULES is every Go module in this repo. Go package patterns stop at a nested +# module boundary — `go list ./...` in the root does NOT descend into +# sidecarapi/ — so anything that walks packages must loop this list or the +# nested module goes unbuilt, unlinted and untested while CI stays green. +MODULES ?= . sidecarapi + +.PHONY: build test test-modules test-integration test-all lint lint-modules tidy-check manifests generate verify-generated setup-envtest ci docker-build docker-push build: ## Build manager binary. go build -o bin/manager ./cmd/ -test: ## Run tests. +test: test-modules ## Run tests (root module with coverage, then every other module). go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out -lint: ## Run golangci-lint. - $(GOLANGCI_LINT) run +test-modules: ## Run tests in every non-root module. + @set -e; for m in $(MODULES); do \ + [ "$$m" = "." ] && continue; \ + echo "==> go test ./... ($$m)"; \ + (cd $$m && go test ./...); \ + done + +tidy-check: ## Fail if any module's go.mod/go.sum is not tidy. + @set -e; for m in $(MODULES); do \ + echo "==> go mod tidy -diff ($$m)"; \ + (cd $$m && go mod tidy -diff); \ + done + +lint: ## Run golangci-lint over every module. + @set -e; for m in $(MODULES); do \ + echo "==> golangci-lint run ($$m)"; \ + (cd $$m && $(GOLANGCI_LINT) run); \ + done manifests: ## Generate CRD and RBAC manifests. controller-gen rbac:roleName=manager-role crd webhook paths="./..." \ @@ -59,7 +81,7 @@ verify-generated: manifests generate ## Fail if generated artifacts drift from c echo "ERROR: generated artifacts out of date — run 'make manifests generate' and commit"; \ exit 1; } -ci: lint test verify-generated build ## Run lint, test, verify-generated, and build. +ci: lint tidy-check test verify-generated build ## Run lint, tidy-check, test, verify-generated, and build. docker-build: ## Build docker image. docker build --platform linux/amd64 -t ${IMG} . diff --git a/cmd/main.go b/cmd/main.go index 6174cdfc..74372199 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,8 +23,6 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - sidecar "github.com/sei-protocol/seictl/sidecar/client" - seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" nodecontroller "github.com/sei-protocol/sei-k8s-controller/internal/controller/node" nodetaskcontroller "github.com/sei-protocol/sei-k8s-controller/internal/controller/nodetask" @@ -35,6 +33,7 @@ import ( "github.com/sei-protocol/sei-k8s-controller/internal/platform" "github.com/sei-protocol/sei-k8s-controller/internal/sidecartransport" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) var ( diff --git a/go.mod b/go.mod index ba2633d8..77c455fb 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/google/uuid v1.6.0 github.com/onsi/gomega v1.39.1 github.com/sei-protocol/sei-config v0.0.25 - github.com/sei-protocol/seictl v0.0.68 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 go.opentelemetry.io/otel/exporters/prometheus v0.65.0 @@ -23,10 +22,10 @@ require ( go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 golang.org/x/crypto v0.49.0 - k8s.io/api v0.36.0 + k8s.io/api v0.35.1 k8s.io/apiextensions-apiserver v0.35.0 - k8s.io/apimachinery v0.36.0 - k8s.io/client-go v0.36.0 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/gateway-api v1.5.1 @@ -82,13 +81,14 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oapi-codegen/runtime v1.2.0 // indirect + github.com/oapi-codegen/runtime v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.20.1 // indirect + github.com/sei-protocol/sei-k8s-controller/sidecarapi v0.0.0 github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect @@ -129,11 +129,4 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) -// Pin k8s deps to v0.35.0 — controller-runtime v0.23.1 is incompatible -// with v0.36.x. seictl's transitive constraints don't actually require -// the newer versions for what the controller uses (sidecar/client). -replace ( - k8s.io/api => k8s.io/api v0.35.0 - k8s.io/apimachinery => k8s.io/apimachinery v0.35.0 - k8s.io/client-go => k8s.io/client-go v0.35.0 -) +replace github.com/sei-protocol/sei-k8s-controller/sidecarapi => ./sidecarapi diff --git a/go.sum b/go.sum index c26abb84..7f5b264c 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -146,8 +146,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= -github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= +github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc= github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= @@ -170,14 +172,8 @@ github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4Ul github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sei-protocol/sei-config v0.0.23 h1:pGFxRnKoXZLK3Ew/Vd5lgC5RvH9Wo58NnL19CuMgFIk= -github.com/sei-protocol/sei-config v0.0.23/go.mod h1:zcEdLzyIH2AyP0/QRBE3s4Y9eGn0C/qAUx1c4o4EROU= -github.com/sei-protocol/sei-config v0.0.24 h1:DqjehEjC24E7/vVQR6EDPjLOdOUOCegxFwZAeB7S23k= -github.com/sei-protocol/sei-config v0.0.24/go.mod h1:zcEdLzyIH2AyP0/QRBE3s4Y9eGn0C/qAUx1c4o4EROU= github.com/sei-protocol/sei-config v0.0.25 h1:YHW6YOD3DWSF5QRo+Om4TLeQ9o8E8qnG9jcR8E8fjGo= github.com/sei-protocol/sei-config v0.0.25/go.mod h1:zcEdLzyIH2AyP0/QRBE3s4Y9eGn0C/qAUx1c4o4EROU= -github.com/sei-protocol/seictl v0.0.68 h1:dCT94Ys4OjiPt5Y6TWqtgJ8GKTiWv7tN87D8HqIQxbk= -github.com/sei-protocol/seictl v0.0.68/go.mod h1:kI3HIAIWzuJSme8LqJH1WZiITrSIx5yDtJJea07Xt8Y= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -284,16 +280,16 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= -k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= -k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= -k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= -k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= -k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= diff --git a/internal/controller/node/envtest/workflow_lifecycle_test.go b/internal/controller/node/envtest/workflow_lifecycle_test.go index 03a95864..a9f53950 100644 --- a/internal/controller/node/envtest/workflow_lifecycle_test.go +++ b/internal/controller/node/envtest/workflow_lifecycle_test.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" . "github.com/onsi/gomega" - sidecar "github.com/sei-protocol/seictl/sidecar/client" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -24,6 +23,7 @@ import ( "github.com/sei-protocol/sei-k8s-controller/internal/planner" "github.com/sei-protocol/sei-k8s-controller/internal/platform/platformtest" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // fakeSidecar is a controllable task.SidecarClient: every submitted task diff --git a/internal/controller/node/plan_execution_integration_test.go b/internal/controller/node/plan_execution_integration_test.go index ca7edd13..d1c0321f 100644 --- a/internal/controller/node/plan_execution_integration_test.go +++ b/internal/controller/node/plan_execution_integration_test.go @@ -6,12 +6,12 @@ import ( "github.com/google/uuid" . "github.com/onsi/gomega" - sidecar "github.com/sei-protocol/seictl/sidecar/client" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/planner" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // driveTask submits one task and completes it via the full Reconcile pipeline. diff --git a/internal/controller/node/plan_execution_test.go b/internal/controller/node/plan_execution_test.go index 6ce875eb..61ac97b7 100644 --- a/internal/controller/node/plan_execution_test.go +++ b/internal/controller/node/plan_execution_test.go @@ -10,7 +10,6 @@ import ( "github.com/google/uuid" . "github.com/onsi/gomega" seiconfig "github.com/sei-protocol/sei-config" - sidecar "github.com/sei-protocol/seictl/sidecar/client" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,6 +25,7 @@ import ( "github.com/sei-protocol/sei-k8s-controller/internal/planner" "github.com/sei-protocol/sei-k8s-controller/internal/platform/platformtest" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) const ( diff --git a/internal/controller/nodetask/controller.go b/internal/controller/nodetask/controller.go index ce828fc1..b4de4402 100644 --- a/internal/controller/nodetask/controller.go +++ b/internal/controller/nodetask/controller.go @@ -26,12 +26,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - "github.com/sei-protocol/seictl/sidecar/wire" - seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/controller/observability" "github.com/sei-protocol/sei-k8s-controller/internal/platform" "github.com/sei-protocol/sei-k8s-controller/internal/task" + "github.com/sei-protocol/sei-k8s-controller/sidecarapi/wire" ) const ( diff --git a/internal/controller/nodetask/controller_gov_test.go b/internal/controller/nodetask/controller_gov_test.go index 4480b718..371dd64c 100644 --- a/internal/controller/nodetask/controller_gov_test.go +++ b/internal/controller/nodetask/controller_gov_test.go @@ -10,9 +10,8 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - sidecar "github.com/sei-protocol/seictl/sidecar/client" - seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // setResultPayload stages a terminal task result carrying a structured result diff --git a/internal/controller/nodetask/controller_test.go b/internal/controller/nodetask/controller_test.go index 8373553e..8d78b647 100644 --- a/internal/controller/nodetask/controller_test.go +++ b/internal/controller/nodetask/controller_test.go @@ -9,7 +9,6 @@ import ( "github.com/google/uuid" . "github.com/onsi/gomega" - sidecar "github.com/sei-protocol/seictl/sidecar/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -22,6 +21,7 @@ import ( seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/platform/platformtest" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) const ( diff --git a/internal/controller/nodetask/govoutputs.go b/internal/controller/nodetask/govoutputs.go index 748cfdf9..916dec19 100644 --- a/internal/controller/nodetask/govoutputs.go +++ b/internal/controller/nodetask/govoutputs.go @@ -3,10 +3,9 @@ package nodetask import ( "encoding/json" - "github.com/sei-protocol/seictl/sidecar/wire" - seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/task" + "github.com/sei-protocol/sei-k8s-controller/sidecarapi/wire" ) // resulter is the optional accessor sidecarExecution implements to surface the diff --git a/internal/controller/seinetwork/envtest/stubs.go b/internal/controller/seinetwork/envtest/stubs.go index 9231647e..e1f8404d 100644 --- a/internal/controller/seinetwork/envtest/stubs.go +++ b/internal/controller/seinetwork/envtest/stubs.go @@ -14,7 +14,8 @@ import ( "time" "github.com/google/uuid" - sidecar "github.com/sei-protocol/seictl/sidecar/client" + + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // StubSidecarClient satisfies internal/task.SidecarClient (the narrow, diff --git a/internal/controller/seinetwork/envtest/suite_test.go b/internal/controller/seinetwork/envtest/suite_test.go index 05573b77..ace40a0b 100644 --- a/internal/controller/seinetwork/envtest/suite_test.go +++ b/internal/controller/seinetwork/envtest/suite_test.go @@ -29,8 +29,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - sidecar "github.com/sei-protocol/seictl/sidecar/client" - seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" nodecontroller "github.com/sei-protocol/sei-k8s-controller/internal/controller/node" seinetworkcontroller "github.com/sei-protocol/sei-k8s-controller/internal/controller/seinetwork" @@ -38,6 +36,7 @@ import ( "github.com/sei-protocol/sei-k8s-controller/internal/planner" "github.com/sei-protocol/sei-k8s-controller/internal/platform/platformtest" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // Package-level handles populated by TestMain and consumed by individual diff --git a/internal/peering/resolver_test.go b/internal/peering/resolver_test.go index c9622526..8e235a0b 100644 --- a/internal/peering/resolver_test.go +++ b/internal/peering/resolver_test.go @@ -9,7 +9,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/google/uuid" - sidecar "github.com/sei-protocol/seictl/sidecar/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -18,6 +17,7 @@ import ( seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) const ( diff --git a/internal/planner/archive_test.go b/internal/planner/archive_test.go index f8b7db4b..46ef716b 100644 --- a/internal/planner/archive_test.go +++ b/internal/planner/archive_test.go @@ -7,10 +7,10 @@ import ( "testing" seiconfig "github.com/sei-protocol/sei-config" - sidecar "github.com/sei-protocol/seictl/sidecar/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) func TestArchivePlanner_BlockSyncProgression(t *testing.T) { diff --git a/internal/planner/executor_test.go b/internal/planner/executor_test.go index e9395195..06cab6c0 100644 --- a/internal/planner/executor_test.go +++ b/internal/planner/executor_test.go @@ -10,7 +10,6 @@ import ( "maps" "github.com/google/uuid" - sidecar "github.com/sei-protocol/seictl/sidecar/client" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" @@ -19,6 +18,7 @@ import ( seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // mockSidecarClient returns ErrNotFound for GetTask until a task has been diff --git a/internal/planner/full_test.go b/internal/planner/full_test.go index 01bd3580..76859b88 100644 --- a/internal/planner/full_test.go +++ b/internal/planner/full_test.go @@ -5,10 +5,10 @@ import ( "strings" "testing" - sidecar "github.com/sei-protocol/seictl/sidecar/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) func TestFullNodePlanner_Validate_SnapshotGeneration(t *testing.T) { diff --git a/internal/planner/group.go b/internal/planner/group.go index 81498bd5..94721e4f 100644 --- a/internal/planner/group.go +++ b/internal/planner/group.go @@ -4,11 +4,11 @@ import ( "encoding/json" "github.com/google/uuid" - sidecar "github.com/sei-protocol/seictl/sidecar/client" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) const groupAssemblyMaxRetries = 180 diff --git a/internal/planner/group_accounts_test.go b/internal/planner/group_accounts_test.go index daa6c758..e91bffbc 100644 --- a/internal/planner/group_accounts_test.go +++ b/internal/planner/group_accounts_test.go @@ -5,10 +5,10 @@ import ( "strings" "testing" - sidecar "github.com/sei-protocol/seictl/sidecar/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) const ( diff --git a/internal/planner/group_test.go b/internal/planner/group_test.go index 7f176233..4ef3906a 100644 --- a/internal/planner/group_test.go +++ b/internal/planner/group_test.go @@ -4,12 +4,12 @@ import ( "encoding/json" "testing" - sidecar "github.com/sei-protocol/seictl/sidecar/client" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) func TestBuildGroupAssemblyPlan(t *testing.T) { diff --git a/internal/planner/planner.go b/internal/planner/planner.go index fa78c3b0..c06472de 100644 --- a/internal/planner/planner.go +++ b/internal/planner/planner.go @@ -11,7 +11,6 @@ import ( "github.com/google/uuid" seiconfig "github.com/sei-protocol/sei-config" - sidecar "github.com/sei-protocol/seictl/sidecar/client" "go.opentelemetry.io/otel/metric" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -22,6 +21,7 @@ import ( "github.com/sei-protocol/sei-k8s-controller/internal/noderesource" "github.com/sei-protocol/sei-k8s-controller/internal/platform" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) const unknownValue = "unknown" diff --git a/internal/planner/sidecar_probe_test.go b/internal/planner/sidecar_probe_test.go index d76b0f99..23400092 100644 --- a/internal/planner/sidecar_probe_test.go +++ b/internal/planner/sidecar_probe_test.go @@ -7,12 +7,12 @@ import ( "github.com/google/uuid" . "github.com/onsi/gomega" - sidecar "github.com/sei-protocol/seictl/sidecar/client" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) type fakeSidecarClient struct { diff --git a/internal/planner/workflow.go b/internal/planner/workflow.go index dc0e76fb..19ad39e0 100644 --- a/internal/planner/workflow.go +++ b/internal/planner/workflow.go @@ -4,10 +4,10 @@ import ( "fmt" "github.com/google/uuid" - sidecar "github.com/sei-protocol/seictl/sidecar/client" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // Task-type strings for the three new sidecar tasks the StateSync recipe diff --git a/internal/task/config.go b/internal/task/config.go index 64563851..6baa547c 100644 --- a/internal/task/config.go +++ b/internal/task/config.go @@ -2,7 +2,8 @@ package task import ( seiconfig "github.com/sei-protocol/sei-config" - sidecar "github.com/sei-protocol/seictl/sidecar/client" + + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // configApplyTask satisfies sidecar.TaskBuilder for config-apply. The diff --git a/internal/task/config_patch.go b/internal/task/config_patch.go index fb6bb1b9..54e95601 100644 --- a/internal/task/config_patch.go +++ b/internal/task/config_patch.go @@ -1,7 +1,7 @@ package task import ( - sidecar "github.com/sei-protocol/seictl/sidecar/client" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // ConfigPatchTask stamps controller-owned TOML keys into named seid diff --git a/internal/task/seinodetask_params.go b/internal/task/seinodetask_params.go index 3f2e3d97..b8c420b4 100644 --- a/internal/task/seinodetask_params.go +++ b/internal/task/seinodetask_params.go @@ -5,9 +5,8 @@ import ( "errors" "fmt" - sidecar "github.com/sei-protocol/seictl/sidecar/client" - seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // SeiNodeTaskParams holds a synthesized task type and its payload. The caller diff --git a/internal/task/seinodetask_params_test.go b/internal/task/seinodetask_params_test.go index 73ae7f73..e9ffacee 100644 --- a/internal/task/seinodetask_params_test.go +++ b/internal/task/seinodetask_params_test.go @@ -5,10 +5,10 @@ import ( "errors" "testing" - sidecar "github.com/sei-protocol/seictl/sidecar/client" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) const ( diff --git a/internal/task/sidecar.go b/internal/task/sidecar.go index 89921da8..5b339662 100644 --- a/internal/task/sidecar.go +++ b/internal/task/sidecar.go @@ -7,8 +7,9 @@ import ( "fmt" "github.com/google/uuid" - sidecar "github.com/sei-protocol/seictl/sidecar/client" "sigs.k8s.io/controller-runtime/pkg/log" + + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // sidecarExecution is a generic TaskExecution backed by the sidecar HTTP API. diff --git a/internal/task/task.go b/internal/task/task.go index fbe53e68..c97960a3 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -11,12 +11,12 @@ import ( "fmt" "github.com/google/uuid" - sidecar "github.com/sei-protocol/seictl/sidecar/client" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/internal/platform" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // taskIDNamespace is a fixed UUID v5 namespace for generating deterministic diff --git a/internal/task/workflow_registry_test.go b/internal/task/workflow_registry_test.go index 40537516..bb10e947 100644 --- a/internal/task/workflow_registry_test.go +++ b/internal/task/workflow_registry_test.go @@ -7,9 +7,9 @@ import ( "github.com/google/uuid" . "github.com/onsi/gomega" - sidecar "github.com/sei-protocol/seictl/sidecar/client" "github.com/sei-protocol/sei-k8s-controller/internal/task" + sidecar "github.com/sei-protocol/sei-k8s-controller/sidecarapi/client" ) // recordingSidecar records whether GetTask was polled. A fire-and-forget task diff --git a/sdk/sei/provider/k8s/handle.go b/sdk/sei/provider/k8s/handle.go index 4a97276d..1dd0d365 100644 --- a/sdk/sei/provider/k8s/handle.go +++ b/sdk/sei/provider/k8s/handle.go @@ -11,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/util/wait" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" - "github.com/sei-protocol/sei-k8s-controller/sdk/sei" ) diff --git a/sdk/sei/provider/k8s/k8s.go b/sdk/sei/provider/k8s/k8s.go index f92297b8..38fcd5d6 100644 --- a/sdk/sei/provider/k8s/k8s.go +++ b/sdk/sei/provider/k8s/k8s.go @@ -18,7 +18,6 @@ import ( ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" - "github.com/sei-protocol/sei-k8s-controller/sdk/sei" "github.com/sei-protocol/sei-k8s-controller/sdk/sei/provider" ) diff --git a/sdk/sei/provider/k8s/k8s_test.go b/sdk/sei/provider/k8s/k8s_test.go index 8e5a0ca5..baec7289 100644 --- a/sdk/sei/provider/k8s/k8s_test.go +++ b/sdk/sei/provider/k8s/k8s_test.go @@ -17,7 +17,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" - "github.com/sei-protocol/sei-k8s-controller/sdk/sei" ) diff --git a/sdk/sei/provider/k8s/k8s_workflow_test.go b/sdk/sei/provider/k8s/k8s_workflow_test.go index 72b39d0e..6d51e16f 100644 --- a/sdk/sei/provider/k8s/k8s_workflow_test.go +++ b/sdk/sei/provider/k8s/k8s_workflow_test.go @@ -11,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/types" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" - "github.com/sei-protocol/sei-k8s-controller/sdk/sei" ) diff --git a/sdk/sei/provider/k8s/render.go b/sdk/sei/provider/k8s/render.go index f7011bd6..2eee4f39 100644 --- a/sdk/sei/provider/k8s/render.go +++ b/sdk/sei/provider/k8s/render.go @@ -9,7 +9,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" - "github.com/sei-protocol/sei-k8s-controller/sdk/sei" ) diff --git a/sdk/sei/provider/k8s/render_task_test.go b/sdk/sei/provider/k8s/render_task_test.go index 56a0e1b5..10fe6faa 100644 --- a/sdk/sei/provider/k8s/render_task_test.go +++ b/sdk/sei/provider/k8s/render_task_test.go @@ -5,7 +5,6 @@ import ( "time" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" - "github.com/sei-protocol/sei-k8s-controller/sdk/sei" ) diff --git a/sdk/sei/provider/k8s/render_workflow_test.go b/sdk/sei/provider/k8s/render_workflow_test.go index 29fb3364..412f2c4e 100644 --- a/sdk/sei/provider/k8s/render_workflow_test.go +++ b/sdk/sei/provider/k8s/render_workflow_test.go @@ -5,7 +5,6 @@ import ( "time" seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" - "github.com/sei-protocol/sei-k8s-controller/sdk/sei" ) diff --git a/sdk/sei/provider/registry_drift_test.go b/sdk/sei/provider/registry_drift_test.go index b51f6743..3ff4ecec 100644 --- a/sdk/sei/provider/registry_drift_test.go +++ b/sdk/sei/provider/registry_drift_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/sei-protocol/sei-k8s-controller/sdk/sei" - _ "github.com/sei-protocol/sei-k8s-controller/sdk/sei/provider/docker" _ "github.com/sei-protocol/sei-k8s-controller/sdk/sei/provider/k8s" ) diff --git a/sidecarapi/api/codegen.yaml b/sidecarapi/api/codegen.yaml new file mode 100644 index 00000000..b61120ee --- /dev/null +++ b/sidecarapi/api/codegen.yaml @@ -0,0 +1,7 @@ +package: client +output: ../client/sidecar.gen.go +generate: + models: true + client: true +output-options: + skip-prune: true diff --git a/sidecarapi/api/generate.go b/sidecarapi/api/generate.go new file mode 100644 index 00000000..bdd8c4f6 --- /dev/null +++ b/sidecarapi/api/generate.go @@ -0,0 +1,3 @@ +package api + +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.4.1 -config codegen.yaml openapi.yaml diff --git a/sidecarapi/api/openapi.yaml b/sidecarapi/api/openapi.yaml new file mode 100644 index 00000000..251724b1 --- /dev/null +++ b/sidecarapi/api/openapi.yaml @@ -0,0 +1,283 @@ +openapi: 3.1.0 +info: + title: sei-sidecar API + description: | + HTTP API for the sei-sidecar task executor. + + ## Authentication + + Controlled by the `SEI_SIDECAR_AUTHN_MODE` environment variable: + + - **Unset (default):** API is unauthenticated. The sidecar binds + all interfaces. Any actor with network reach to the listen port + can submit txs as the validator's operator account. Acceptable + only on validator-only pod networks. + - **`trusted-header`:** A `kube-rbac-proxy` container fronts this + API on TLS `:8443` and performs TokenReview + a single coarse + `create seinodetasks.sei.io` SAR against the K8s API. The + sidecar binds loopback only and requires `X-Remote-User` on + every request, except for four paths that bypass auth so + probes and scrapes — none of which carry auth headers — keep + working: + + - `/v0/healthz` — kubelet readiness probe + - `/v0/startupz` — kubelet startup probe + - `/v0/livez` — kubelet liveness probe + - `/v0/metrics` — Prometheus scrape + + `kube-rbac-proxy` must include all four in its `--allow-paths`. + version: 0.8.0 + license: + name: Apache-2.0 + +servers: + - url: http://localhost:7777 + +paths: + /v0/healthz: + get: + operationId: healthz + summary: Readiness probe + description: Returns 200 after mark-ready has completed; 503 otherwise. + # Deliberately public: one of the four bypass paths. The kubelet probe + # carries no auth header. Empty rather than absent so the distinction + # between "public by design" and "security block forgotten" is written + # down — see /v0/status. + security: [] + responses: + "200": + description: Sidecar is ready. + "503": + description: Sidecar is not yet ready. + + /v0/status: + get: + operationId: getStatus + summary: Status snapshot + # Authenticated. /v0/status is NOT one of the four bypass paths, so the + # sidecar's middleware requires X-Remote-User here. Stated explicitly + # because this file is the contract of record: an empty security block + # would read as "public" and invite adding this path to the proxy's + # --allow-paths and to the middleware bypass list, which would expose the + # status snapshot unauthenticated. + security: + - remoteUserHeader: [] + responses: + "200": + description: Current status. + content: + application/json: + schema: + $ref: "#/components/schemas/StatusResponse" + + /v0/tasks: + post: + operationId: submitTask + summary: Submit a task + description: | + Submit a task for one-time execution (201). The task type is + carried in the body (`type` field); `params` is task-type- + specific and validated server-side. The endpoint authorizes a + single coarse SAR (`create seinodetasks.sei.io`) regardless of + task type — per-task narrowing is additive via `resourceNames` + on the ClusterRole. + security: + - remoteUserHeader: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TaskRequest" + responses: + "201": + description: Task created. + content: + application/json: + schema: + $ref: "#/components/schemas/TaskSubmitResponse" + "202": + description: Task accepted (completed synchronously or deduplicated). + content: + application/json: + schema: + $ref: "#/components/schemas/TaskSubmitResponse" + "400": + description: Invalid request. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + get: + operationId: listTasks + summary: List recent task results + description: Returns the most recent task results. + security: + - remoteUserHeader: [] + responses: + "200": + description: Task results. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TaskResult" + + /v0/tasks/{id}: + get: + operationId: getTask + summary: Get a task result + security: + - remoteUserHeader: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Task result. + content: + application/json: + schema: + $ref: "#/components/schemas/TaskResult" + "404": + description: Task not found. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + delete: + operationId: deleteTask + summary: Remove a task + description: Removes a task result or cancels an active task. + security: + - remoteUserHeader: [] + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "204": + description: Task deleted. + "404": + description: Task not found. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "503": + description: >- + Transient store failure; the task is left recoverable and the + DELETE is safe to retry. A Retry-After header advises the delay. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + +components: + securitySchemes: + remoteUserHeader: + type: apiKey + in: header + name: X-Remote-User + description: | + Trusted-header model: the sidecar trusts the value of + `X-Remote-User` because it binds loopback and a co-located + `kube-rbac-proxy` is the sole reachable client. The proxy + performs TokenReview + SAR against the K8s API and forwards + the authenticated identity in this header. Tooling MUST NOT + send this header directly — it is set by the proxy. + + Only applies when `SEI_SIDECAR_AUTHN_MODE=trusted-header`. With + the env var unset the API is unauthenticated and this scheme + is not enforced. + + schemas: + TaskRequest: + type: object + required: [type] + properties: + id: + type: string + format: uuid + description: | + Caller-provided task identifier. When set, the engine uses + this as the canonical ID (enabling deterministic IDs from + the controller); if a task with this ID exists, the request + is idempotent and returns the existing ID. When omitted, a + random UUID is generated. + type: + type: string + description: Task type identifier. + params: + type: object + additionalProperties: true + description: Task-type-specific parameters; validated server-side. + + StatusResponse: + type: object + required: [status] + properties: + status: + type: string + enum: [Initializing, Ready] + + TaskSubmitResponse: + type: object + required: [id] + properties: + id: + type: string + format: uuid + description: The assigned task UUID. + + TaskResult: + type: object + required: [id, type, status, submittedAt] + properties: + id: + type: string + format: uuid + type: + type: string + description: Task type that was executed. + status: + type: string + enum: [running, completed, failed] + description: Current task lifecycle state. + params: + type: object + additionalProperties: true + result: + type: object + additionalProperties: true + x-go-type: json.RawMessage + description: | + Handler's structured result, present on any task that emits one — + on both success and failure (e.g. assemble-and-upload-genesis + returns {"genesisHash":""} on success; a gov submit stamps + txHash/inclusionStatus even when the task fails). Delivered over this + trusted channel rather than via shared storage. + error: + type: string + description: Error message if the task failed. + submittedAt: + type: string + format: date-time + completedAt: + type: string + format: date-time + + ErrorResponse: + type: object + required: [error] + properties: + error: + type: string diff --git a/sidecarapi/client/client.go b/sidecarapi/client/client.go new file mode 100644 index 00000000..306b3935 --- /dev/null +++ b/sidecarapi/client/client.go @@ -0,0 +1,360 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "github.com/google/uuid" +) + +const DefaultPort int32 = 7777 + +// ErrNotFound is returned when the requested task does not exist (HTTP 404). +var ErrNotFound = errors.New("sidecar: task not found") + +// SidecarClient wraps the generated ClientWithResponses with a simpler, +// error-oriented API. +type SidecarClient struct { + inner *ClientWithResponses + baseURL string + doer HttpRequestDoer +} + +// Option configures optional SidecarClient parameters. +type Option func(*sidecarOpts) + +type sidecarOpts struct { + httpClient HttpRequestDoer + timeout time.Duration +} + +// WithHTTPDoer overrides the underlying HTTP transport. +func WithHTTPDoer(doer HttpRequestDoer) Option { + return func(o *sidecarOpts) { o.httpClient = doer } +} + +// WithTimeout sets the HTTP client timeout. Defaults to 10s. +func WithTimeout(d time.Duration) Option { + return func(o *sidecarOpts) { o.timeout = d } +} + +// NewSidecarClient creates a client from an explicit base URL. +func NewSidecarClient(baseURL string, opts ...Option) (*SidecarClient, error) { + o := sidecarOpts{timeout: 10 * time.Second} + for _, fn := range opts { + fn(&o) + } + + httpClient := o.httpClient + if httpClient == nil { + httpClient = &http.Client{Timeout: o.timeout} + } + + inner, err := NewClientWithResponses(baseURL, WithHTTPClient(httpClient)) + if err != nil { + return nil, err + } + return &SidecarClient{inner: inner, baseURL: baseURL, doer: httpClient}, nil +} + +// NewSidecarClientFromPodDNS builds a client targeting the sidecar via +// Kubernetes headless-service DNS: +// +// http://{name}-0.{name}.{namespace}.svc.cluster.local:{port} +func NewSidecarClientFromPodDNS(name, namespace string, port int32, opts ...Option) (*SidecarClient, error) { + if port == 0 { + port = DefaultPort + } + baseURL := fmt.Sprintf("http://%s-0.%s.%s.svc.cluster.local:%d", name, name, namespace, port) + return NewSidecarClient(baseURL, opts...) +} + +// Status queries the sidecar's current lifecycle state. +func (c *SidecarClient) Status(ctx context.Context) (*StatusResponse, error) { + resp, err := c.inner.GetStatusWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("querying sidecar status: %w", err) + } + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("sidecar status returned %d: %s", resp.StatusCode(), bytes.TrimSpace(resp.Body)) + } + if resp.JSON200 == nil { + return nil, fmt.Errorf("sidecar status returned 200 but empty body") + } + return resp.JSON200, nil +} + +// SubmitTask sends a TaskRequest to the sidecar. This is the generic +// submission path used internally by the typed Submit*Task methods and +// by the controller for dynamic dispatch. Prefer the typed methods for +// compile-time validation of task parameters. +func (c *SidecarClient) SubmitTask(ctx context.Context, task TaskRequest) (uuid.UUID, error) { + resp, err := c.inner.SubmitTaskWithResponse(ctx, task) + if err != nil { + return uuid.Nil, fmt.Errorf("submitting %s task to sidecar: %w", task.Type, err) + } + + switch resp.StatusCode() { + case http.StatusCreated: + if resp.JSON201 == nil { + return uuid.Nil, fmt.Errorf("sidecar returned 201 but no task ID in response body") + } + id := resp.JSON201.Id + if id == uuid.Nil { + return uuid.Nil, fmt.Errorf("sidecar returned 201 with nil task ID") + } + return id, nil + + case http.StatusAccepted: + if resp.JSON202 == nil { + return uuid.Nil, fmt.Errorf("sidecar returned 202 but no task ID in response body") + } + id := resp.JSON202.Id + if id == uuid.Nil { + return uuid.Nil, fmt.Errorf("sidecar returned 202 with nil task ID") + } + return id, nil + + case http.StatusBadRequest: + if resp.JSON400 != nil { + return uuid.Nil, fmt.Errorf("sidecar rejected %s task: %s", task.Type, resp.JSON400.Error) + } + return uuid.Nil, fmt.Errorf("sidecar rejected %s task: %s", task.Type, bytes.TrimSpace(resp.Body)) + + default: + return uuid.Nil, fmt.Errorf("sidecar %s task submission returned %d: %s", task.Type, resp.StatusCode(), bytes.TrimSpace(resp.Body)) + } +} + +// ListTasks returns recent task results. +func (c *SidecarClient) ListTasks(ctx context.Context) ([]TaskResult, error) { + resp, err := c.inner.ListTasksWithResponse(ctx) + if err != nil { + return nil, fmt.Errorf("listing sidecar tasks: %w", err) + } + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("sidecar list tasks returned %d: %s", resp.StatusCode(), bytes.TrimSpace(resp.Body)) + } + if resp.JSON200 == nil { + return []TaskResult{}, nil + } + return *resp.JSON200, nil +} + +// GetTask retrieves a single task result by ID. +func (c *SidecarClient) GetTask(ctx context.Context, id uuid.UUID) (*TaskResult, error) { + resp, err := c.inner.GetTaskWithResponse(ctx, id) + if err != nil { + return nil, fmt.Errorf("getting sidecar task %s: %w", id, err) + } + switch resp.StatusCode() { + case http.StatusOK: + if resp.JSON200 == nil { + return nil, fmt.Errorf("sidecar returned 200 for task %s but empty body", id) + } + return resp.JSON200, nil + case http.StatusNotFound: + return nil, ErrNotFound + default: + return nil, fmt.Errorf("sidecar get task returned %d: %s", resp.StatusCode(), bytes.TrimSpace(resp.Body)) + } +} + +// DeleteTask removes a task result or cancels a running task. +func (c *SidecarClient) DeleteTask(ctx context.Context, id uuid.UUID) error { + resp, err := c.inner.DeleteTaskWithResponse(ctx, id) + if err != nil { + return fmt.Errorf("deleting sidecar task %s: %w", id, err) + } + switch resp.StatusCode() { + case http.StatusNoContent: + return nil + case http.StatusNotFound: + return ErrNotFound + default: + return fmt.Errorf("sidecar delete task returned %d: %s", resp.StatusCode(), bytes.TrimSpace(resp.Body)) + } +} + +// Healthz checks whether the sidecar is healthy. +// Returns (true, nil) for 200, (false, nil) for 503, and (false, error) +// for network failures or unexpected status codes. +func (c *SidecarClient) Healthz(ctx context.Context) (bool, error) { + resp, err := c.inner.HealthzWithResponse(ctx) + if err != nil { + return false, fmt.Errorf("querying sidecar healthz: %w", err) + } + switch resp.StatusCode() { + case http.StatusOK: + return true, nil + case http.StatusServiceUnavailable: + return false, nil + default: + return false, fmt.Errorf("sidecar healthz returned %d: %s", resp.StatusCode(), bytes.TrimSpace(resp.Body)) + } +} + +// GetNodeID queries the sidecar's /v0/node-id endpoint and returns the +// Tendermint node ID (hex-encoded). This is a direct HTTP call rather than +// using the generated client, since /v0/node-id is outside the OpenAPI spec. +func (c *SidecarClient) GetNodeID(ctx context.Context) (string, error) { + url := c.baseURL + "/v0/node-id" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", fmt.Errorf("building node-id request: %w", err) + } + resp, err := c.doer.Do(req) + if err != nil { + return "", fmt.Errorf("querying node-id: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("reading node-id response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("node-id returned %d: %s", resp.StatusCode, bytes.TrimSpace(body)) + } + + var result struct { + NodeID string `json:"nodeId"` + } + if err := json.Unmarshal(body, &result); err != nil { + return "", fmt.Errorf("parsing node-id response: %w", err) + } + if result.NodeID == "" { + return "", fmt.Errorf("node-id response missing nodeId field") + } + return result.NodeID, nil +} + +// --------------------------------------------------------------------------- +// Typed submit methods -- primary public API for task submission. +// Each validates the typed struct and delegates to SubmitTask. +// --------------------------------------------------------------------------- + +func (c *SidecarClient) SubmitSnapshotRestoreTask(ctx context.Context, task SnapshotRestoreTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitSnapshotUploadTask(ctx context.Context, task SnapshotUploadTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitConfigureGenesisTask(ctx context.Context, task ConfigureGenesisTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitConfigPatchTask(ctx context.Context, task ConfigPatchTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitConfigApplyTask(ctx context.Context, task ConfigApplyTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitConfigValidateTask(ctx context.Context, task ConfigValidateTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitConfigReloadTask(ctx context.Context, task ConfigReloadTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitMarkReadyTask(ctx context.Context, task MarkReadyTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitRestartSeidTask(ctx context.Context, task RestartSeidTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitConfigureStateSyncTask(ctx context.Context, task ConfigureStateSyncTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitResultExportTask(ctx context.Context, task ResultExportTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitAwaitConditionTask(ctx context.Context, task AwaitConditionTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitGenerateIdentityTask(ctx context.Context, task GenerateIdentityTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitGenerateGentxTask(ctx context.Context, task GenerateGentxTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitUploadGenesisArtifactsTask(ctx context.Context, task UploadGenesisArtifactsTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitAssembleAndUploadGenesisTask(ctx context.Context, task AssembleAndUploadGenesisTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} + +func (c *SidecarClient) SubmitSetGenesisPeersTask(ctx context.Context, task SetGenesisPeersTask) (uuid.UUID, error) { + if err := task.Validate(); err != nil { + return uuid.Nil, fmt.Errorf("task validation failed: %w", err) + } + return c.SubmitTask(ctx, task.ToTaskRequest()) +} diff --git a/sidecarapi/client/client_test.go b/sidecarapi/client/client_test.go new file mode 100644 index 00000000..23f45061 --- /dev/null +++ b/sidecarapi/client/client_test.go @@ -0,0 +1,490 @@ +package client + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/uuid" +) + +func newTestClient(t *testing.T, handler http.Handler) *SidecarClient { + t.Helper() + srv := httptest.NewServer(handler) + t.Cleanup(srv.Close) + c, err := NewSidecarClient(srv.URL) + if err != nil { + t.Fatalf("NewSidecarClient: %v", err) + } + return c +} + +func TestStatus_OK(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v0/status" || r.Method != http.MethodGet { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(StatusResponse{Status: Ready}) + })) + + resp, err := c.Status(context.Background()) + if err != nil { + t.Fatalf("Status() error = %v", err) + } + if resp.Status != Ready { + t.Errorf("Status = %q, want %q", resp.Status, Ready) + } +} + +func TestStatus_ServerError(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "internal error", http.StatusInternalServerError) + })) + + _, err := c.Status(context.Background()) + if err == nil { + t.Fatal("expected error for 500 response") + } +} + +func TestSubmitTask_HTTP201(t *testing.T) { + taskID := uuid.New() + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v0/tasks" || r.Method != http.MethodPost { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + var req TaskRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode body: %v", err) + } + if req.Type != TaskTypeSnapshotRestore { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeSnapshotRestore) + } + if req.Params == nil { + t.Fatal("Params is nil") + } + params := *req.Params + if params["targetHeight"] != float64(100000000) { + t.Errorf("targetHeight = %v, want 100000000", params["targetHeight"]) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(TaskSubmitResponse{Id: taskID}) + })) + + task := SnapshotRestoreTask{ + TargetHeight: 100000000, + } + id, err := c.SubmitSnapshotRestoreTask(context.Background(), task) + if err != nil { + t.Fatalf("SubmitSnapshotRestoreTask() error = %v", err) + } + if id != taskID { + t.Errorf("returned id = %s, want %s", id, taskID) + } +} + +func TestSubmitTask_HTTP202(t *testing.T) { + taskID := uuid.New() + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(TaskSubmitResponse{Id: taskID}) + })) + + id, err := c.SubmitTask(context.Background(), TaskRequest{Type: TaskTypeMarkReady}) + if err != nil { + t.Fatalf("SubmitTask() error = %v", err) + } + if id != taskID { + t.Errorf("returned id = %s, want %s", id, taskID) + } +} + +func TestSubmitTask_HTTP202_MalformedBody(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write([]byte(`not json`)) + })) + + _, err := c.SubmitTask(context.Background(), TaskRequest{Type: TaskTypeMarkReady}) + if err == nil { + t.Fatal("expected error for malformed 202 body") + } +} + +func TestSubmitTask_HTTP202_NilUUID(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _ = json.NewEncoder(w).Encode(TaskSubmitResponse{Id: uuid.Nil}) + })) + + _, err := c.SubmitTask(context.Background(), TaskRequest{Type: TaskTypeMarkReady}) + if err == nil { + t.Fatal("expected error for nil UUID in 202 response") + } + if !strings.Contains(err.Error(), "nil task ID") { + t.Errorf("error = %v, expected to contain 'nil task ID'", err) + } +} + +func TestSubmitTask_HTTP201_NilUUID(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(TaskSubmitResponse{Id: uuid.Nil}) + })) + + _, err := c.SubmitTask(context.Background(), TaskRequest{Type: TaskTypeMarkReady}) + if err == nil { + t.Fatal("expected error for nil UUID in 201 response") + } + if !strings.Contains(err.Error(), "nil task ID") { + t.Errorf("error = %v, expected to contain 'nil task ID'", err) + } +} + +func TestSubmitTask_ServerError(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "internal error", http.StatusInternalServerError) + })) + + _, err := c.SubmitTask(context.Background(), TaskRequest{Type: TaskTypeMarkReady}) + if err == nil { + t.Fatal("expected error for 500 response") + } + if !strings.Contains(err.Error(), "500") { + t.Errorf("error = %v, expected to contain '500'", err) + } +} + +func TestSubmitTask_Conflict(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "conflict", http.StatusConflict) + })) + + _, err := c.SubmitTask(context.Background(), TaskRequest{Type: TaskTypeMarkReady}) + if err == nil { + t.Fatal("expected error for 409 response") + } + if !strings.Contains(err.Error(), "409") { + t.Errorf("error = %v, expected to contain '409'", err) + } +} + +func TestSubmitTask_BadRequest(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(ErrorResponse{Error: "unknown task type"}) + })) + + _, err := c.SubmitTask(context.Background(), TaskRequest{Type: "invalid"}) + if err == nil { + t.Fatal("expected error for 400 response") + } + if !strings.Contains(err.Error(), "unknown task type") { + t.Errorf("error = %v, expected to contain 'unknown task type'", err) + } + if !strings.Contains(err.Error(), "invalid") { + t.Errorf("error = %v, expected to contain the task type 'invalid'", err) + } +} + +func TestSubmitTask_ValidationFailure(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + t.Fatal("server should not be called when validation fails") + })) + + // ConfigPatchTask requires at least one file — empty should fail validation. + _, err := c.SubmitConfigPatchTask(context.Background(), ConfigPatchTask{}) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "validation failed") { + t.Errorf("error = %v, expected to contain 'validation failed'", err) + } +} + +func TestListTasks_OK(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v0/tasks" || r.Method != http.MethodGet { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode([]TaskResult{ + {Id: uuid.New(), Type: "mark-ready"}, + }) + })) + + results, err := c.ListTasks(context.Background()) + if err != nil { + t.Fatalf("ListTasks() error = %v", err) + } + if len(results) != 1 { + t.Fatalf("got %d results, want 1", len(results)) + } + if results[0].Type != "mark-ready" { + t.Errorf("Type = %q, want mark-ready", results[0].Type) + } +} + +func TestGetTask_OK(t *testing.T) { + taskID := uuid.New() + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("unexpected method: %s", r.Method) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(TaskResult{Id: taskID, Type: "config-patch"}) + })) + + result, err := c.GetTask(context.Background(), taskID) + if err != nil { + t.Fatalf("GetTask() error = %v", err) + } + if result.Type != "config-patch" { + t.Errorf("Type = %q, want config-patch", result.Type) + } +} + +func TestGetTask_NotFound(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _ = json.NewEncoder(w).Encode(ErrorResponse{Error: "not found"}) + })) + + _, err := c.GetTask(context.Background(), uuid.New()) + if !errors.Is(err, ErrNotFound) { + t.Errorf("error = %v, want ErrNotFound", err) + } +} + +func TestDeleteTask_OK(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("unexpected method: %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + })) + + if err := c.DeleteTask(context.Background(), uuid.New()); err != nil { + t.Fatalf("DeleteTask() error = %v", err) + } +} + +func TestDeleteTask_NotFound(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + _ = json.NewEncoder(w).Encode(ErrorResponse{Error: "not found"}) + })) + + err := c.DeleteTask(context.Background(), uuid.New()) + if !errors.Is(err, ErrNotFound) { + t.Errorf("error = %v, want ErrNotFound", err) + } +} + +func TestHealthz_OK(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v0/healthz" || r.Method != http.MethodGet { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.WriteHeader(http.StatusOK) + })) + + healthy, err := c.Healthz(context.Background()) + if err != nil { + t.Fatalf("Healthz() error = %v", err) + } + if !healthy { + t.Error("Healthz() = false, want true") + } +} + +func TestHealthz_ServiceUnavailable(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + })) + + healthy, err := c.Healthz(context.Background()) + if err != nil { + t.Fatalf("Healthz() error = %v", err) + } + if healthy { + t.Error("Healthz() = true, want false") + } +} + +func TestHealthz_UnexpectedStatus(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadGateway) + })) + + _, err := c.Healthz(context.Background()) + if err == nil { + t.Fatal("expected error for 502 response") + } +} + +func TestNewSidecarClientFromPodDNS_URLFormat(t *testing.T) { + c, err := NewSidecarClientFromPodDNS("sei-node", "default", 7777) + if err != nil { + t.Fatalf("NewSidecarClientFromPodDNS: %v", err) + } + inner := c.inner.ClientInterface.(*Client) + want := "http://sei-node-0.sei-node.default.svc.cluster.local:7777/" + if inner.Server != want { + t.Errorf("Server = %q, want %q", inner.Server, want) + } +} + +func TestNewSidecarClientFromPodDNS_DefaultPort(t *testing.T) { + c, err := NewSidecarClientFromPodDNS("mynode", "prod", 0) + if err != nil { + t.Fatalf("NewSidecarClientFromPodDNS: %v", err) + } + inner := c.inner.ClientInterface.(*Client) + want := "http://mynode-0.mynode.prod.svc.cluster.local:7777/" + if inner.Server != want { + t.Errorf("Server = %q, want %q", inner.Server, want) + } +} + +func TestSubmitAwaitConditionTask(t *testing.T) { + taskID := uuid.New() + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req TaskRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode body: %v", err) + } + if req.Type != TaskTypeAwaitCondition { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeAwaitCondition) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(TaskSubmitResponse{Id: taskID}) + })) + + id, err := c.SubmitAwaitConditionTask(context.Background(), AwaitConditionTask{ + Condition: ConditionHeight, + TargetHeight: 1000, + Action: ActionSIGTERM, + }) + if err != nil { + t.Fatalf("SubmitAwaitConditionTask() error = %v", err) + } + if id != taskID { + t.Errorf("returned id = %s, want %s", id, taskID) + } +} + +func TestSubmitAwaitConditionTask_ValidationError(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + t.Fatal("server should not be called when validation fails") + })) + + _, err := c.SubmitAwaitConditionTask(context.Background(), AwaitConditionTask{ + Condition: ConditionHeight, + TargetHeight: -1, + }) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "validation failed") { + t.Errorf("error = %v, expected to contain 'validation failed'", err) + } +} + +func TestListTasks_EmptyArray(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`[]`)) + })) + + results, err := c.ListTasks(context.Background()) + if err != nil { + t.Fatalf("ListTasks() error = %v", err) + } + if results == nil { + t.Fatal("ListTasks() returned nil, want empty slice") + } + if len(results) != 0 { + t.Errorf("got %d results, want 0", len(results)) + } +} + +func TestConfigPatchTask_Validate_Empty(t *testing.T) { + task := ConfigPatchTask{} + if err := task.Validate(); err == nil { + t.Fatal("expected validation error for empty ConfigPatchTask") + } +} + +func TestConfigPatchTask_Validate_OK(t *testing.T) { + task := ConfigPatchTask{ + Files: map[string]map[string]any{ + "config.toml": {"p2p": map[string]any{"seeds": "foo"}}, + }, + } + if err := task.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestGetNodeID_OK(t *testing.T) { + want := "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v0/node-id" || r.Method != http.MethodGet { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"nodeId": want}) + })) + + got, err := c.GetNodeID(context.Background()) + if err != nil { + t.Fatalf("GetNodeID() error = %v", err) + } + if got != want { + t.Errorf("GetNodeID() = %q, want %q", got, want) + } +} + +func TestGetNodeID_ServerError(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "not ready", http.StatusInternalServerError) + })) + + _, err := c.GetNodeID(context.Background()) + if err == nil { + t.Fatal("expected error for 500 response") + } + if !strings.Contains(err.Error(), "500") { + t.Errorf("error = %v, expected to contain '500'", err) + } +} + +func TestGetNodeID_EmptyNodeID(t *testing.T) { + c := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"nodeId": ""}) + })) + + _, err := c.GetNodeID(context.Background()) + if err == nil { + t.Fatal("expected error for empty nodeId") + } + if !strings.Contains(err.Error(), "missing nodeId") { + t.Errorf("error = %v, expected to contain 'missing nodeId'", err) + } +} diff --git a/sidecarapi/client/gov_param_change_test.go b/sidecarapi/client/gov_param_change_test.go new file mode 100644 index 00000000..4074ed6f --- /dev/null +++ b/sidecarapi/client/gov_param_change_test.go @@ -0,0 +1,110 @@ +package client + +import ( + "encoding/json" + "testing" +) + +func validGovParamChangeTask() GovParamChangeTask { + return GovParamChangeTask{ + ChainID: "arctic-1", + KeyName: "node_admin", + Title: "Update Consensus Timeout Params", + Description: "Tighten timeouts.", + Changes: []ParamChangeInput{ + {Subspace: "baseapp", Key: "TimeoutParams", Value: json.RawMessage(`{"propose":"300000000"}`)}, + }, + InitialDeposit: "10000000usei", + Fees: "8000usei", + Gas: 300000, + } +} + +func TestGovParamChangeTask_Validate(t *testing.T) { + if err := validGovParamChangeTask().Validate(); err != nil { + t.Fatalf("valid task: unexpected error: %v", err) + } + + cases := []struct { + name string + mut func(*GovParamChangeTask) + }{ + {"missing chainId", func(tk *GovParamChangeTask) { tk.ChainID = "" }}, + {"missing keyName", func(tk *GovParamChangeTask) { tk.KeyName = "" }}, + {"missing title", func(tk *GovParamChangeTask) { tk.Title = "" }}, + {"missing description", func(tk *GovParamChangeTask) { tk.Description = "" }}, + {"empty changes", func(tk *GovParamChangeTask) { tk.Changes = nil }}, + {"empty subspace", func(tk *GovParamChangeTask) { tk.Changes[0].Subspace = "" }}, + {"empty key", func(tk *GovParamChangeTask) { tk.Changes[0].Key = "" }}, + {"empty value", func(tk *GovParamChangeTask) { tk.Changes[0].Value = nil }}, + {"missing initialDeposit", func(tk *GovParamChangeTask) { tk.InitialDeposit = "" }}, + {"missing fees", func(tk *GovParamChangeTask) { tk.Fees = "" }}, + {"zero gas", func(tk *GovParamChangeTask) { tk.Gas = 0 }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tk := validGovParamChangeTask() + tc.mut(&tk) + if err := tk.Validate(); err == nil { + t.Errorf("expected validation error for %q", tc.name) + } + }) + } +} + +func TestGovParamChangeTask_ToTaskRequest(t *testing.T) { + tk := validGovParamChangeTask() + req := tk.ToTaskRequest() + if req.Type != TaskTypeGovParamChange { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeGovParamChange) + } + if req.Params == nil { + t.Fatal("Params is nil") + } + p := *req.Params + for _, k := range []string{"chainId", "keyName", "title", "description", "changes", "initialDeposit", "fees", "gas"} { + if _, ok := p[k]; !ok { + t.Errorf("params missing key %q", k) + } + } + // memo omitted when empty (matches the sibling tasks). + if _, ok := p["memo"]; ok { + t.Errorf("memo present but was empty") + } +} + +// The per-change value must survive the client encode (ToTaskRequest → the HTTP +// layer's json.Marshal of Params) byte-identical, for any JSON shape — never +// re-escaped. This is the client-side half of the single-encode contract that +// the prop-252 double-encode bug violated. +func TestGovParamChangeTask_ValueSingleEncodedOnWire(t *testing.T) { + for _, raw := range []string{ + `{"propose":"300000000","commit":"200000000"}`, // object + `"86400000000000"`, // scalar string + `100`, // scalar number + `true`, // scalar bool + } { + t.Run(raw, func(t *testing.T) { + tk := validGovParamChangeTask() + tk.Changes = []ParamChangeInput{{Subspace: "baseapp", Key: "K", Value: json.RawMessage(raw)}} + req := tk.ToTaskRequest() + + // Marshal Params exactly as the HTTP client would, then re-extract. + b, err := json.Marshal(req.Params) + if err != nil { + t.Fatalf("marshal params: %v", err) + } + var decoded struct { + Changes []struct { + Value json.RawMessage `json:"value"` + } `json:"changes"` + } + if err := json.Unmarshal(b, &decoded); err != nil { + t.Fatalf("unmarshal params: %v", err) + } + if got := string(decoded.Changes[0].Value); got != raw { + t.Errorf("wire value = %q, want %q (double-encoded?)", got, raw) + } + }) + } +} diff --git a/sidecarapi/client/sidecar.gen.go b/sidecarapi/client/sidecar.gen.go new file mode 100644 index 00000000..ecd25db0 --- /dev/null +++ b/sidecarapi/client/sidecar.gen.go @@ -0,0 +1,903 @@ +// Package client provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/oapi-codegen/runtime" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +const ( + RemoteUserHeaderScopes = "remoteUserHeader.Scopes" +) + +// Defines values for StatusResponseStatus. +const ( + Initializing StatusResponseStatus = "Initializing" + Ready StatusResponseStatus = "Ready" +) + +// Defines values for TaskResultStatus. +const ( + Completed TaskResultStatus = "completed" + Failed TaskResultStatus = "failed" + Running TaskResultStatus = "running" +) + +// ErrorResponse defines model for ErrorResponse. +type ErrorResponse struct { + Error string `json:"error"` +} + +// StatusResponse defines model for StatusResponse. +type StatusResponse struct { + Status StatusResponseStatus `json:"status"` +} + +// StatusResponseStatus defines model for StatusResponse.Status. +type StatusResponseStatus string + +// TaskRequest defines model for TaskRequest. +type TaskRequest struct { + // Id Caller-provided task identifier. When set, the engine uses + // this as the canonical ID (enabling deterministic IDs from + // the controller); if a task with this ID exists, the request + // is idempotent and returns the existing ID. When omitted, a + // random UUID is generated. + Id *openapi_types.UUID `json:"id,omitempty"` + + // Params Task-type-specific parameters; validated server-side. + Params *map[string]interface{} `json:"params,omitempty"` + + // Type Task type identifier. + Type string `json:"type"` +} + +// TaskResult defines model for TaskResult. +type TaskResult struct { + CompletedAt *time.Time `json:"completedAt,omitempty"` + + // Error Error message if the task failed. + Error *string `json:"error,omitempty"` + Id openapi_types.UUID `json:"id"` + Params *map[string]interface{} `json:"params,omitempty"` + + // Result Handler's structured result, present on any task that emits one — + // on both success and failure (e.g. assemble-and-upload-genesis + // returns {"genesisHash":""} on success; a gov submit stamps + // txHash/inclusionStatus even when the task fails). Delivered over this + // trusted channel rather than via shared storage. + Result *json.RawMessage `json:"result,omitempty"` + + // Status Current task lifecycle state. + Status TaskResultStatus `json:"status"` + SubmittedAt time.Time `json:"submittedAt"` + + // Type Task type that was executed. + Type string `json:"type"` +} + +// TaskResultStatus Current task lifecycle state. +type TaskResultStatus string + +// TaskSubmitResponse defines model for TaskSubmitResponse. +type TaskSubmitResponse struct { + // Id The assigned task UUID. + Id openapi_types.UUID `json:"id"` +} + +// SubmitTaskJSONRequestBody defines body for SubmitTask for application/json ContentType. +type SubmitTaskJSONRequestBody = TaskRequest + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // Healthz request + Healthz(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetStatus request + GetStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ListTasks request + ListTasks(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // SubmitTaskWithBody request with any body + SubmitTaskWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + SubmitTask(ctx context.Context, body SubmitTaskJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteTask request + DeleteTask(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetTask request + GetTask(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) Healthz(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewHealthzRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetStatusRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ListTasks(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListTasksRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) SubmitTaskWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSubmitTaskRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) SubmitTask(ctx context.Context, body SubmitTaskJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSubmitTaskRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteTask(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteTaskRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetTask(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTaskRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewHealthzRequest generates requests for Healthz +func NewHealthzRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/healthz") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetStatusRequest generates requests for GetStatus +func NewGetStatusRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/status") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewListTasksRequest generates requests for ListTasks +func NewListTasksRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/tasks") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewSubmitTaskRequest calls the generic SubmitTask builder with application/json body +func NewSubmitTaskRequest(server string, body SubmitTaskJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewSubmitTaskRequestWithBody(server, "application/json", bodyReader) +} + +// NewSubmitTaskRequestWithBody generates requests for SubmitTask with any type of body +func NewSubmitTaskRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/tasks") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeleteTaskRequest generates requests for DeleteTask +func NewDeleteTaskRequest(server string, id openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/tasks/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetTaskRequest generates requests for GetTask +func NewGetTaskRequest(server string, id openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v0/tasks/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // HealthzWithResponse request + HealthzWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*HealthzResponse, error) + + // GetStatusWithResponse request + GetStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetStatusResponse, error) + + // ListTasksWithResponse request + ListTasksWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListTasksResponse, error) + + // SubmitTaskWithBodyWithResponse request with any body + SubmitTaskWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitTaskResponse, error) + + SubmitTaskWithResponse(ctx context.Context, body SubmitTaskJSONRequestBody, reqEditors ...RequestEditorFn) (*SubmitTaskResponse, error) + + // DeleteTaskWithResponse request + DeleteTaskWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteTaskResponse, error) + + // GetTaskWithResponse request + GetTaskWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetTaskResponse, error) +} + +type HealthzResponse struct { + Body []byte + HTTPResponse *http.Response +} + +// Status returns HTTPResponse.Status +func (r HealthzResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r HealthzResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetStatusResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *StatusResponse +} + +// Status returns HTTPResponse.Status +func (r GetStatusResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetStatusResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ListTasksResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]TaskResult +} + +// Status returns HTTPResponse.Status +func (r ListTasksResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListTasksResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type SubmitTaskResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *TaskSubmitResponse + JSON202 *TaskSubmitResponse + JSON400 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r SubmitTaskResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SubmitTaskResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteTaskResponse struct { + Body []byte + HTTPResponse *http.Response + JSON404 *ErrorResponse + JSON503 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r DeleteTaskResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteTaskResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetTaskResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *TaskResult + JSON404 *ErrorResponse +} + +// Status returns HTTPResponse.Status +func (r GetTaskResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetTaskResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// HealthzWithResponse request returning *HealthzResponse +func (c *ClientWithResponses) HealthzWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*HealthzResponse, error) { + rsp, err := c.Healthz(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseHealthzResponse(rsp) +} + +// GetStatusWithResponse request returning *GetStatusResponse +func (c *ClientWithResponses) GetStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetStatusResponse, error) { + rsp, err := c.GetStatus(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetStatusResponse(rsp) +} + +// ListTasksWithResponse request returning *ListTasksResponse +func (c *ClientWithResponses) ListTasksWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListTasksResponse, error) { + rsp, err := c.ListTasks(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListTasksResponse(rsp) +} + +// SubmitTaskWithBodyWithResponse request with arbitrary body returning *SubmitTaskResponse +func (c *ClientWithResponses) SubmitTaskWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitTaskResponse, error) { + rsp, err := c.SubmitTaskWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSubmitTaskResponse(rsp) +} + +func (c *ClientWithResponses) SubmitTaskWithResponse(ctx context.Context, body SubmitTaskJSONRequestBody, reqEditors ...RequestEditorFn) (*SubmitTaskResponse, error) { + rsp, err := c.SubmitTask(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSubmitTaskResponse(rsp) +} + +// DeleteTaskWithResponse request returning *DeleteTaskResponse +func (c *ClientWithResponses) DeleteTaskWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*DeleteTaskResponse, error) { + rsp, err := c.DeleteTask(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteTaskResponse(rsp) +} + +// GetTaskWithResponse request returning *GetTaskResponse +func (c *ClientWithResponses) GetTaskWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetTaskResponse, error) { + rsp, err := c.GetTask(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetTaskResponse(rsp) +} + +// ParseHealthzResponse parses an HTTP response from a HealthzWithResponse call +func ParseHealthzResponse(rsp *http.Response) (*HealthzResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &HealthzResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetStatusResponse parses an HTTP response from a GetStatusWithResponse call +func ParseGetStatusResponse(rsp *http.Response) (*GetStatusResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetStatusResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest StatusResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseListTasksResponse parses an HTTP response from a ListTasksWithResponse call +func ParseListTasksResponse(rsp *http.Response) (*ListTasksResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListTasksResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []TaskResult + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseSubmitTaskResponse parses an HTTP response from a SubmitTaskWithResponse call +func ParseSubmitTaskResponse(rsp *http.Response) (*SubmitTaskResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SubmitTaskResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest TaskSubmitResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest TaskSubmitResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + } + + return response, nil +} + +// ParseDeleteTaskResponse parses an HTTP response from a DeleteTaskWithResponse call +func ParseDeleteTaskResponse(rsp *http.Response) (*DeleteTaskResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteTaskResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + + } + + return response, nil +} + +// ParseGetTaskResponse parses an HTTP response from a GetTaskWithResponse call +func ParseGetTaskResponse(rsp *http.Response) (*GetTaskResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetTaskResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest TaskResult + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest ErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} diff --git a/sidecarapi/client/tasks.go b/sidecarapi/client/tasks.go new file mode 100644 index 00000000..aec4afce --- /dev/null +++ b/sidecarapi/client/tasks.go @@ -0,0 +1,929 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/cosmos/btcutil/bech32" + seiconfig "github.com/sei-protocol/sei-config" + "github.com/sei-protocol/sei-k8s-controller/sidecarapi/wire" +) + +const seiBech32HRP = "sei" + +// btcutil's bech32 decode validates checksum + alphabet; cheaper than +// pulling sei-cosmos's sdk.AccAddressFromBech32 just for client-side +// validation. Sidecar still does the SDK-shape check server-side. +func validateSeiAccountAddress(addr string) error { + hrp, _, err := bech32.Decode(addr, 1023) // bech32 spec max length + if err != nil { + return fmt.Errorf("address %q: %w", addr, err) + } + if hrp != seiBech32HRP { + return fmt.Errorf("address %q: hrp %q, expected %q", addr, hrp, seiBech32HRP) + } + return nil +} + +// TaskBuilder is implemented by every typed task struct. It converts a +// strongly-typed task description into the generic TaskRequest wire format +// and validates required fields before submission. +type TaskBuilder interface { + TaskType() string + Validate() error + ToTaskRequest() TaskRequest +} + +// Task type constants re-exported from wire for external consumers. +const ( + TaskTypeSnapshotRestore = string(wire.TaskSnapshotRestore) + TaskTypeConfigPatch = string(wire.TaskConfigPatch) + TaskTypeConfigApply = string(wire.TaskConfigApply) + TaskTypeConfigValidate = string(wire.TaskConfigValidate) + TaskTypeConfigReload = string(wire.TaskConfigReload) + TaskTypeMarkReady = string(wire.TaskMarkReady) + TaskTypeRestartSeid = string(wire.TaskRestartSeid) + TaskTypeConfigureGenesis = string(wire.TaskConfigureGenesis) + TaskTypeConfigureStateSync = string(wire.TaskConfigureStateSync) + TaskTypeSnapshotUpload = string(wire.TaskSnapshotUpload) + TaskTypeSnapshotUploadOnce = string(wire.TaskSnapshotUploadOnce) + TaskTypeResultExport = string(wire.TaskResultExport) + TaskTypeAwaitCondition = string(wire.TaskAwaitCondition) + + TaskTypeGenerateIdentity = string(wire.TaskGenerateIdentity) + TaskTypeGenerateGentx = string(wire.TaskGenerateGentx) + TaskTypeUploadGenesisArtifacts = string(wire.TaskUploadGenesisArtifacts) + TaskTypeAssembleGenesis = string(wire.TaskAssembleAndUploadGenesis) + TaskTypeSetGenesisPeers = string(wire.TaskSetGenesisPeers) + + TaskTypeGovVote = string(wire.TaskGovVote) + TaskTypeGovSoftwareUpgrade = string(wire.TaskGovSoftwareUpgrade) + TaskTypeGovParamChange = string(wire.TaskGovParamChange) + + TaskTypeMarkNotReady = string(wire.TaskMarkNotReady) + TaskTypeStopSeid = string(wire.TaskStopSeid) + TaskTypeResetData = string(wire.TaskResetData) +) + +// Snapshot-upload outcome contract, re-exported from wire so CLI consumers +// classify a result against the same definition the handler emits. +type ( + UploadOutcome = wire.UploadOutcome + NoopReason = wire.NoopReason +) + +const ( + OutcomeUploaded = wire.OutcomeUploaded + OutcomeNoop = wire.OutcomeNoop + OutcomeError = wire.OutcomeError + + NoopFewerThanTwoSnapshots = wire.NoopFewerThanTwoSnapshots + NoopAlreadyUploaded = wire.NoopAlreadyUploaded +) + +// Known condition and action values for AwaitConditionTask. +const ( + ConditionHeight = "height" + ConditionCatchingUp = "catchingUp" + ActionSIGTERM = "SIGTERM_SEID" +) + +// SnapshotRestoreTask downloads and extracts a snapshot archive from S3. +// S3 coordinates are derived by the sidecar from its environment. +// TargetHeight selects the highest available snapshot <= that height. +// When zero, the latest snapshot (from latest.txt) is used. +type SnapshotRestoreTask struct { + TargetHeight int64 +} + +func (t SnapshotRestoreTask) TaskType() string { return TaskTypeSnapshotRestore } + +func (t SnapshotRestoreTask) Validate() error { return nil } + +func (t SnapshotRestoreTask) ToTaskRequest() TaskRequest { + var p *map[string]interface{} + if t.TargetHeight > 0 { + m := map[string]interface{}{"targetHeight": t.TargetHeight} + p = &m + } + req := TaskRequest{Type: t.TaskType(), Params: p} + return req +} + +// SnapshotUploadTask archives and streams a local snapshot to S3. +// S3 coordinates are derived by the sidecar from its environment. +type SnapshotUploadTask struct { +} + +func (t SnapshotUploadTask) TaskType() string { return TaskTypeSnapshotUpload } + +func (t SnapshotUploadTask) Validate() error { return nil } + +func (t SnapshotUploadTask) ToTaskRequest() TaskRequest { + req := TaskRequest{Type: t.TaskType()} + return req +} + +// SnapshotUploadOnceTask runs a single snapshot upload and reaches a real +// terminal (completed with an outcome, or failed) rather than looping. A poller +// reads the structured result to distinguish uploaded / noop / error. S3 +// coordinates and the per-task deadline are derived by the sidecar from its +// environment. +// +// Each invocation MUST be submitted with a fresh, unique task ID. The engine +// coalesces a resubmit onto an existing Completed row without re-running, so a +// reused ID reads back the prior run's stale result and never uploads. +type SnapshotUploadOnceTask struct{} + +func (t SnapshotUploadOnceTask) TaskType() string { return TaskTypeSnapshotUploadOnce } + +func (t SnapshotUploadOnceTask) Validate() error { return nil } + +func (t SnapshotUploadOnceTask) ToTaskRequest() TaskRequest { + return TaskRequest{Type: t.TaskType()} +} + +// ConfigureGenesisTask instructs the sidecar to resolve and write genesis.json. +// The sidecar resolves genesis from its chain ID: embedded config is checked +// first, then S3 fallback at {bucket}/{chainID}/genesis.json using env vars. +// +// ExpectedGenesisHash is the bare SHA-256 hex digest (no "sha256:" prefix) the +// downloaded genesis.json must match. When set it gates the S3 download and the +// sidecar fails closed on mismatch. When empty the download is unverified — +// callers that omit it (and the field is omitted from the wire request) keep +// the sidecar's pre-verification behavior. +type ConfigureGenesisTask struct { + ExpectedGenesisHash string +} + +func (t ConfigureGenesisTask) TaskType() string { return TaskTypeConfigureGenesis } + +func (t ConfigureGenesisTask) Validate() error { return nil } + +func (t ConfigureGenesisTask) ToTaskRequest() TaskRequest { + if t.ExpectedGenesisHash == "" { + return TaskRequest{Type: t.TaskType()} + } + p := map[string]interface{}{"expectedGenesisHash": t.ExpectedGenesisHash} + return TaskRequest{Type: t.TaskType(), Params: &p} +} + +// ConfigPatchTask applies generic TOML merge-patches to seid config files. +// Files maps a filename (e.g. "config.toml", "app.toml") to a nested patch +// that will be recursively merged into the existing file. +type ConfigPatchTask struct { + Files map[string]map[string]any +} + +func (t ConfigPatchTask) TaskType() string { return TaskTypeConfigPatch } + +func (t ConfigPatchTask) Validate() error { + if len(t.Files) == 0 { + return fmt.Errorf("config-patch: at least one file is required") + } + return nil +} + +func (t ConfigPatchTask) ToTaskRequest() TaskRequest { + files := make(map[string]interface{}, len(t.Files)) + for k, v := range t.Files { + files[k] = v + } + p := map[string]interface{}{"files": files} + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// ConfigureStateSyncTask discovers a trust point and configures state sync. +// When UseLocalSnapshot is true, the task uses the locally-restored snapshot +// height as the trust height and sets use-local-snapshot = true in config.toml. +type ConfigureStateSyncTask struct { + UseLocalSnapshot bool + TrustPeriod string + BackfillBlocks int64 + RpcServers []string +} + +func (t ConfigureStateSyncTask) TaskType() string { return TaskTypeConfigureStateSync } +func (t ConfigureStateSyncTask) Validate() error { return nil } + +func (t ConfigureStateSyncTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{} + if t.UseLocalSnapshot { + p["useLocalSnapshot"] = true + } + if t.TrustPeriod != "" { + p["trustPeriod"] = t.TrustPeriod + } + if t.BackfillBlocks > 0 { + p["backfillBlocks"] = t.BackfillBlocks + } + if len(t.RpcServers) > 0 { + p["rpcServers"] = t.RpcServers + } + var req TaskRequest + if len(p) == 0 { + req = TaskRequest{Type: t.TaskType()} + } else { + req = TaskRequest{Type: t.TaskType(), Params: &p} + } + return req +} + +// MarkReadyTask signals that bootstrap is complete. +type MarkReadyTask struct{} + +func (t MarkReadyTask) TaskType() string { return TaskTypeMarkReady } +func (t MarkReadyTask) Validate() error { return nil } + +func (t MarkReadyTask) ToTaskRequest() TaskRequest { + req := TaskRequest{Type: t.TaskType()} + return req +} + +// RestartSeidTask restarts the co-located seid process in place so it +// re-reads config.toml without bouncing the sidecar. The task completes +// once seid's local RPC is serving again. +type RestartSeidTask struct{} + +func (t RestartSeidTask) TaskType() string { return TaskTypeRestartSeid } +func (t RestartSeidTask) Validate() error { return nil } + +func (t RestartSeidTask) ToTaskRequest() TaskRequest { + req := TaskRequest{Type: t.TaskType()} + return req +} + +// MarkNotReadyTask re-arms the seid start gate for a node hold: it purges +// recorded mark-ready results and flips the sidecar readiness flag false, so +// the next container restart parks seid at the gate (healthz 503). +// +// Consumers MUST poll this task to a terminal state (never fire-and-forget): +// the purge it performs is a precondition for the destructive steps that follow +// in the hold recipe, so releasing the next step before it completes is unsafe. +// This contrasts with mark-ready, which is fire-and-forget by design. +type MarkNotReadyTask struct{} + +func (t MarkNotReadyTask) TaskType() string { return TaskTypeMarkNotReady } +func (t MarkNotReadyTask) Validate() error { return nil } + +func (t MarkNotReadyTask) ToTaskRequest() TaskRequest { + return TaskRequest{Type: t.TaskType()} +} + +// StopSeidTask SIGTERMs the co-located seid process and confirms it exited, +// without waiting for it to come back up. Paired with a prior mark-not-ready, +// the restarted container blocks at the gate instead of booting. +type StopSeidTask struct{} + +func (t StopSeidTask) TaskType() string { return TaskTypeStopSeid } +func (t StopSeidTask) Validate() error { return nil } + +func (t StopSeidTask) ToTaskRequest() TaskRequest { + return TaskRequest{Type: t.TaskType()} +} + +// ResetDataTask clears the chain data directory (data/ only), rewrites an empty +// priv_validator_state, and removes the state-sync completion marker so the +// node re-bootstraps through state sync. Refuses to run while seid's RPC serves. +type ResetDataTask struct{} + +func (t ResetDataTask) TaskType() string { return TaskTypeResetData } +func (t ResetDataTask) Validate() error { return nil } + +func (t ResetDataTask) ToTaskRequest() TaskRequest { + return TaskRequest{Type: t.TaskType()} +} + +// SetGenesisPeersTask requests the sidecar to publish this node's peer +// entry to the shared genesis peers list (S3 coordinates derived from +// the sidecar environment). +type SetGenesisPeersTask struct{} + +func (t SetGenesisPeersTask) TaskType() string { return TaskTypeSetGenesisPeers } +func (t SetGenesisPeersTask) Validate() error { return nil } + +func (t SetGenesisPeersTask) ToTaskRequest() TaskRequest { + req := TaskRequest{Type: t.TaskType()} + return req +} + +// ConfigApplyTask generates or patches node config using sei-config's +// intent resolution pipeline. The caller builds a ConfigIntent describing +// the desired state; the sidecar resolves it via sei-config. +type ConfigApplyTask struct { + Intent seiconfig.ConfigIntent +} + +func (t ConfigApplyTask) TaskType() string { return TaskTypeConfigApply } + +func (t ConfigApplyTask) Validate() error { + result := seiconfig.ValidateIntent(t.Intent) + if !result.Valid { + return fmt.Errorf("config-apply: invalid intent: %v", result.Diagnostics) + } + return nil +} + +func (t ConfigApplyTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "mode": string(t.Intent.Mode), + "incremental": t.Intent.Incremental, + "targetVersion": t.Intent.TargetVersion, + } + if len(t.Intent.Overrides) > 0 { + overrides := make(map[string]interface{}, len(t.Intent.Overrides)) + for k, v := range t.Intent.Overrides { + overrides[k] = v + } + p["overrides"] = overrides + } + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// ConfigValidateTask reads on-disk config and returns validation diagnostics. +type ConfigValidateTask struct{} + +func (t ConfigValidateTask) TaskType() string { return TaskTypeConfigValidate } +func (t ConfigValidateTask) Validate() error { return nil } + +func (t ConfigValidateTask) ToTaskRequest() TaskRequest { + req := TaskRequest{Type: t.TaskType()} + return req +} + +// ConfigReloadTask patches hot-reloadable fields on disk and signals seid +// to re-read its configuration. +type ConfigReloadTask struct { + Fields map[string]string +} + +func (t ConfigReloadTask) TaskType() string { return TaskTypeConfigReload } + +func (t ConfigReloadTask) Validate() error { + if len(t.Fields) == 0 { + return fmt.Errorf("config-reload: at least one field is required") + } + return nil +} + +func (t ConfigReloadTask) ToTaskRequest() TaskRequest { + fields := make(map[string]interface{}, len(t.Fields)) + for k, v := range t.Fields { + fields[k] = v + } + p := map[string]interface{}{"fields": fields} + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// ResultExportTask queries the local seid RPC for block results and uploads +// them in paginated NDJSON files to S3. Setting CanonicalRPC enables comparison +// mode — the sidecar compares local block results against the canonical chain — +// and the remaining fields tune that comparison. By default the task completes +// on the first divergence; ContinueOnDivergence surveys past divergences and +// runs until stopped. +type ResultExportTask struct { + Bucket string + Prefix string + Region string + CanonicalRPC string + + // Comparison-mode tuning — all require CanonicalRPC. MigrationMode keys the + // verdict on execution results for an AppHash-breaking migration shadow; + // ContinueOnDivergence surveys past divergences instead of halting on the + // first; ShadowEVMRPC + CanonicalEVMRPC enable Layer 2 (logical state) diff, + // with TraceRPC sourcing each block's touched keys. + MigrationMode bool + ContinueOnDivergence bool + ShadowEVMRPC string + CanonicalEVMRPC string + TraceRPC string +} + +func (t ResultExportTask) TaskType() string { return TaskTypeResultExport } + +func (t ResultExportTask) Validate() error { + if t.Bucket == "" { + return fmt.Errorf("result-export: missing required field Bucket") + } + if t.Region == "" { + return fmt.Errorf("result-export: missing required field Region") + } + // The comparison-tuning fields are silently inert without CanonicalRPC (the + // plain export path never reads them), so reject that misconfiguration here + // rather than let it pass as a no-op. + if t.CanonicalRPC == "" && + (t.MigrationMode || t.ContinueOnDivergence || + t.ShadowEVMRPC != "" || t.CanonicalEVMRPC != "" || t.TraceRPC != "") { + return fmt.Errorf("result-export: comparison-mode fields require CanonicalRPC") + } + return nil +} + +func (t ResultExportTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "bucket": t.Bucket, + "region": t.Region, + } + if t.Prefix != "" { + p["prefix"] = t.Prefix + } + if t.CanonicalRPC != "" { + p["canonicalRpc"] = t.CanonicalRPC + } + if t.MigrationMode { + p["migrationMode"] = true + } + if t.ContinueOnDivergence { + p["continueOnDivergence"] = true + } + if t.ShadowEVMRPC != "" { + p["shadowEvmRpc"] = t.ShadowEVMRPC + } + if t.CanonicalEVMRPC != "" { + p["canonicalEvmRpc"] = t.CanonicalEVMRPC + } + if t.TraceRPC != "" { + p["traceRpc"] = t.TraceRPC + } + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// GenerateIdentityTask creates validator identity (keys, node ID). +type GenerateIdentityTask struct { + ChainID string + Moniker string +} + +func (t GenerateIdentityTask) TaskType() string { return TaskTypeGenerateIdentity } + +func (t GenerateIdentityTask) Validate() error { + if t.ChainID == "" { + return fmt.Errorf("generate-identity: missing required field ChainID") + } + if t.Moniker == "" { + return fmt.Errorf("generate-identity: missing required field Moniker") + } + return nil +} + +func (t GenerateIdentityTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "chainId": t.ChainID, + "moniker": t.Moniker, + } + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// GenerateGentxTask creates a gentx for the validator. The handler discovers +// the node's own account address from the keys generated during identity +// creation and funds it with AccountBalance before generating the gentx. +type GenerateGentxTask struct { + ChainID string + StakingAmount string + AccountBalance string +} + +func (t GenerateGentxTask) TaskType() string { return TaskTypeGenerateGentx } + +func (t GenerateGentxTask) Validate() error { + if t.ChainID == "" { + return fmt.Errorf("generate-gentx: missing required field ChainID") + } + if t.StakingAmount == "" { + return fmt.Errorf("generate-gentx: missing required field StakingAmount") + } + if t.AccountBalance == "" { + return fmt.Errorf("generate-gentx: missing required field AccountBalance") + } + return nil +} + +func (t GenerateGentxTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "chainId": t.ChainID, + "stakingAmount": t.StakingAmount, + "accountBalance": t.AccountBalance, + } + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// UploadGenesisArtifactsTask uploads identity.json and gentx.json to S3. +// S3 coordinates are derived by the sidecar from its environment. +type UploadGenesisArtifactsTask struct { + NodeName string +} + +func (t UploadGenesisArtifactsTask) TaskType() string { return TaskTypeUploadGenesisArtifacts } + +func (t UploadGenesisArtifactsTask) Validate() error { + if t.NodeName == "" { + return fmt.Errorf("upload-genesis-artifacts: missing required field NodeName") + } + return nil +} + +func (t UploadGenesisArtifactsTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "nodeName": t.NodeName, + } + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// GenesisNodeParam is the wire format for nodes[] in assemble-and-upload-genesis. +type GenesisNodeParam struct { + Name string `json:"name"` +} + +// GenesisAccountEntry and GenesisAccountVesting are the wire contract, aliased +// here so callers keep writing client.GenesisAccountEntry. The sidecar's +// handler side (sidecar/tasks) aliases the same definitions, so the request +// this package builds and the payload the server unmarshals cannot drift. +type ( + GenesisAccountEntry = wire.GenesisAccountEntry + GenesisAccountVesting = wire.GenesisAccountVesting +) + +func genesisAccountsToWire(accounts []GenesisAccountEntry) []interface{} { + if len(accounts) == 0 { + return nil + } + out := make([]interface{}, len(accounts)) + for i, a := range accounts { + entry := map[string]interface{}{"address": a.Address, "balance": a.Balance} + if a.Vesting != nil { + entry["vesting"] = map[string]interface{}{ + "amount": a.Vesting.Amount, + "endTime": a.Vesting.EndTime, + "delayed": a.Vesting.Delayed, + } + } + out[i] = entry + } + return out +} + +// Balance shape is validated server-side via sdk.ParseCoinsNormalized. +func validateGenesisAccounts(prefix string, accounts []GenesisAccountEntry) error { + for i, a := range accounts { + if a.Address == "" { + return fmt.Errorf("%s: accounts[%d] missing required field Address", prefix, i) + } + if a.Balance == "" { + return fmt.Errorf("%s: accounts[%d] missing required field Balance", prefix, i) + } + if err := validateSeiAccountAddress(a.Address); err != nil { + return fmt.Errorf("%s: accounts[%d]: %w", prefix, i, err) + } + if a.Vesting != nil { + if a.Vesting.Amount == "" { + return fmt.Errorf("%s: accounts[%d].vesting missing required field Amount", prefix, i) + } + if a.Vesting.EndTime <= 0 { + return fmt.Errorf("%s: accounts[%d].vesting: EndTime must be a positive unix timestamp", prefix, i) + } + } + } + return nil +} + +// AssembleAndUploadGenesisTask collects per-node artifacts and produces final genesis.json. +// S3 coordinates are derived by the sidecar from its environment. +// +// Overrides is a flat map of dotted-path keys into genesis.app_state to raw +// JSON values, applied to the assembled genesis after collect-gentxs runs. +// The controller validates keys (immutability post-bootstrap) via CEL; the +// sidecar applies them verbatim and fails loudly on bad paths. +// +// RESULT: this task produces the assembled genesis.json's bare SHA-256 hex +// digest (the value the controller writes to status.genesisHash and plumbs +// into followers' ConfigureGenesisTask.ExpectedGenesisHash). The digest is +// returned in-band on the task result as {"genesisHash":""}, which +// the controller reads over the trusted GET /v0/tasks/{id} channel. It is +// never written to S3, where the prefix is attacker-writable. +type AssembleAndUploadGenesisTask struct { + AccountBalance string + Namespace string + Nodes []GenesisNodeParam + Accounts []GenesisAccountEntry + Overrides map[string]json.RawMessage +} + +func (t AssembleAndUploadGenesisTask) TaskType() string { return TaskTypeAssembleGenesis } + +func (t AssembleAndUploadGenesisTask) Validate() error { + if t.AccountBalance == "" { + return fmt.Errorf("assemble-and-upload-genesis: missing required field AccountBalance") + } + if t.Namespace == "" { + return fmt.Errorf("assemble-and-upload-genesis: missing required field Namespace") + } + if len(t.Nodes) == 0 { + return fmt.Errorf("assemble-and-upload-genesis: at least one node is required") + } + return validateGenesisAccounts("assemble-and-upload-genesis", t.Accounts) +} + +func (t AssembleAndUploadGenesisTask) ToTaskRequest() TaskRequest { + nodes := make([]interface{}, len(t.Nodes)) + for i, n := range t.Nodes { + nodes[i] = map[string]interface{}{"name": n.Name} + } + p := map[string]interface{}{ + "accountBalance": t.AccountBalance, + "namespace": t.Namespace, + "nodes": nodes, + } + if accounts := genesisAccountsToWire(t.Accounts); accounts != nil { + p["accounts"] = accounts + } + if len(t.Overrides) > 0 { + overrides := make(map[string]interface{}, len(t.Overrides)) + for k, v := range t.Overrides { + overrides[k] = v + } + p["overrides"] = overrides + } + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// AwaitConditionTask blocks until a condition is met, then optionally +// executes a post-condition action. Currently supports the "height" +// condition and the "SIGTERM_SEID" action. +type AwaitConditionTask struct { + Condition string + TargetHeight int64 + Action string +} + +func (t AwaitConditionTask) TaskType() string { return TaskTypeAwaitCondition } + +func (t AwaitConditionTask) Validate() error { + switch t.Condition { + case ConditionHeight: + if t.TargetHeight <= 0 { + return fmt.Errorf("await-condition: height condition requires TargetHeight > 0") + } + case ConditionCatchingUp: + // No parameters: caught-up is derived from the local node's /status + // (catching_up=false, height>1). + default: + return fmt.Errorf("await-condition: unknown condition %q", t.Condition) + } + if t.Action != "" && t.Action != ActionSIGTERM { + return fmt.Errorf("await-condition: unknown action %q", t.Action) + } + return nil +} + +func (t AwaitConditionTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "condition": t.Condition, + } + // targetHeight is meaningful only for the height condition; omit it + // otherwise so a catchingUp request doesn't carry a spurious zero. + if t.TargetHeight > 0 { + p["targetHeight"] = t.TargetHeight + } + if t.Action != "" { + p["action"] = t.Action + } + req := TaskRequest{Type: t.TaskType(), Params: &p} + return req +} + +// GovVoteTask submits a gov v1beta1 vote. +type GovVoteTask struct { + ChainID string + KeyName string + ProposalID uint64 + Option string // yes | no | abstain | no_with_veto + Memo string + Fees string + Gas uint64 +} + +func (t GovVoteTask) TaskType() string { return TaskTypeGovVote } + +func (t GovVoteTask) Validate() error { + if t.ChainID == "" { + return errors.New("gov-vote: chainId required") + } + if t.KeyName == "" { + return errors.New("gov-vote: keyName required") + } + if t.ProposalID == 0 { + return errors.New("gov-vote: proposalId required (must be > 0)") + } + if _, err := wire.ParseVoteOption(t.Option); err != nil { + return fmt.Errorf("gov-vote: %w", err) + } + if t.Fees == "" { + return errors.New("gov-vote: fees required") + } + if t.Gas == 0 { + return errors.New("gov-vote: gas required (must be > 0)") + } + return nil +} + +func (t GovVoteTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "chainId": t.ChainID, + "keyName": t.KeyName, + "proposalId": t.ProposalID, + "option": t.Option, + "fees": t.Fees, + "gas": t.Gas, + } + if t.Memo != "" { + p["memo"] = t.Memo + } + return TaskRequest{Type: t.TaskType(), Params: &p} +} + +// GovSoftwareUpgradeTask submits a gov v1beta1 software-upgrade +// proposal. The chain auto-assigns proposalID; it is not returned via +// the task result (operators correlate via the memo's taskID= tag). +// +// REHYDRATION WARNING: MsgSubmitProposal is NOT chain-idempotent. See +// the handler doc in sidecar/tasks/gov_software_upgrade.go and #174. +type GovSoftwareUpgradeTask struct { + ChainID string + KeyName string + + Title string + Description string + + UpgradeName string + UpgradeHeight int64 + UpgradeInfo string + + InitialDeposit string + + Memo string + Fees string + Gas uint64 +} + +func (t GovSoftwareUpgradeTask) TaskType() string { return TaskTypeGovSoftwareUpgrade } + +func (t GovSoftwareUpgradeTask) Validate() error { + if t.ChainID == "" { + return errors.New("gov-software-upgrade: chainId required") + } + if t.KeyName == "" { + return errors.New("gov-software-upgrade: keyName required") + } + if t.Title == "" { + return errors.New("gov-software-upgrade: title required") + } + if t.Description == "" { + return errors.New("gov-software-upgrade: description required") + } + if t.UpgradeName == "" { + return errors.New("gov-software-upgrade: upgradeName required") + } + if t.UpgradeHeight <= 0 { + return errors.New("gov-software-upgrade: upgradeHeight required (must be > 0)") + } + if t.InitialDeposit == "" { + return errors.New("gov-software-upgrade: initialDeposit required") + } + if t.Fees == "" { + return errors.New("gov-software-upgrade: fees required") + } + if t.Gas == 0 { + return errors.New("gov-software-upgrade: gas required (must be > 0)") + } + return nil +} + +func (t GovSoftwareUpgradeTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "chainId": t.ChainID, + "keyName": t.KeyName, + "title": t.Title, + "description": t.Description, + "upgradeName": t.UpgradeName, + "upgradeHeight": t.UpgradeHeight, + "initialDeposit": t.InitialDeposit, + "fees": t.Fees, + "gas": t.Gas, + } + if t.UpgradeInfo != "" { + p["upgradeInfo"] = t.UpgradeInfo + } + if t.Memo != "" { + p["memo"] = t.Memo + } + return TaskRequest{Type: t.TaskType(), Params: &p} +} + +// ParamChangeInput is one (subspace, key, value) entry of a +// ParameterChangeProposal. Value is raw JSON of whatever shape the +// param's registered type expects — a scalar (100), a string +// ("86400000000000"), a bool, or an object. It is carried as +// json.RawMessage and stringified exactly ONCE in the handler +// (gov_param_change.go); a pre-escaped string would double-encode and +// fail at apply time. Integer-valued params must be JSON strings (e.g. +// "100"), not bare numbers — the sidecar decode is float64-based and +// loses precision above 2^53 (Sei large-integer params are string-encoded +// by convention). +type ParamChangeInput struct { + Subspace string `json:"subspace"` + Key string `json:"key"` + Value json.RawMessage `json:"value"` +} + +// GovParamChangeTask submits a gov v1beta1 ParameterChangeProposal. The +// chain auto-assigns proposalID; it is not returned via the task result +// (operators correlate via the memo's taskID= tag). +// +// REHYDRATION WARNING: MsgSubmitProposal is NOT chain-idempotent, and — +// unlike a software upgrade, which the upgrade module applies once at a +// named height — a param-change has no "applies once" safety net: a +// rehydration double-submit produces two real proposals and two +// deposits. See the handler doc in sidecar/tasks/gov_param_change.go +// and #174. +type GovParamChangeTask struct { + ChainID string + KeyName string + + Title string + Description string + + Changes []ParamChangeInput + + InitialDeposit string + + Memo string + Fees string + Gas uint64 +} + +func (t GovParamChangeTask) TaskType() string { return TaskTypeGovParamChange } + +func (t GovParamChangeTask) Validate() error { + if t.ChainID == "" { + return errors.New("gov-param-change: chainId required") + } + if t.KeyName == "" { + return errors.New("gov-param-change: keyName required") + } + if t.Title == "" { + return errors.New("gov-param-change: title required") + } + if t.Description == "" { + return errors.New("gov-param-change: description required") + } + if len(t.Changes) == 0 { + return errors.New("gov-param-change: at least one change required") + } + for i, c := range t.Changes { + if c.Subspace == "" { + return fmt.Errorf("gov-param-change: changes[%d].subspace required", i) + } + if c.Key == "" { + return fmt.Errorf("gov-param-change: changes[%d].key required", i) + } + if len(c.Value) == 0 { + return fmt.Errorf("gov-param-change: changes[%d].value required", i) + } + } + if t.InitialDeposit == "" { + return errors.New("gov-param-change: initialDeposit required") + } + if t.Fees == "" { + return errors.New("gov-param-change: fees required") + } + if t.Gas == 0 { + return errors.New("gov-param-change: gas required (must be > 0)") + } + return nil +} + +func (t GovParamChangeTask) ToTaskRequest() TaskRequest { + p := map[string]interface{}{ + "chainId": t.ChainID, + "keyName": t.KeyName, + "title": t.Title, + "description": t.Description, + "changes": t.Changes, + "initialDeposit": t.InitialDeposit, + "fees": t.Fees, + "gas": t.Gas, + } + if t.Memo != "" { + p["memo"] = t.Memo + } + return TaskRequest{Type: t.TaskType(), Params: &p} +} diff --git a/sidecarapi/client/tasks_genesis_accounts_test.go b/sidecarapi/client/tasks_genesis_accounts_test.go new file mode 100644 index 00000000..b3c11023 --- /dev/null +++ b/sidecarapi/client/tasks_genesis_accounts_test.go @@ -0,0 +1,109 @@ +package client + +import ( + "strings" + "testing" +) + +const ( + validSeiAddr1 = "sei1zg69v7y6hn00qy352euf40x77qfrg4nclsjzp9" + validSeiAddr2 = "sei140x77qfrg4ncn27dauqjx3t83x4ummcpmrsjjl" +) + +func validNonForkTask(accounts []GenesisAccountEntry) AssembleAndUploadGenesisTask { + return AssembleAndUploadGenesisTask{ + AccountBalance: "1000usei", + Namespace: "default", + Nodes: []GenesisNodeParam{{Name: "node-0"}}, + Accounts: accounts, + } +} + +func TestAssembleAndUploadGenesisTask_ValidateAccounts(t *testing.T) { + cases := []struct { + name string + accounts []GenesisAccountEntry + wantErr string + }{ + {name: "no accounts", accounts: nil}, + {name: "valid single", accounts: []GenesisAccountEntry{{Address: validSeiAddr1, Balance: "1usei"}}}, + {name: "valid multiple", accounts: []GenesisAccountEntry{{Address: validSeiAddr1, Balance: "1usei"}, {Address: validSeiAddr2, Balance: "2usei"}}}, + {name: "missing address", accounts: []GenesisAccountEntry{{Balance: "1usei"}}, wantErr: "missing required field Address"}, + {name: "missing balance", accounts: []GenesisAccountEntry{{Address: validSeiAddr1}}, wantErr: "missing required field Balance"}, + {name: "wrong hrp", accounts: []GenesisAccountEntry{{Address: "cosmos1zg69v7y6hn00qy352euf40x77qfrg4ncjur58y", Balance: "1usei"}}, wantErr: `hrp "cosmos"`}, + {name: "bad checksum", accounts: []GenesisAccountEntry{{Address: "sei1zg69v7y6hn00qy352euf40x77qfrg4nclsjzpz", Balance: "1usei"}}, wantErr: "address"}, + {name: "not bech32", accounts: []GenesisAccountEntry{{Address: "junk", Balance: "1usei"}}, wantErr: "address"}, + {name: "valid vesting", accounts: []GenesisAccountEntry{{Address: validSeiAddr1, Balance: "2usei", Vesting: &GenesisAccountVesting{Amount: "1usei", EndTime: 1893456000}}}}, + {name: "vesting missing amount", accounts: []GenesisAccountEntry{{Address: validSeiAddr1, Balance: "1usei", Vesting: &GenesisAccountVesting{EndTime: 1893456000}}}, wantErr: "vesting missing required field Amount"}, + {name: "vesting non-positive end time", accounts: []GenesisAccountEntry{{Address: validSeiAddr1, Balance: "1usei", Vesting: &GenesisAccountVesting{Amount: "1usei"}}}, wantErr: "EndTime must be a positive unix timestamp"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := validNonForkTask(tc.accounts).Validate() + if tc.wantErr == "" { + if err != nil { + t.Errorf("expected nil error, got %v", err) + } + return + } + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErr) + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("error: got %q, want substring %q", err.Error(), tc.wantErr) + } + }) + } +} + +func TestAssembleAndUploadGenesisTask_ToTaskRequest_OmitsEmpty(t *testing.T) { + req := validNonForkTask(nil).ToTaskRequest() + if req.Params == nil { + t.Fatal("Params nil") + } + if _, present := (*req.Params)["accounts"]; present { + t.Errorf("nil accounts should omit field; got: %+v", *req.Params) + } +} + +func TestAssembleAndUploadGenesisTask_ToTaskRequest_SerializesAccounts(t *testing.T) { + accs := []GenesisAccountEntry{{Address: validSeiAddr1, Balance: "1000usei"}} + req := validNonForkTask(accs).ToTaskRequest() + if req.Type != TaskTypeAssembleGenesis { + t.Errorf("Type: got %q, want %q", req.Type, TaskTypeAssembleGenesis) + } + got, ok := (*req.Params)["accounts"].([]interface{}) + if !ok || len(got) != 1 { + t.Fatalf("accounts: %+v", (*req.Params)["accounts"]) + } + entry := got[0].(map[string]interface{}) + if entry["address"] != validSeiAddr1 || entry["balance"] != "1000usei" { + t.Errorf("entry: got %+v", entry) + } + if _, present := entry["vesting"]; present { + t.Errorf("non-vesting account should omit vesting key; got %+v", entry) + } +} + +// genesisAccountsToWire hand-builds the request map, a second serializer of the +// same object independent of the struct's json tags, so this pins the keys and +// value types it emits. The server unmarshals into the same wire type, so the +// tags themselves need no cross-package check. +func TestAssembleAndUploadGenesisTask_ToTaskRequest_SerializesVesting(t *testing.T) { + accs := []GenesisAccountEntry{{ + Address: validSeiAddr1, + Balance: "2000000usei", + Vesting: &GenesisAccountVesting{Amount: "1000000usei", EndTime: 1893456000, Delayed: true}, + }} + req := validNonForkTask(accs).ToTaskRequest() + + got := (*req.Params)["accounts"].([]interface{}) + entry := got[0].(map[string]interface{}) + vesting, ok := entry["vesting"].(map[string]interface{}) + if !ok { + t.Fatalf("vesting key: got %+v", entry["vesting"]) + } + if vesting["amount"] != "1000000usei" || vesting["endTime"] != int64(1893456000) || vesting["delayed"] != true { + t.Errorf("vesting map: got %+v", vesting) + } +} diff --git a/sidecarapi/client/tasks_test.go b/sidecarapi/client/tasks_test.go new file mode 100644 index 00000000..faa1b7bd --- /dev/null +++ b/sidecarapi/client/tasks_test.go @@ -0,0 +1,637 @@ +package client + +import ( + "encoding/json" + "testing" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/gen" + "github.com/leanovate/gopter/prop" +) + +func genNonEmptyString() gopter.Gen { + return gen.AlphaString().SuchThat(func(v string) bool { return len(v) > 0 }) +} + +func genSnapshotRestoreTask() gopter.Gen { + return gen.Int64Range(0, 300000000).Map(func(h int64) SnapshotRestoreTask { + return SnapshotRestoreTask{TargetHeight: h} + }) +} + +func genSnapshotUploadTask() gopter.Gen { + return gen.Const(SnapshotUploadTask{}) +} + +func genConfigureGenesisTask() gopter.Gen { + return gen.Const(ConfigureGenesisTask{}) +} + +func genConfigPatchTask() gopter.Gen { + return gopter.CombineGens( + genNonEmptyString(), + genNonEmptyString(), + ).Map(func(v []interface{}) ConfigPatchTask { + return ConfigPatchTask{ + Files: map[string]map[string]any{ + "config.toml": { + "p2p": map[string]any{"persistent-peers": v[0].(string)}, + }, + "app.toml": { + "pruning": v[1].(string), + }, + }, + } + }) +} + +func TestSnapshotRestoreRoundTrip(t *testing.T) { + properties := gopter.NewProperties(gopter.DefaultTestParameters()) + properties.Property("SnapshotRestoreTask round-trips through TaskRequest", prop.ForAll( + func(task SnapshotRestoreTask) bool { + if err := task.Validate(); err != nil { + return false + } + req := task.ToTaskRequest() + if req.Type != TaskTypeSnapshotRestore { + return false + } + if task.TargetHeight == 0 { + return req.Params == nil + } + rebuilt := snapshotRestoreTaskFromParams(*req.Params) + return rebuilt.TargetHeight == task.TargetHeight + }, + genSnapshotRestoreTask(), + )) + properties.TestingRun(t) +} + +func TestSnapshotUploadRoundTrip(t *testing.T) { + properties := gopter.NewProperties(gopter.DefaultTestParameters()) + properties.Property("SnapshotUploadTask round-trips through TaskRequest", prop.ForAll( + func(task SnapshotUploadTask) bool { + if err := task.Validate(); err != nil { + return false + } + req := task.ToTaskRequest() + return req.Type == TaskTypeSnapshotUpload && req.Params == nil + }, + genSnapshotUploadTask(), + )) + properties.TestingRun(t) +} + +func TestSnapshotUploadOnceRoundTrip(t *testing.T) { + task := SnapshotUploadOnceTask{} + if err := task.Validate(); err != nil { + t.Fatalf("Validate() = %v, want nil", err) + } + req := task.ToTaskRequest() + if req.Type != TaskTypeSnapshotUploadOnce { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeSnapshotUploadOnce) + } + if req.Params != nil { + t.Errorf("Params = %v, want nil (empty params)", req.Params) + } +} + +func TestConfigureGenesisRoundTrip_S3(t *testing.T) { + properties := gopter.NewProperties(gopter.DefaultTestParameters()) + properties.Property("ConfigureGenesisTask round-trips through TaskRequest", prop.ForAll( + func(task ConfigureGenesisTask) bool { + if err := task.Validate(); err != nil { + return false + } + req := task.ToTaskRequest() + return req.Type == TaskTypeConfigureGenesis && req.Params == nil + }, + genConfigureGenesisTask(), + )) + properties.TestingRun(t) +} + +func TestConfigPatchRoundTrip(t *testing.T) { + properties := gopter.NewProperties(gopter.DefaultTestParameters()) + properties.Property("ConfigPatchTask round-trips through TaskRequest", prop.ForAll( + func(task ConfigPatchTask) bool { + if err := task.Validate(); err != nil { + return false + } + req := task.ToTaskRequest() + if req.Type != TaskTypeConfigPatch { + return false + } + if req.Params == nil { + return false + } + files, ok := (*req.Params)["files"] + if !ok { + return false + } + filesMap, ok := files.(map[string]interface{}) + if !ok { + return false + } + return len(filesMap) == len(task.Files) + }, + genConfigPatchTask(), + )) + properties.TestingRun(t) +} + +func TestConfigureStateSyncRoundTrip(t *testing.T) { + task := ConfigureStateSyncTask{} + if err := task.Validate(); err != nil { + t.Fatalf("Validate() = %v", err) + } + req := task.ToTaskRequest() + if req.Type != TaskTypeConfigureStateSync { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeConfigureStateSync) + } + if req.Params != nil { + t.Errorf("Params = %v, want nil", req.Params) + } +} + +// Wire contract the controller depends on: RpcServers must surface verbatim +// under the "rpcServers" params key the sidecar handler reads. +func TestConfigureStateSyncRpcServersWire(t *testing.T) { + witnesses := []string{ + "syncer-0-0-0.syncer-0-0.arctic-1.svc.cluster.local:26657", + "syncer-0-1-0.syncer-0-1.arctic-1.svc.cluster.local:26657", + } + req := ConfigureStateSyncTask{RpcServers: witnesses}.ToTaskRequest() + if req.Params == nil { + t.Fatal("Params = nil, want rpcServers populated") + } + got, ok := (*req.Params)["rpcServers"].([]string) + if !ok { + t.Fatalf("params[rpcServers] = %T, want []string", (*req.Params)["rpcServers"]) + } + if len(got) != len(witnesses) { + t.Fatalf("rpcServers = %v, want %v", got, witnesses) + } + for i := range witnesses { + if got[i] != witnesses[i] { + t.Errorf("rpcServers[%d] = %q, want %q", i, got[i], witnesses[i]) + } + } +} + +func TestMarkReadyRoundTrip(t *testing.T) { + task := MarkReadyTask{} + if err := task.Validate(); err != nil { + t.Fatalf("Validate() = %v", err) + } + req := task.ToTaskRequest() + if req.Type != TaskTypeMarkReady { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeMarkReady) + } + if req.Params != nil { + t.Errorf("Params = %v, want nil", req.Params) + } +} + +func TestWorkflowHoldTasksRoundTrip(t *testing.T) { + cases := []struct { + task TaskBuilder + wantType string + }{ + {MarkNotReadyTask{}, TaskTypeMarkNotReady}, + {StopSeidTask{}, TaskTypeStopSeid}, + {ResetDataTask{}, TaskTypeResetData}, + } + for _, tc := range cases { + t.Run(tc.wantType, func(t *testing.T) { + if err := tc.task.Validate(); err != nil { + t.Fatalf("Validate() = %v", err) + } + req := tc.task.ToTaskRequest() + if req.Type != tc.wantType { + t.Errorf("Type = %q, want %q", req.Type, tc.wantType) + } + if req.Params != nil { + t.Errorf("Params = %v, want nil (empty-payload task)", req.Params) + } + }) + } +} + +func TestAwaitCatchingUpRoundTrip(t *testing.T) { + task := AwaitConditionTask{Condition: ConditionCatchingUp} + if err := task.Validate(); err != nil { + t.Fatalf("Validate() = %v", err) + } + req := task.ToTaskRequest() + if req.Type != TaskTypeAwaitCondition { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeAwaitCondition) + } + if req.Params == nil { + t.Fatal("expected non-nil Params") + } + p := *req.Params + if p["condition"] != ConditionCatchingUp { + t.Errorf("condition = %v, want %q", p["condition"], ConditionCatchingUp) + } + // targetHeight is meaningless for catchingUp and must be omitted. + if _, ok := p["targetHeight"]; ok { + t.Errorf("targetHeight should be omitted for catchingUp, got %v", p["targetHeight"]) + } +} + +func TestSetGenesisPeersRoundTrip(t *testing.T) { + task := SetGenesisPeersTask{} + if err := task.Validate(); err != nil { + t.Fatalf("Validate() = %v", err) + } + req := task.ToTaskRequest() + if req.Type != TaskTypeSetGenesisPeers { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeSetGenesisPeers) + } + if req.Params != nil { + t.Errorf("Params = %v, want nil", req.Params) + } +} + +func TestSnapshotRestoreValidation(t *testing.T) { + // SnapshotRestoreTask has no required fields — TargetHeight=0 means "use latest" + cases := []struct { + name string + task SnapshotRestoreTask + }{ + {"zero height (latest)", SnapshotRestoreTask{}}, + {"with target height", SnapshotRestoreTask{TargetHeight: 100000000}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if err := tc.task.Validate(); err != nil { + t.Errorf("unexpected validation error: %v", err) + } + }) + } +} + +func TestSnapshotUploadValidation(t *testing.T) { + task := SnapshotUploadTask{} + if err := task.Validate(); err != nil { + t.Errorf("unexpected error: %v", err) + } +} + +func TestConfigureGenesisValidation(t *testing.T) { + task := ConfigureGenesisTask{} + if err := task.Validate(); err != nil { + t.Errorf("expected no error, got %v", err) + } +} + +func TestConfigPatchToTaskRequest_NestedValuesPreserved(t *testing.T) { + task := ConfigPatchTask{ + Files: map[string]map[string]any{ + "config.toml": { + "statesync": map[string]any{ + "use-local-snapshot": true, + "backfill-blocks": int64(0), + }, + "p2p": map[string]any{ + "persistent-peers": "abc@1.2.3.4:26656", + }, + }, + "app.toml": { + "pruning": "nothing", + "snapshot-interval": int64(2000), + }, + }, + } + + req := task.ToTaskRequest() + + // Simulate the JSON round-trip that happens on the wire. + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + var decoded TaskRequest + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + + files, ok := (*decoded.Params)["files"].(map[string]any) + if !ok { + t.Fatal("expected files to be a map after JSON round-trip") + } + + configToml, ok := files["config.toml"].(map[string]any) + if !ok { + t.Fatal("expected config.toml entry to be a map") + } + statesync, ok := configToml["statesync"].(map[string]any) + if !ok { + t.Fatal("expected statesync section to be a map") + } + if statesync["use-local-snapshot"] != true { + t.Errorf("use-local-snapshot = %v, want true", statesync["use-local-snapshot"]) + } + + appToml, ok := files["app.toml"].(map[string]any) + if !ok { + t.Fatal("expected app.toml entry to be a map") + } + if appToml["pruning"] != "nothing" { + t.Errorf("pruning = %v, want nothing", appToml["pruning"]) + } + // JSON unmarshals numbers as float64. + if appToml["snapshot-interval"] != float64(2000) { + t.Errorf("snapshot-interval = %v, want 2000", appToml["snapshot-interval"]) + } +} + +func TestConfigPatchValidationRejectsEmpty(t *testing.T) { + task := ConfigPatchTask{} + if err := task.Validate(); err == nil { + t.Error("expected error for empty ConfigPatchTask") + } +} + +func TestStatusResponseJSONRoundTrip(t *testing.T) { + properties := gopter.NewProperties(gopter.DefaultTestParameters()) + properties.Property("StatusResponse JSON round-trips", prop.ForAll( + func(status StatusResponseStatus) bool { + sr := StatusResponse{Status: status} + data, err := json.Marshal(sr) + if err != nil { + return false + } + var decoded StatusResponse + if err := json.Unmarshal(data, &decoded); err != nil { + return false + } + return decoded.Status == sr.Status + }, + gen.OneConstOf(Initializing, Ready), + )) + properties.TestingRun(t) +} + +func TestTaskRequestJSONRoundTrip(t *testing.T) { + properties := gopter.NewProperties(gopter.DefaultTestParameters()) + properties.Property("TaskRequest JSON round-trips preserve type", prop.ForAll( + func(taskType string) bool { + req := TaskRequest{Type: taskType} + data, err := json.Marshal(req) + if err != nil { + return false + } + var decoded TaskRequest + if err := json.Unmarshal(data, &decoded); err != nil { + return false + } + return decoded.Type == taskType + }, + gen.OneConstOf( + TaskTypeSnapshotRestore, + TaskTypeConfigPatch, + TaskTypeMarkReady, + TaskTypeConfigureGenesis, + TaskTypeConfigureStateSync, + TaskTypeSnapshotUpload, + ), + )) + properties.TestingRun(t) +} + +func TestResultExportRoundTrip(t *testing.T) { + properties := gopter.NewProperties(gopter.DefaultTestParameters()) + properties.Property("ResultExportTask round-trips through TaskRequest", prop.ForAll( + func(bucket, region string) bool { + task := ResultExportTask{Bucket: bucket, Region: region} + if err := task.Validate(); err != nil { + return false + } + req := task.ToTaskRequest() + if req.Type != TaskTypeResultExport { + return false + } + rebuilt := resultExportTaskFromParams(*req.Params) + return rebuilt.Bucket == task.Bucket && + rebuilt.Region == task.Region + }, + genNonEmptyString(), + genNonEmptyString(), + )) + properties.TestingRun(t) +} + +func TestResultExportValidation(t *testing.T) { + cases := []struct { + name string + task ResultExportTask + ok bool + }{ + {"valid", ResultExportTask{Bucket: "b", Region: "r"}, true}, + {"missing bucket", ResultExportTask{Region: "r"}, false}, + {"missing region", ResultExportTask{Bucket: "b"}, false}, + {"all empty", ResultExportTask{}, false}, + {"comparison fields with canonicalRpc", ResultExportTask{Bucket: "b", Region: "r", CanonicalRPC: "http://c:26657", MigrationMode: true, ContinueOnDivergence: true}, true}, + {"continueOnDivergence without canonicalRpc", ResultExportTask{Bucket: "b", Region: "r", ContinueOnDivergence: true}, false}, + {"migrationMode without canonicalRpc", ResultExportTask{Bucket: "b", Region: "r", MigrationMode: true}, false}, + {"evmRpc without canonicalRpc", ResultExportTask{Bucket: "b", Region: "r", ShadowEVMRPC: "http://s:8545"}, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.task.Validate() + if tc.ok && err != nil { + t.Errorf("unexpected error: %v", err) + } + if !tc.ok && err == nil { + t.Error("expected validation error, got nil") + } + }) + } +} + +func TestResultExportTask_WithCanonicalRPC(t *testing.T) { + task := ResultExportTask{ + Bucket: "b", + Prefix: "shadow-results/pacific-1/", + Region: "eu-central-1", + CanonicalRPC: "http://canonical-rpc:26657", + } + req := task.ToTaskRequest() + p := *req.Params + if p["canonicalRpc"] != "http://canonical-rpc:26657" { + t.Errorf("canonicalRpc = %v, want %q", p["canonicalRpc"], "http://canonical-rpc:26657") + } + + rebuilt := resultExportTaskFromParams(p) + if rebuilt.CanonicalRPC != task.CanonicalRPC { + t.Errorf("round-trip CanonicalRPC = %q, want %q", rebuilt.CanonicalRPC, task.CanonicalRPC) + } + if rebuilt.Bucket != task.Bucket { + t.Errorf("round-trip Bucket = %q, want %q", rebuilt.Bucket, task.Bucket) + } +} + +func TestResultExportTask_WithoutCanonicalRPC_OmitsParam(t *testing.T) { + task := ResultExportTask{Bucket: "b", Region: "r"} + req := task.ToTaskRequest() + p := *req.Params + if _, ok := p["canonicalRpc"]; ok { + t.Errorf("expected canonicalRpc to be absent, got %v", p["canonicalRpc"]) + } + // Comparison-mode keys are omitted entirely at their zero value. + for _, k := range []string{"migrationMode", "continueOnDivergence", "shadowEvmRpc", "canonicalEvmRpc", "traceRpc"} { + if _, ok := p[k]; ok { + t.Errorf("expected %q to be absent at zero value, got %v", k, p[k]) + } + } +} + +func TestResultExportTask_ComparisonTuningParams(t *testing.T) { + task := ResultExportTask{ + Bucket: "b", + Region: "eu-central-1", + CanonicalRPC: "http://canonical-rpc:26657", + MigrationMode: true, + ContinueOnDivergence: true, + ShadowEVMRPC: "http://shadow:8545", + CanonicalEVMRPC: "http://canonical:8545", + TraceRPC: "http://trace:8545", + } + p := *task.ToTaskRequest().Params + + if p["migrationMode"] != true { + t.Errorf("migrationMode = %v, want true", p["migrationMode"]) + } + if p["continueOnDivergence"] != true { + t.Errorf("continueOnDivergence = %v, want true", p["continueOnDivergence"]) + } + if p["shadowEvmRpc"] != task.ShadowEVMRPC { + t.Errorf("shadowEvmRpc = %v, want %q", p["shadowEvmRpc"], task.ShadowEVMRPC) + } + if p["canonicalEvmRpc"] != task.CanonicalEVMRPC { + t.Errorf("canonicalEvmRpc = %v, want %q", p["canonicalEvmRpc"], task.CanonicalEVMRPC) + } + if p["traceRpc"] != task.TraceRPC { + t.Errorf("traceRpc = %v, want %q", p["traceRpc"], task.TraceRPC) + } +} + +func TestAwaitConditionValidation(t *testing.T) { + cases := []struct { + name string + task AwaitConditionTask + ok bool + }{ + {"valid height", AwaitConditionTask{Condition: ConditionHeight, TargetHeight: 1000}, true}, + {"valid height with action", AwaitConditionTask{Condition: ConditionHeight, TargetHeight: 1000, Action: ActionSIGTERM}, true}, + {"zero height", AwaitConditionTask{Condition: ConditionHeight, TargetHeight: 0}, false}, + {"negative height", AwaitConditionTask{Condition: ConditionHeight, TargetHeight: -1}, false}, + {"unknown condition", AwaitConditionTask{Condition: "foo", TargetHeight: 1000}, false}, + {"empty condition", AwaitConditionTask{TargetHeight: 1000}, false}, + {"unknown action", AwaitConditionTask{Condition: ConditionHeight, TargetHeight: 1000, Action: "BAD"}, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := tc.task.Validate() + if tc.ok && err != nil { + t.Errorf("expected no error, got %v", err) + } + if !tc.ok && err == nil { + t.Error("expected validation error, got nil") + } + }) + } +} + +func TestAwaitConditionToTaskRequest(t *testing.T) { + task := AwaitConditionTask{ + Condition: ConditionHeight, + TargetHeight: 5000, + Action: ActionSIGTERM, + } + req := task.ToTaskRequest() + if req.Type != TaskTypeAwaitCondition { + t.Errorf("Type = %q, want %q", req.Type, TaskTypeAwaitCondition) + } + if req.Params == nil { + t.Fatal("expected non-nil Params") + } + p := *req.Params + if p["condition"] != ConditionHeight { + t.Errorf("condition = %v, want %q", p["condition"], ConditionHeight) + } + if p["targetHeight"] != int64(5000) { + t.Errorf("targetHeight = %v, want 5000", p["targetHeight"]) + } + if p["action"] != ActionSIGTERM { + t.Errorf("action = %v, want %q", p["action"], ActionSIGTERM) + } +} + +func TestAwaitConditionToTaskRequest_NoAction(t *testing.T) { + task := AwaitConditionTask{ + Condition: ConditionHeight, + TargetHeight: 100, + } + req := task.ToTaskRequest() + p := *req.Params + if _, ok := p["action"]; ok { + t.Errorf("expected action to be absent, got %v", p["action"]) + } +} + +func TestAwaitConditionJSONRoundTrip(t *testing.T) { + task := AwaitConditionTask{ + Condition: ConditionHeight, + TargetHeight: 8000, + Action: ActionSIGTERM, + } + req := task.ToTaskRequest() + + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Marshal: %v", err) + } + var decoded TaskRequest + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if decoded.Type != TaskTypeAwaitCondition { + t.Errorf("decoded Type = %q, want %q", decoded.Type, TaskTypeAwaitCondition) + } + p := *decoded.Params + if p["condition"] != ConditionHeight { + t.Errorf("condition = %v, want %q", p["condition"], ConditionHeight) + } + // JSON numbers decode as float64. + if p["targetHeight"] != float64(8000) { + t.Errorf("targetHeight = %v (type %T), want 8000", p["targetHeight"], p["targetHeight"]) + } +} + +// snapshotRestoreTaskFromParams reconstructs a SnapshotRestoreTask from +// a generic params map. Useful for round-trip testing. +func snapshotRestoreTaskFromParams(params map[string]interface{}) SnapshotRestoreTask { + var t SnapshotRestoreTask + switch h := params["targetHeight"].(type) { + case float64: + t.TargetHeight = int64(h) + case int64: + t.TargetHeight = h + } + return t +} + +// resultExportTaskFromParams reconstructs a ResultExportTask from +// a generic params map. +func resultExportTaskFromParams(params map[string]interface{}) ResultExportTask { + s := func(k string) string { v, _ := params[k].(string); return v } + return ResultExportTask{ + Bucket: s("bucket"), + Prefix: s("prefix"), + Region: s("region"), + CanonicalRPC: s("canonicalRpc"), + } +} diff --git a/sidecarapi/go.mod b/sidecarapi/go.mod new file mode 100644 index 00000000..a0ac09f6 --- /dev/null +++ b/sidecarapi/go.mod @@ -0,0 +1,17 @@ +module github.com/sei-protocol/sei-k8s-controller/sidecarapi + +go 1.26.0 + +require ( + github.com/cosmos/btcutil v1.0.5 + github.com/google/uuid v1.6.0 + github.com/leanovate/gopter v0.2.11 + github.com/oapi-codegen/runtime v1.6.0 + github.com/sei-protocol/sei-config v0.0.25 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect +) diff --git a/sidecarapi/go.sum b/sidecarapi/go.sum new file mode 100644 index 00000000..7d117fd1 --- /dev/null +++ b/sidecarapi/go.sum @@ -0,0 +1,629 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= +github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sei-protocol/sei-config v0.0.25 h1:YHW6YOD3DWSF5QRo+Om4TLeQ9o8E8qnG9jcR8E8fjGo= +github.com/sei-protocol/sei-config v0.0.25/go.mod h1:zcEdLzyIH2AyP0/QRBE3s4Y9eGn0C/qAUx1c4o4EROU= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/sidecarapi/wire/genesis_accounts.go b/sidecarapi/wire/genesis_accounts.go new file mode 100644 index 00000000..b2bde84f --- /dev/null +++ b/sidecarapi/wire/genesis_accounts.go @@ -0,0 +1,27 @@ +package wire + +// GenesisAccountEntry represents one externally-supplied genesis account. +// Mirrors SeiNetwork.Spec.Genesis.Accounts[] on the controller-CRD side. +// +// This is the single definition both sides of the wire use: sidecar/client +// aliases it for the request it builds, and sidecar/tasks aliases it for the +// payload it unmarshals. The two used to be separate structs whose json tags +// had to be kept in step by hand, checked by a cross-package round-trip test. +// One definition makes that drift unrepresentable, so no test is needed. +type GenesisAccountEntry struct { + Address string `json:"address"` + Balance string `json:"balance"` + + // Vesting, when set, locks Balance under a vesting schedule instead of + // a standard account; nil produces today's plain account. + Vesting *GenesisAccountVesting `json:"vesting,omitempty"` +} + +// GenesisAccountVesting locks part of a GenesisAccountEntry's Balance on an +// unlock schedule completing at EndTime: linear from genesis time by default, +// or all-at-once when Delayed. Amount must not exceed Balance. +type GenesisAccountVesting struct { + Amount string `json:"amount"` + EndTime int64 `json:"endTime"` + Delayed bool `json:"delayed,omitempty"` +} diff --git a/sidecarapi/wire/wire.go b/sidecarapi/wire/wire.go new file mode 100644 index 00000000..8fd5faa8 --- /dev/null +++ b/sidecarapi/wire/wire.go @@ -0,0 +1,125 @@ +// Package wire holds the light, dependency-free contract types shared between +// the sidecar server (tasks/engine) and its clients (the generated client, the +// controller). It imports no sei-chain: importing any symbol here must never +// drag the chain graph into a light consumer. Server-side packages re-export +// these (e.g. engine.TaskType aliases wire.TaskType) so their call sites are +// unchanged. +package wire + +import ( + "fmt" + "strings" +) + +// TaskType identifies a task on the wire (the request/result `type` field). +type TaskType string + +const ( + TaskSnapshotRestore TaskType = "snapshot-restore" + TaskConfigPatch TaskType = "config-patch" + TaskConfigApply TaskType = "config-apply" + TaskConfigValidate TaskType = "config-validate" + TaskConfigReload TaskType = "config-reload" + TaskMarkReady TaskType = "mark-ready" + TaskRestartSeid TaskType = "restart-seid" + TaskConfigureGenesis TaskType = "configure-genesis" + TaskConfigureStateSync TaskType = "configure-state-sync" + TaskSnapshotUpload TaskType = "snapshot-upload" + TaskSnapshotUploadOnce TaskType = "snapshot-upload-once" + TaskResultExport TaskType = "result-export" + TaskAwaitCondition TaskType = "await-condition" + TaskGenerateIdentity TaskType = "generate-identity" + TaskGenerateGentx TaskType = "generate-gentx" + TaskUploadGenesisArtifacts TaskType = "upload-genesis-artifacts" + TaskAssembleAndUploadGenesis TaskType = "assemble-and-upload-genesis" + TaskSetGenesisPeers TaskType = "set-genesis-peers" + TaskGovVote TaskType = "gov-vote" + TaskGovSoftwareUpgrade TaskType = "gov-software-upgrade" + TaskGovParamChange TaskType = "gov-param-change" + TaskEvmLogicalDigest TaskType = "evm-logical-digest" + + // Workflow node-hold tasks (SeiNodeTaskWorkflow StateSync recipe). These + // three compose the durable seid hold: mark-not-ready re-arms the start + // gate, stop-seid parks seid behind it, reset-data clears the chain data + // directory. Wire strings are a published contract (one-way door). + TaskMarkNotReady TaskType = "mark-not-ready" + TaskStopSeid TaskType = "stop-seid" + TaskResetData TaskType = "reset-data" +) + +// VoteOption mirrors cosmos gov v1beta1 VoteOption values so callers can parse +// and validate a vote string without importing sei-chain. The values are the +// stable protobuf enum numbers (guarded by a test against govtypes). +type VoteOption int + +const ( + OptionEmpty VoteOption = 0 + OptionYes VoteOption = 1 + OptionAbstain VoteOption = 2 + OptionNo VoteOption = 3 + OptionNoWithVeto VoteOption = 4 +) + +// ParseVoteOption is the single accepted-option set shared by client and server. +func ParseVoteOption(s string) (VoteOption, error) { + switch strings.ToLower(s) { + case "yes": + return OptionYes, nil + case "no": + return OptionNo, nil + case "abstain": + return OptionAbstain, nil + case "no_with_veto", "no-with-veto": + return OptionNoWithVeto, nil + default: + return 0, fmt.Errorf("invalid vote option %q (allowed: yes | no | abstain | no_with_veto)", s) + } +} + +// Inclusion outcomes carried on GovTxResult.InclusionStatus — the stable enum +// the controller keys its condition/reason and re-submit decision on. +const ( + InclusionCommittedOK = "committed_ok" + InclusionCommittedFailed = "committed_failed" + InclusionPending = "pending" + // InclusionUnverifiable: the tx was broadcast and accepted at CheckTx, but + // its on-chain outcome cannot be observed from the target node (its tx + // index is disabled), so the sidecar can confirm neither success nor + // failure. Terminal — retrying the same node is futile — but distinct from + // committed_failed: the operator must verify via an indexed RPC. + InclusionUnverifiable = "unverifiable" +) + +// GovTxResult is the structured result a gov sign-tx handler returns; the engine +// persists it on TaskResult.Result and the controller decodes it into the +// SeiNodeTask outputs. ProposalID is 0 for votes and not-yet-included submits. +type GovTxResult struct { + TxHash string `json:"txHash"` + Height int64 `json:"height,omitempty"` + ProposalID uint64 `json:"proposalId,omitempty"` + Code uint32 `json:"code,omitempty"` + Codespace string `json:"codespace,omitempty"` + RawLog string `json:"rawLog,omitempty"` + InclusionStatus string `json:"inclusionStatus"` +} + +// UploadOutcome is the terminal classification of a snapshot-upload-once, +// carried on the task result's outcome field. A one-shot poller keys its verdict +// on it, so the values are a result-wire contract shared by the sidecar handler +// and its CLI/controller consumers. +type UploadOutcome string + +const ( + OutcomeUploaded UploadOutcome = "uploaded" + OutcomeNoop UploadOutcome = "noop" + OutcomeError UploadOutcome = "error" +) + +// NoopReason explains why a snapshot-upload-once returned OutcomeNoop. Empty on +// any other outcome. +type NoopReason string + +const ( + NoopFewerThanTwoSnapshots NoopReason = "fewer-than-2-snapshots" + NoopAlreadyUploaded NoopReason = "already-uploaded" +) diff --git a/sidecarapi/wire/wire_test.go b/sidecarapi/wire/wire_test.go new file mode 100644 index 00000000..fd148aa1 --- /dev/null +++ b/sidecarapi/wire/wire_test.go @@ -0,0 +1,58 @@ +package wire + +import "testing" + +// The snapshot-upload outcome values are a wire contract: the CLI poller and the +// controller classify against these exact strings. A rename here that drifted a +// value would silently reclassify every upload, so pin the bytes. +func TestSnapshotUploadWireValues(t *testing.T) { + cases := []struct { + got string + want string + }{ + {string(OutcomeUploaded), "uploaded"}, + {string(OutcomeNoop), "noop"}, + {string(OutcomeError), "error"}, + {string(NoopFewerThanTwoSnapshots), "fewer-than-2-snapshots"}, + {string(NoopAlreadyUploaded), "already-uploaded"}, + } + for _, c := range cases { + if c.got != c.want { + t.Errorf("wire value = %q, want %q", c.got, c.want) + } + } +} + +func TestParseVoteOption(t *testing.T) { + cases := []struct { + in string + want VoteOption + err bool + }{ + {"yes", OptionYes, false}, + {"YES", OptionYes, false}, + {"no", OptionNo, false}, + {"abstain", OptionAbstain, false}, + {"no_with_veto", OptionNoWithVeto, false}, + {"no-with-veto", OptionNoWithVeto, false}, + {"NO_WITH_VETO", OptionNoWithVeto, false}, + {"", 0, true}, + {"maybe", 0, true}, + } + for _, c := range cases { + got, err := ParseVoteOption(c.in) + if c.err { + if err == nil { + t.Errorf("ParseVoteOption(%q): want err, got %v", c.in, got) + } + continue + } + if err != nil { + t.Errorf("ParseVoteOption(%q): unexpected err: %v", c.in, err) + continue + } + if got != c.want { + t.Errorf("ParseVoteOption(%q) = %v, want %v", c.in, got, c.want) + } + } +} diff --git a/test/integration/Dockerfile b/test/integration/Dockerfile index 37b21f70..a893c086 100644 --- a/test/integration/Dockerfile +++ b/test/integration/Dockerfile @@ -9,6 +9,10 @@ ARG TARGETARCH WORKDIR /workspace COPY go.mod go.mod COPY go.sum go.sum +# sidecarapi is resolved through a filesystem `replace`, so its manifests must +# exist before the prefetch. See the same copy in the root Dockerfile. +COPY sidecarapi/go.mod sidecarapi/go.mod +COPY sidecarapi/go.sum sidecarapi/go.sum RUN go mod download COPY . . diff --git a/test/integration/giga_migration_test.go b/test/integration/giga_migration_test.go index 023790a9..d04c4f8f 100644 --- a/test/integration/giga_migration_test.go +++ b/test/integration/giga_migration_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1" "github.com/sei-protocol/sei-k8s-controller/sdk/sei" ) From a9d89c52902342601684951b3c5a2d3a4b53dd27 Mon Sep 17 00:00:00 2001 From: bdchatham Date: Thu, 13 Aug 2026 13:34:37 -0700 Subject: [PATCH 2/2] style(sidecarapi): satisfy the linters now that this code is linted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding sidecarapi to the lint matrix surfaced 51 findings in code that had never been linted — seictl carries no golangci-lint config at all. The root module's `lint (.)` leg hides its own backlog behind only-new-issues, but every file here is new to this branch, so nothing was filtered. 34 modernize hits were interface{} where any will do. Replaced in the three hand-written files. sidecar.gen.go keeps its two: it is generated and marked DO NOT EDIT, and the generated-exclusion already keeps it out of the report. goconst is excluded for sidecarapi/client instead. What it flags there are the JSON keys inside the request-params maps — chainId, keyName, fees, gas, title, initialDeposit, address. Those literals are the wire contract: being able to read a params map and see the JSON it emits is what makes this client reviewable against the server's struct tags, and it is why the client/server key mismatch this module was extracted over was findable at all. Hoisting them behind constants trades that legibility for a DRY score. The rest of the hits are test fixtures (1usei, config.toml, an RPC URL). Verified with golangci-lint against the module: 0 issues. Build, tests, tidy-check and verify-generated unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .golangci.yml | 9 ++++ sidecarapi/client/tasks.go | 53 ++++++++++--------- .../client/tasks_genesis_accounts_test.go | 10 ++-- sidecarapi/client/tasks_test.go | 8 +-- 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3bf89dfc..0f26bc92 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -80,6 +80,15 @@ linters: - dupl - lll path: sidecarapi/* + # goconst flags the JSON keys inside the request-params maps — chainId, + # keyName, fees, gas, title, initialDeposit, address. Those literals are + # the wire contract: reading a params map and seeing the JSON it produces + # is the property that makes the client reviewable against the server's + # struct tags. Hoisting them into constants would trade that away for a + # DRY score. The remaining hits are test fixtures (1usei, config.toml). + - linters: + - goconst + path: sidecarapi/client/* paths: - third_party$ - builtin$ diff --git a/sidecarapi/client/tasks.go b/sidecarapi/client/tasks.go index aec4afce..83c9fce2 100644 --- a/sidecarapi/client/tasks.go +++ b/sidecarapi/client/tasks.go @@ -7,6 +7,7 @@ import ( "github.com/cosmos/btcutil/bech32" seiconfig "github.com/sei-protocol/sei-config" + "github.com/sei-protocol/sei-k8s-controller/sidecarapi/wire" ) @@ -102,9 +103,9 @@ func (t SnapshotRestoreTask) TaskType() string { return TaskTypeSnapshotRestore func (t SnapshotRestoreTask) Validate() error { return nil } func (t SnapshotRestoreTask) ToTaskRequest() TaskRequest { - var p *map[string]interface{} + var p *map[string]any if t.TargetHeight > 0 { - m := map[string]interface{}{"targetHeight": t.TargetHeight} + m := map[string]any{"targetHeight": t.TargetHeight} p = &m } req := TaskRequest{Type: t.TaskType(), Params: p} @@ -165,7 +166,7 @@ func (t ConfigureGenesisTask) ToTaskRequest() TaskRequest { if t.ExpectedGenesisHash == "" { return TaskRequest{Type: t.TaskType()} } - p := map[string]interface{}{"expectedGenesisHash": t.ExpectedGenesisHash} + p := map[string]any{"expectedGenesisHash": t.ExpectedGenesisHash} return TaskRequest{Type: t.TaskType(), Params: &p} } @@ -186,11 +187,11 @@ func (t ConfigPatchTask) Validate() error { } func (t ConfigPatchTask) ToTaskRequest() TaskRequest { - files := make(map[string]interface{}, len(t.Files)) + files := make(map[string]any, len(t.Files)) for k, v := range t.Files { files[k] = v } - p := map[string]interface{}{"files": files} + p := map[string]any{"files": files} req := TaskRequest{Type: t.TaskType(), Params: &p} return req } @@ -209,7 +210,7 @@ func (t ConfigureStateSyncTask) TaskType() string { return TaskTypeConfigureStat func (t ConfigureStateSyncTask) Validate() error { return nil } func (t ConfigureStateSyncTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{} + p := map[string]any{} if t.UseLocalSnapshot { p["useLocalSnapshot"] = true } @@ -327,13 +328,13 @@ func (t ConfigApplyTask) Validate() error { } func (t ConfigApplyTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "mode": string(t.Intent.Mode), "incremental": t.Intent.Incremental, "targetVersion": t.Intent.TargetVersion, } if len(t.Intent.Overrides) > 0 { - overrides := make(map[string]interface{}, len(t.Intent.Overrides)) + overrides := make(map[string]any, len(t.Intent.Overrides)) for k, v := range t.Intent.Overrides { overrides[k] = v } @@ -370,11 +371,11 @@ func (t ConfigReloadTask) Validate() error { } func (t ConfigReloadTask) ToTaskRequest() TaskRequest { - fields := make(map[string]interface{}, len(t.Fields)) + fields := make(map[string]any, len(t.Fields)) for k, v := range t.Fields { fields[k] = v } - p := map[string]interface{}{"fields": fields} + p := map[string]any{"fields": fields} req := TaskRequest{Type: t.TaskType(), Params: &p} return req } @@ -424,7 +425,7 @@ func (t ResultExportTask) Validate() error { } func (t ResultExportTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "bucket": t.Bucket, "region": t.Region, } @@ -472,7 +473,7 @@ func (t GenerateIdentityTask) Validate() error { } func (t GenerateIdentityTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "chainId": t.ChainID, "moniker": t.Moniker, } @@ -505,7 +506,7 @@ func (t GenerateGentxTask) Validate() error { } func (t GenerateGentxTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "chainId": t.ChainID, "stakingAmount": t.StakingAmount, "accountBalance": t.AccountBalance, @@ -530,7 +531,7 @@ func (t UploadGenesisArtifactsTask) Validate() error { } func (t UploadGenesisArtifactsTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "nodeName": t.NodeName, } req := TaskRequest{Type: t.TaskType(), Params: &p} @@ -551,15 +552,15 @@ type ( GenesisAccountVesting = wire.GenesisAccountVesting ) -func genesisAccountsToWire(accounts []GenesisAccountEntry) []interface{} { +func genesisAccountsToWire(accounts []GenesisAccountEntry) []any { if len(accounts) == 0 { return nil } - out := make([]interface{}, len(accounts)) + out := make([]any, len(accounts)) for i, a := range accounts { - entry := map[string]interface{}{"address": a.Address, "balance": a.Balance} + entry := map[string]any{"address": a.Address, "balance": a.Balance} if a.Vesting != nil { - entry["vesting"] = map[string]interface{}{ + entry["vesting"] = map[string]any{ "amount": a.Vesting.Amount, "endTime": a.Vesting.EndTime, "delayed": a.Vesting.Delayed, @@ -632,11 +633,11 @@ func (t AssembleAndUploadGenesisTask) Validate() error { } func (t AssembleAndUploadGenesisTask) ToTaskRequest() TaskRequest { - nodes := make([]interface{}, len(t.Nodes)) + nodes := make([]any, len(t.Nodes)) for i, n := range t.Nodes { - nodes[i] = map[string]interface{}{"name": n.Name} + nodes[i] = map[string]any{"name": n.Name} } - p := map[string]interface{}{ + p := map[string]any{ "accountBalance": t.AccountBalance, "namespace": t.Namespace, "nodes": nodes, @@ -645,7 +646,7 @@ func (t AssembleAndUploadGenesisTask) ToTaskRequest() TaskRequest { p["accounts"] = accounts } if len(t.Overrides) > 0 { - overrides := make(map[string]interface{}, len(t.Overrides)) + overrides := make(map[string]any, len(t.Overrides)) for k, v := range t.Overrides { overrides[k] = v } @@ -685,7 +686,7 @@ func (t AwaitConditionTask) Validate() error { } func (t AwaitConditionTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "condition": t.Condition, } // targetHeight is meaningful only for the height condition; omit it @@ -736,7 +737,7 @@ func (t GovVoteTask) Validate() error { } func (t GovVoteTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "chainId": t.ChainID, "keyName": t.KeyName, "proposalId": t.ProposalID, @@ -808,7 +809,7 @@ func (t GovSoftwareUpgradeTask) Validate() error { } func (t GovSoftwareUpgradeTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "chainId": t.ChainID, "keyName": t.KeyName, "title": t.Title, @@ -912,7 +913,7 @@ func (t GovParamChangeTask) Validate() error { } func (t GovParamChangeTask) ToTaskRequest() TaskRequest { - p := map[string]interface{}{ + p := map[string]any{ "chainId": t.ChainID, "keyName": t.KeyName, "title": t.Title, diff --git a/sidecarapi/client/tasks_genesis_accounts_test.go b/sidecarapi/client/tasks_genesis_accounts_test.go index b3c11023..99c679f9 100644 --- a/sidecarapi/client/tasks_genesis_accounts_test.go +++ b/sidecarapi/client/tasks_genesis_accounts_test.go @@ -72,11 +72,11 @@ func TestAssembleAndUploadGenesisTask_ToTaskRequest_SerializesAccounts(t *testin if req.Type != TaskTypeAssembleGenesis { t.Errorf("Type: got %q, want %q", req.Type, TaskTypeAssembleGenesis) } - got, ok := (*req.Params)["accounts"].([]interface{}) + got, ok := (*req.Params)["accounts"].([]any) if !ok || len(got) != 1 { t.Fatalf("accounts: %+v", (*req.Params)["accounts"]) } - entry := got[0].(map[string]interface{}) + entry := got[0].(map[string]any) if entry["address"] != validSeiAddr1 || entry["balance"] != "1000usei" { t.Errorf("entry: got %+v", entry) } @@ -97,9 +97,9 @@ func TestAssembleAndUploadGenesisTask_ToTaskRequest_SerializesVesting(t *testing }} req := validNonForkTask(accs).ToTaskRequest() - got := (*req.Params)["accounts"].([]interface{}) - entry := got[0].(map[string]interface{}) - vesting, ok := entry["vesting"].(map[string]interface{}) + got := (*req.Params)["accounts"].([]any) + entry := got[0].(map[string]any) + vesting, ok := entry["vesting"].(map[string]any) if !ok { t.Fatalf("vesting key: got %+v", entry["vesting"]) } diff --git a/sidecarapi/client/tasks_test.go b/sidecarapi/client/tasks_test.go index faa1b7bd..a34abd9b 100644 --- a/sidecarapi/client/tasks_test.go +++ b/sidecarapi/client/tasks_test.go @@ -31,7 +31,7 @@ func genConfigPatchTask() gopter.Gen { return gopter.CombineGens( genNonEmptyString(), genNonEmptyString(), - ).Map(func(v []interface{}) ConfigPatchTask { + ).Map(func(v []any) ConfigPatchTask { return ConfigPatchTask{ Files: map[string]map[string]any{ "config.toml": { @@ -129,7 +129,7 @@ func TestConfigPatchRoundTrip(t *testing.T) { if !ok { return false } - filesMap, ok := files.(map[string]interface{}) + filesMap, ok := files.(map[string]any) if !ok { return false } @@ -613,7 +613,7 @@ func TestAwaitConditionJSONRoundTrip(t *testing.T) { // snapshotRestoreTaskFromParams reconstructs a SnapshotRestoreTask from // a generic params map. Useful for round-trip testing. -func snapshotRestoreTaskFromParams(params map[string]interface{}) SnapshotRestoreTask { +func snapshotRestoreTaskFromParams(params map[string]any) SnapshotRestoreTask { var t SnapshotRestoreTask switch h := params["targetHeight"].(type) { case float64: @@ -626,7 +626,7 @@ func snapshotRestoreTaskFromParams(params map[string]interface{}) SnapshotRestor // resultExportTaskFromParams reconstructs a ResultExportTask from // a generic params map. -func resultExportTaskFromParams(params map[string]interface{}) ResultExportTask { +func resultExportTaskFromParams(params map[string]any) ResultExportTask { s := func(k string) string { v, _ := params[k].(string); return v } return ResultExportTask{ Bucket: s("bucket"),