From 6cf6f95f587536c6ca399dd30e26f0c221019a7e Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:39:43 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/bug-report.md | 26 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature-request.md | 55 + .github/ISSUE_TEMPLATE/proposal.md | 75 + .github/ISSUE_TEMPLATE/question.md | 10 + .github/dependabot.yml | 7 + .github/pull_request_template.md | 37 + .github/semantic.yml | 33 + .github/stable.yml | 12 + .github/workflows/codeql-analysis.yml | 22 + .github/workflows/comment-check.yml | 155 + .github/workflows/gitee-sync.yml | 27 + .github/workflows/go.yml | 92 + .github/workflows/issue-translator.yml | 16 + .github/workflows/lint.yml | 36 + .gitignore | 40 + .golangci.yml | 71 + CODE_OF_CONDUCT.md | 128 + CONTRIBUTING.md | 121 + LICENSE | 21 + Makefile | 99 + README.md | 147 + README.wehub.md | 7 + README_zh.md | 148 + ROADMAP.md | 82 + SECURITY.md | 19 + app.go | 212 + app_test.go | 300 + buf.gen.yaml | 9 + buf.yaml | 12 + cmd/kratos/go.mod | 55 + cmd/kratos/go.sum | 146 + cmd/kratos/internal/base/install.go | 25 + cmd/kratos/internal/base/mod.go | 62 + cmd/kratos/internal/base/mod_test.go | 43 + cmd/kratos/internal/base/path.go | 108 + cmd/kratos/internal/base/repo.go | 126 + cmd/kratos/internal/base/repo_test.go | 51 + cmd/kratos/internal/base/vcs_url.go | 58 + cmd/kratos/internal/base/vcs_url_test.go | 55 + cmd/kratos/internal/change/change.go | 45 + cmd/kratos/internal/change/get.go | 234 + cmd/kratos/internal/change/get_test.go | 25 + cmd/kratos/internal/project/add.go | 76 + cmd/kratos/internal/project/new.go | 66 + cmd/kratos/internal/project/project.go | 205 + .../internal/project/project_linux_test.go | 28 + cmd/kratos/internal/project/project_test.go | 144 + .../internal/project/project_windows_test.go | 29 + cmd/kratos/internal/proto/add/add.go | 78 + cmd/kratos/internal/proto/add/add_test.go | 38 + cmd/kratos/internal/proto/add/proto.go | 40 + cmd/kratos/internal/proto/add/template.go | 52 + cmd/kratos/internal/proto/client/client.go | 130 + cmd/kratos/internal/proto/proto.go | 22 + cmd/kratos/internal/proto/server/server.go | 124 + .../internal/proto/server/server_test.go | 117 + cmd/kratos/internal/proto/server/template.go | 147 + cmd/kratos/internal/run/run.go | 145 + cmd/kratos/internal/upgrade/upgrade.go | 32 + cmd/kratos/main.go | 34 + cmd/kratos/version.go | 4 + cmd/protoc-gen-go-errors/buf.gen.yaml | 6 + cmd/protoc-gen-go-errors/buf.lock | 2 + cmd/protoc-gen-go-errors/buf.yaml | 16 + cmd/protoc-gen-go-errors/errors.go | 135 + cmd/protoc-gen-go-errors/errors/errors.pb.go | 229 + cmd/protoc-gen-go-errors/errors/errors.proto | 25 + cmd/protoc-gen-go-errors/errorsTemplate.tpl | 17 + cmd/protoc-gen-go-errors/errors_test.go | 62 + cmd/protoc-gen-go-errors/go.mod | 8 + cmd/protoc-gen-go-errors/go.sum | 6 + cmd/protoc-gen-go-errors/main.go | 32 + cmd/protoc-gen-go-errors/template.go | 35 + cmd/protoc-gen-go-errors/version.go | 4 + cmd/protoc-gen-go-http/.gitignore | 2 + cmd/protoc-gen-go-http/go.mod | 7 + cmd/protoc-gen-go-http/go.sum | 6 + cmd/protoc-gen-go-http/http.go | 395 ++ cmd/protoc-gen-go-http/httpTemplate.tpl | 348 ++ cmd/protoc-gen-go-http/http_test.go | 289 + cmd/protoc-gen-go-http/main.go | 35 + cmd/protoc-gen-go-http/template.go | 61 + cmd/protoc-gen-go-http/version.go | 4 + codecov.yml | 3 + config/README.md | 25 + config/config.go | 185 + config/config_test.go | 205 + config/env/env.go | 62 + config/env/env_test.go | 429 ++ config/env/watcher.go | 30 + config/env/watcher_test.go | 32 + config/file/file.go | 80 + config/file/file_test.go | 341 ++ config/file/format.go | 10 + config/file/format_test.go | 57 + config/file/watcher.go | 70 + config/merge.go | 48 + config/merge_test.go | 126 + config/options.go | 203 + config/options_test.go | 397 ++ config/reader.go | 172 + config/reader_test.go | 322 ++ config/source.go | 20 + config/value.go | 192 + config/value_test.go | 224 + contrib/config/apollo/README.md | 100 + contrib/config/apollo/apollo.go | 286 + contrib/config/apollo/apollo_test.go | 90 + contrib/config/apollo/go.mod | 28 + contrib/config/apollo/go.sum | 609 ++ contrib/config/apollo/parser.go | 15 + contrib/config/apollo/watcher.go | 108 + contrib/config/apollo/watcher_test.go | 146 + contrib/config/consul/README.md | 25 + contrib/config/consul/config.go | 90 + contrib/config/consul/config_test.go | 259 + contrib/config/consul/go.mod | 32 + contrib/config/consul/go.sum | 240 + contrib/config/consul/watcher.go | 103 + contrib/config/etcd/README.md | 44 + contrib/config/etcd/config.go | 96 + contrib/config/etcd/config_test.go | 167 + contrib/config/etcd/go.mod | 30 + contrib/config/etcd/go.sum | 108 + contrib/config/etcd/watcher.go | 51 + contrib/config/kubernetes/README.md | 34 + contrib/config/kubernetes/config.go | 145 + contrib/config/kubernetes/config_test.go | 208 + contrib/config/kubernetes/go.mod | 64 + contrib/config/kubernetes/go.sum | 128 + contrib/config/kubernetes/watcher.go | 61 + contrib/config/kubernetes/watcher_test.go | 42 + contrib/config/nacos/README.md | 34 + contrib/config/nacos/config.go | 81 + contrib/config/nacos/config_test.go | 208 + contrib/config/nacos/go.mod | 32 + contrib/config/nacos/go.sum | 182 + contrib/config/nacos/watcher.go | 66 + contrib/config/polaris/README.md | 24 + contrib/config/polaris/config.go | 108 + contrib/config/polaris/go.mod | 44 + contrib/config/polaris/go.sum | 1010 ++++ contrib/config/polaris/polaris.yaml | 8 + contrib/config/polaris/watcher.go | 80 + contrib/encoding/json/go.mod | 10 + contrib/encoding/json/go.sum | 4 + contrib/encoding/json/json.go | 71 + contrib/encoding/json/json_test.go | 168 + contrib/encoding/msgpack/go.mod | 15 + contrib/encoding/msgpack/go.sum | 12 + contrib/encoding/msgpack/msgpack.go | 29 + contrib/encoding/msgpack/msgpack_test.go | 63 + contrib/errortracker/sentry/README.md | 64 + contrib/errortracker/sentry/go.mod | 28 + contrib/errortracker/sentry/go.sum | 87 + contrib/errortracker/sentry/sentry.go | 151 + contrib/errortracker/sentry/sentry_test.go | 65 + contrib/middleware/jwt/go.mod | 20 + contrib/middleware/jwt/go.sum | 34 + contrib/middleware/jwt/jwt.go | 187 + contrib/middleware/jwt/jwt_test.go | 759 +++ contrib/middleware/validate/README.md | 15 + contrib/middleware/validate/go.mod | 27 + contrib/middleware/validate/go.sum | 55 + .../validate/internal/testdata/generate.go | 3 + .../validate/internal/testdata/test.pb.go | 258 + .../internal/testdata/test.pb.validate.go | 369 ++ .../validate/internal/testdata/test.proto | 23 + contrib/middleware/validate/validate.go | 37 + contrib/middleware/validate/validate_test.go | 82 + contrib/opensergo/README.md | 32 + contrib/opensergo/go.mod | 25 + contrib/opensergo/go.sum | 171 + contrib/opensergo/opensergo.go | 201 + contrib/opensergo/opensergo_test.go | 385 ++ contrib/otel/README.md | 40 + contrib/otel/go.mod | 38 + contrib/otel/go.sum | 76 + contrib/otel/log/log.go | 103 + contrib/otel/log/log_test.go | 165 + contrib/otel/metrics/metrics.go | 210 + contrib/otel/metrics/metrics_test.go | 256 + contrib/otel/metrics/otel.go | 7 + contrib/otel/tracing/metadata.go | 46 + contrib/otel/tracing/metadata_test.go | 107 + contrib/otel/tracing/span.go | 152 + contrib/otel/tracing/span_test.go | 251 + contrib/otel/tracing/statshandler.go | 43 + contrib/otel/tracing/statshandler_test.go | 80 + contrib/otel/tracing/tracer.go | 82 + contrib/otel/tracing/tracer_test.go | 55 + contrib/otel/tracing/tracing.go | 99 + contrib/otel/tracing/tracing_test.go | 231 + contrib/polaris/config.go | 127 + contrib/polaris/config_test.go | 386 ++ contrib/polaris/go.mod | 49 + contrib/polaris/go.sum | 1022 ++++ contrib/polaris/limiter.go | 105 + contrib/polaris/polaris.go | 101 + contrib/polaris/ratelimit.go | 52 + contrib/polaris/registry.go | 394 ++ contrib/polaris/registry_test.go | 73 + contrib/polaris/router.go | 257 + contrib/polaris/router_test.go | 205 + contrib/registry/consul/client.go | 310 ++ contrib/registry/consul/go.mod | 30 + contrib/registry/consul/go.sum | 230 + contrib/registry/consul/registry.go | 297 + contrib/registry/consul/registry_test.go | 1029 ++++ contrib/registry/consul/service.go | 41 + contrib/registry/consul/watcher.go | 44 + contrib/registry/discovery/README.md | 93 + contrib/registry/discovery/discovery.go | 451 ++ .../registry/discovery/discovery_helper.go | 193 + contrib/registry/discovery/go.mod | 16 + contrib/registry/discovery/go.sum | 8 + contrib/registry/discovery/impl_discover.go | 88 + contrib/registry/discovery/impl_registrar.go | 123 + contrib/registry/etcd/go.mod | 29 + contrib/registry/etcd/go.sum | 100 + contrib/registry/etcd/registry.go | 242 + contrib/registry/etcd/registry_test.go | 152 + contrib/registry/etcd/service.go | 20 + contrib/registry/etcd/watcher.go | 95 + contrib/registry/eureka/client.go | 366 ++ contrib/registry/eureka/eureka.go | 137 + contrib/registry/eureka/go.mod | 7 + contrib/registry/eureka/go.sum | 0 contrib/registry/eureka/register.go | 148 + contrib/registry/eureka/register_test.go | 114 + contrib/registry/eureka/watcher.go | 60 + contrib/registry/kubernetes/go.mod | 64 + contrib/registry/kubernetes/go.sum | 130 + contrib/registry/kubernetes/registry.go | 428 ++ contrib/registry/kubernetes/registry_test.go | 185 + contrib/registry/nacos/README.md | 88 + contrib/registry/nacos/fake_client_test.go | 174 + contrib/registry/nacos/go.mod | 60 + contrib/registry/nacos/go.sum | 391 ++ contrib/registry/nacos/mock_nacos_test.go | 225 + contrib/registry/nacos/registry.go | 217 + contrib/registry/nacos/registry_test.go | 446 ++ contrib/registry/nacos/util.go | 17 + contrib/registry/nacos/watcher.go | 106 + contrib/registry/polaris/go.mod | 43 + contrib/registry/polaris/go.sum | 1010 ++++ contrib/registry/polaris/registry.go | 434 ++ contrib/registry/servicecomb/README.md | 67 + contrib/registry/servicecomb/go.mod | 25 + contrib/registry/servicecomb/go.sum | 83 + contrib/registry/servicecomb/registry.go | 175 + contrib/registry/servicecomb/registry_test.go | 158 + contrib/registry/servicecomb/watcher.go | 64 + contrib/registry/zookeeper/go.mod | 11 + contrib/registry/zookeeper/go.sum | 4 + contrib/registry/zookeeper/register.go | 181 + contrib/registry/zookeeper/register_test.go | 420 ++ contrib/registry/zookeeper/service.go | 16 + contrib/registry/zookeeper/watcher.go | 111 + contrib/transport/mcp/README.md | 52 + contrib/transport/mcp/go.mod | 23 + contrib/transport/mcp/go.sum | 43 + contrib/transport/mcp/server.go | 117 + contrib/transport/mcp/server_test.go | 23 + docs/README.md | 0 docs/design/kratos-v2.md | 14 + docs/images/alipay.png | Bin 0 -> 42462 bytes docs/images/kratos-large.png | Bin 0 -> 5600 bytes docs/images/kratos.png | Bin 0 -> 5895 bytes docs/images/wechat.png | Bin 0 -> 4268298 bytes docs/migration/v2-to-v3.md | 183 + docs/migration/v2-to-v3_zh.md | 183 + encoding/README.md | 7 + encoding/encoding.go | 42 + encoding/encoding_test.go | 87 + encoding/form/form.go | 87 + encoding/form/form_test.go | 296 + encoding/form/proto_decode.go | 381 ++ encoding/form/proto_decode_test.go | 348 ++ encoding/form/proto_encode.go | 218 + encoding/form/proto_encode_test.go | 112 + encoding/form/well_known_types.go | 94 + encoding/form/well_known_types_test.go | 95 + encoding/json/json.go | 42 + encoding/json/json_test.go | 142 + encoding/proto/proto.go | 51 + encoding/proto/proto_test.go | 104 + encoding/protojson/protojson.go | 54 + encoding/protojson/protojson_test.go | 58 + encoding/xml/xml.go | 29 + encoding/xml/xml_test.go | 117 + encoding/yaml/yaml.go | 29 + encoding/yaml/yaml_test.go | 104 + errors/errors.go | 152 + errors/errors.pb.go | 194 + errors/errors.proto | 25 + errors/errors_test.go | 149 + errors/types.go | 112 + errors/types_test.go | 38 + errors/wrap.go | 68 + errors/wrap_test.go | 27 + go.mod | 27 + go.sum | 80 + hack/.lintcheck_failures | 1 + hack/.test_ignored_files | 8 + hack/resolve-modules.sh | 38 + hack/tools.sh | 125 + hack/util.sh | 30 + internal/README.md | 1 + internal/circuitbreaker/circuitbreaker.go | 13 + internal/circuitbreaker/sre.go | 184 + internal/circuitbreaker/sre_test.go | 72 + internal/context/context.go | 115 + internal/context/context_test.go | 280 + internal/endpoint/endpoint.go | 34 + internal/endpoint/endpoint_test.go | 126 + internal/group/example_test.go | 42 + internal/group/group.go | 67 + internal/group/group_test.go | 79 + internal/host/host.go | 91 + internal/host/host_test.go | 149 + internal/httputil/http.go | 34 + internal/httputil/http_test.go | 47 + internal/matcher/middleware.go | 62 + internal/matcher/middleware_test.go | 62 + internal/ratelimit/bbr.go | 359 ++ internal/ratelimit/bbr_test.go | 44 + internal/ratelimit/ratelimit.go | 19 + internal/subset/subset.go | 122 + internal/subset/subset_test.go | 30 + internal/testdata/binding/generate.go | 3 + internal/testdata/binding/test.pb.go | 291 + internal/testdata/binding/test.proto | 23 + internal/testdata/complex/complex.pb.go | 586 ++ internal/testdata/complex/complex.proto | 54 + internal/testdata/complex/generate.go | 3 + internal/testdata/encoding/test.pb.go | 258 + internal/testdata/encoding/test.proto | 19 + internal/testdata/helloworld/generate.go | 3 + internal/testdata/helloworld/helloworld.pb.go | 227 + internal/testdata/helloworld/helloworld.proto | 29 + .../testdata/helloworld/helloworld_grpc.pb.go | 178 + .../testdata/helloworld/helloworld_http.pb.go | 77 + log/README.md | 89 + log/builder.go | 129 + log/builder_test.go | 124 + log/context.go | 98 + log/context_test.go | 63 + log/filter.go | 151 + log/filter_test.go | 96 + log/handler.go | 15 + log/level.go | 43 + log/level_test.go | 39 + log/log.go | 119 + log/log_benchmark_test.go | 66 + log/log_test.go | 246 + metadata/metadata.go | 126 + metadata/metadata_test.go | 435 ++ middleware/circuitbreaker/circuitbreaker.go | 68 + .../circuitbreaker/circuitbreaker_test.go | 133 + middleware/logging/logging.go | 137 + middleware/logging/logging_test.go | 211 + middleware/metadata/metadata.go | 116 + middleware/metadata/metadata_test.go | 179 + middleware/middleware.go | 21 + middleware/middleware_test.go | 61 + middleware/ratelimit/ratelimit.go | 59 + middleware/ratelimit/ratelimit_test.go | 62 + middleware/recovery/recovery.go | 79 + middleware/recovery/recovery_test.go | 42 + middleware/selector/selector.go | 143 + middleware/selector/selector_test.go | 356 ++ middleware/validate/validate.go | 59 + middleware/validate/validate_test.go | 43 + options.go | 129 + options_test.go | 199 + registry/README.md | 37 + registry/registry.go | 95 + selector/balancer.go | 38 + selector/default_node.go | 68 + selector/default_selector.go | 86 + selector/filter.go | 6 + selector/filter/version.go | 20 + selector/filter/version_test.go | 42 + selector/global.go | 21 + selector/node/direct/direct.go | 56 + selector/node/direct/direct_test.go | 70 + selector/node/ewma/node.go | 191 + selector/node/ewma/node_test.go | 149 + selector/options.go | 16 + selector/p2c/p2c.go | 100 + selector/p2c/p2c_test.go | 122 + selector/peer.go | 25 + selector/peer_test.go | 24 + selector/random/random.go | 61 + selector/random/random_test.go | 71 + selector/selector.go | 74 + selector/selector_test.go | 291 + selector/wrr/wrr.go | 127 + selector/wrr/wrr_test.go | 331 ++ third_party/README.md | 1 + third_party/buf.yaml | 12 + third_party/buf/validate/README.md | 3 + third_party/buf/validate/validate.proto | 4902 +++++++++++++++++ third_party/errors/errors.proto | 18 + third_party/google/api/annotations.proto | 31 + third_party/google/api/client.proto | 101 + third_party/google/api/field_behavior.proto | 80 + third_party/google/api/http.proto | 375 ++ third_party/google/api/httpbody.proto | 77 + third_party/google/protobuf/descriptor.proto | 911 +++ third_party/validate/README.md | 3 + third_party/validate/validate.proto | 863 +++ transport/grpc/balancer.go | 105 + transport/grpc/balancer_test.go | 32 + transport/grpc/client.go | 316 ++ transport/grpc/client_test.go | 167 + transport/grpc/codec.go | 40 + transport/grpc/codec_test.go | 34 + transport/grpc/interceptor.go | 148 + transport/grpc/resolver/direct/builder.go | 42 + .../grpc/resolver/direct/builder_test.go | 54 + transport/grpc/resolver/direct/resolver.go | 15 + .../grpc/resolver/direct/resolver_test.go | 1 + transport/grpc/resolver/discovery/builder.go | 112 + .../grpc/resolver/discovery/builder_test.go | 97 + transport/grpc/resolver/discovery/resolver.go | 113 + .../grpc/resolver/discovery/resolver_test.go | 127 + transport/grpc/server.go | 267 + transport/grpc/server_test.go | 499 ++ transport/grpc/transport.go | 84 + transport/grpc/transport_test.go | 70 + transport/http/binding.go | 27 + transport/http/binding_test.go | 136 + transport/http/calloption.go | 141 + transport/http/calloption_test.go | 128 + transport/http/client.go | 401 ++ transport/http/client_test.go | 471 ++ transport/http/codec.go | 218 + transport/http/codec_go1.20.go | 9 + transport/http/codec_test.go | 328 ++ transport/http/context.go | 186 + transport/http/context_test.go | 251 + transport/http/filter.go | 16 + transport/http/path.go | 93 + transport/http/path_test.go | 233 + transport/http/pprof/pprof.go | 17 + transport/http/redirect.go | 22 + transport/http/redirect_test.go | 24 + transport/http/resolver.go | 154 + transport/http/resolver_test.go | 203 + transport/http/router.go | 104 + transport/http/router_test.go | 269 + transport/http/server.go | 368 ++ transport/http/server_test.go | 488 ++ transport/http/status/status.go | 114 + transport/http/status/status_test.go | 72 + transport/http/stream.go | 895 +++ transport/http/stream_test.go | 686 +++ transport/http/transport.go | 151 + transport/http/transport_test.go | 251 + transport/transport.go | 96 + transport/transport_test.go | 87 + version.go | 4 + 466 files changed, 63123 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/proposal.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/semantic.yml create mode 100644 .github/stable.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/comment-check.yml create mode 100644 .github/workflows/gitee-sync.yml create mode 100644 .github/workflows/go.yml create mode 100644 .github/workflows/issue-translator.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 README_zh.md create mode 100644 ROADMAP.md create mode 100644 SECURITY.md create mode 100644 app.go create mode 100644 app_test.go create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 cmd/kratos/go.mod create mode 100644 cmd/kratos/go.sum create mode 100644 cmd/kratos/internal/base/install.go create mode 100644 cmd/kratos/internal/base/mod.go create mode 100644 cmd/kratos/internal/base/mod_test.go create mode 100644 cmd/kratos/internal/base/path.go create mode 100644 cmd/kratos/internal/base/repo.go create mode 100644 cmd/kratos/internal/base/repo_test.go create mode 100644 cmd/kratos/internal/base/vcs_url.go create mode 100644 cmd/kratos/internal/base/vcs_url_test.go create mode 100644 cmd/kratos/internal/change/change.go create mode 100644 cmd/kratos/internal/change/get.go create mode 100644 cmd/kratos/internal/change/get_test.go create mode 100644 cmd/kratos/internal/project/add.go create mode 100644 cmd/kratos/internal/project/new.go create mode 100644 cmd/kratos/internal/project/project.go create mode 100644 cmd/kratos/internal/project/project_linux_test.go create mode 100644 cmd/kratos/internal/project/project_test.go create mode 100644 cmd/kratos/internal/project/project_windows_test.go create mode 100644 cmd/kratos/internal/proto/add/add.go create mode 100644 cmd/kratos/internal/proto/add/add_test.go create mode 100644 cmd/kratos/internal/proto/add/proto.go create mode 100644 cmd/kratos/internal/proto/add/template.go create mode 100644 cmd/kratos/internal/proto/client/client.go create mode 100644 cmd/kratos/internal/proto/proto.go create mode 100644 cmd/kratos/internal/proto/server/server.go create mode 100644 cmd/kratos/internal/proto/server/server_test.go create mode 100644 cmd/kratos/internal/proto/server/template.go create mode 100644 cmd/kratos/internal/run/run.go create mode 100644 cmd/kratos/internal/upgrade/upgrade.go create mode 100644 cmd/kratos/main.go create mode 100644 cmd/kratos/version.go create mode 100644 cmd/protoc-gen-go-errors/buf.gen.yaml create mode 100644 cmd/protoc-gen-go-errors/buf.lock create mode 100644 cmd/protoc-gen-go-errors/buf.yaml create mode 100644 cmd/protoc-gen-go-errors/errors.go create mode 100644 cmd/protoc-gen-go-errors/errors/errors.pb.go create mode 100644 cmd/protoc-gen-go-errors/errors/errors.proto create mode 100644 cmd/protoc-gen-go-errors/errorsTemplate.tpl create mode 100644 cmd/protoc-gen-go-errors/errors_test.go create mode 100644 cmd/protoc-gen-go-errors/go.mod create mode 100644 cmd/protoc-gen-go-errors/go.sum create mode 100644 cmd/protoc-gen-go-errors/main.go create mode 100644 cmd/protoc-gen-go-errors/template.go create mode 100644 cmd/protoc-gen-go-errors/version.go create mode 100644 cmd/protoc-gen-go-http/.gitignore create mode 100644 cmd/protoc-gen-go-http/go.mod create mode 100644 cmd/protoc-gen-go-http/go.sum create mode 100644 cmd/protoc-gen-go-http/http.go create mode 100644 cmd/protoc-gen-go-http/httpTemplate.tpl create mode 100644 cmd/protoc-gen-go-http/http_test.go create mode 100644 cmd/protoc-gen-go-http/main.go create mode 100644 cmd/protoc-gen-go-http/template.go create mode 100644 cmd/protoc-gen-go-http/version.go create mode 100644 codecov.yml create mode 100644 config/README.md create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/env/env.go create mode 100644 config/env/env_test.go create mode 100644 config/env/watcher.go create mode 100644 config/env/watcher_test.go create mode 100644 config/file/file.go create mode 100644 config/file/file_test.go create mode 100644 config/file/format.go create mode 100644 config/file/format_test.go create mode 100644 config/file/watcher.go create mode 100644 config/merge.go create mode 100644 config/merge_test.go create mode 100644 config/options.go create mode 100644 config/options_test.go create mode 100644 config/reader.go create mode 100644 config/reader_test.go create mode 100644 config/source.go create mode 100644 config/value.go create mode 100644 config/value_test.go create mode 100644 contrib/config/apollo/README.md create mode 100644 contrib/config/apollo/apollo.go create mode 100644 contrib/config/apollo/apollo_test.go create mode 100644 contrib/config/apollo/go.mod create mode 100644 contrib/config/apollo/go.sum create mode 100644 contrib/config/apollo/parser.go create mode 100644 contrib/config/apollo/watcher.go create mode 100644 contrib/config/apollo/watcher_test.go create mode 100644 contrib/config/consul/README.md create mode 100644 contrib/config/consul/config.go create mode 100644 contrib/config/consul/config_test.go create mode 100644 contrib/config/consul/go.mod create mode 100644 contrib/config/consul/go.sum create mode 100644 contrib/config/consul/watcher.go create mode 100644 contrib/config/etcd/README.md create mode 100644 contrib/config/etcd/config.go create mode 100644 contrib/config/etcd/config_test.go create mode 100644 contrib/config/etcd/go.mod create mode 100644 contrib/config/etcd/go.sum create mode 100644 contrib/config/etcd/watcher.go create mode 100644 contrib/config/kubernetes/README.md create mode 100644 contrib/config/kubernetes/config.go create mode 100644 contrib/config/kubernetes/config_test.go create mode 100644 contrib/config/kubernetes/go.mod create mode 100644 contrib/config/kubernetes/go.sum create mode 100644 contrib/config/kubernetes/watcher.go create mode 100644 contrib/config/kubernetes/watcher_test.go create mode 100644 contrib/config/nacos/README.md create mode 100644 contrib/config/nacos/config.go create mode 100644 contrib/config/nacos/config_test.go create mode 100644 contrib/config/nacos/go.mod create mode 100644 contrib/config/nacos/go.sum create mode 100644 contrib/config/nacos/watcher.go create mode 100644 contrib/config/polaris/README.md create mode 100644 contrib/config/polaris/config.go create mode 100644 contrib/config/polaris/go.mod create mode 100644 contrib/config/polaris/go.sum create mode 100644 contrib/config/polaris/polaris.yaml create mode 100644 contrib/config/polaris/watcher.go create mode 100644 contrib/encoding/json/go.mod create mode 100644 contrib/encoding/json/go.sum create mode 100644 contrib/encoding/json/json.go create mode 100644 contrib/encoding/json/json_test.go create mode 100644 contrib/encoding/msgpack/go.mod create mode 100644 contrib/encoding/msgpack/go.sum create mode 100644 contrib/encoding/msgpack/msgpack.go create mode 100644 contrib/encoding/msgpack/msgpack_test.go create mode 100644 contrib/errortracker/sentry/README.md create mode 100644 contrib/errortracker/sentry/go.mod create mode 100644 contrib/errortracker/sentry/go.sum create mode 100644 contrib/errortracker/sentry/sentry.go create mode 100644 contrib/errortracker/sentry/sentry_test.go create mode 100644 contrib/middleware/jwt/go.mod create mode 100644 contrib/middleware/jwt/go.sum create mode 100644 contrib/middleware/jwt/jwt.go create mode 100644 contrib/middleware/jwt/jwt_test.go create mode 100644 contrib/middleware/validate/README.md create mode 100644 contrib/middleware/validate/go.mod create mode 100644 contrib/middleware/validate/go.sum create mode 100644 contrib/middleware/validate/internal/testdata/generate.go create mode 100644 contrib/middleware/validate/internal/testdata/test.pb.go create mode 100644 contrib/middleware/validate/internal/testdata/test.pb.validate.go create mode 100644 contrib/middleware/validate/internal/testdata/test.proto create mode 100644 contrib/middleware/validate/validate.go create mode 100644 contrib/middleware/validate/validate_test.go create mode 100644 contrib/opensergo/README.md create mode 100644 contrib/opensergo/go.mod create mode 100644 contrib/opensergo/go.sum create mode 100644 contrib/opensergo/opensergo.go create mode 100644 contrib/opensergo/opensergo_test.go create mode 100644 contrib/otel/README.md create mode 100644 contrib/otel/go.mod create mode 100644 contrib/otel/go.sum create mode 100644 contrib/otel/log/log.go create mode 100644 contrib/otel/log/log_test.go create mode 100644 contrib/otel/metrics/metrics.go create mode 100644 contrib/otel/metrics/metrics_test.go create mode 100644 contrib/otel/metrics/otel.go create mode 100644 contrib/otel/tracing/metadata.go create mode 100644 contrib/otel/tracing/metadata_test.go create mode 100644 contrib/otel/tracing/span.go create mode 100644 contrib/otel/tracing/span_test.go create mode 100644 contrib/otel/tracing/statshandler.go create mode 100644 contrib/otel/tracing/statshandler_test.go create mode 100644 contrib/otel/tracing/tracer.go create mode 100644 contrib/otel/tracing/tracer_test.go create mode 100644 contrib/otel/tracing/tracing.go create mode 100644 contrib/otel/tracing/tracing_test.go create mode 100644 contrib/polaris/config.go create mode 100644 contrib/polaris/config_test.go create mode 100644 contrib/polaris/go.mod create mode 100644 contrib/polaris/go.sum create mode 100644 contrib/polaris/limiter.go create mode 100644 contrib/polaris/polaris.go create mode 100644 contrib/polaris/ratelimit.go create mode 100644 contrib/polaris/registry.go create mode 100644 contrib/polaris/registry_test.go create mode 100644 contrib/polaris/router.go create mode 100644 contrib/polaris/router_test.go create mode 100644 contrib/registry/consul/client.go create mode 100644 contrib/registry/consul/go.mod create mode 100644 contrib/registry/consul/go.sum create mode 100644 contrib/registry/consul/registry.go create mode 100644 contrib/registry/consul/registry_test.go create mode 100644 contrib/registry/consul/service.go create mode 100644 contrib/registry/consul/watcher.go create mode 100644 contrib/registry/discovery/README.md create mode 100644 contrib/registry/discovery/discovery.go create mode 100644 contrib/registry/discovery/discovery_helper.go create mode 100644 contrib/registry/discovery/go.mod create mode 100644 contrib/registry/discovery/go.sum create mode 100644 contrib/registry/discovery/impl_discover.go create mode 100644 contrib/registry/discovery/impl_registrar.go create mode 100644 contrib/registry/etcd/go.mod create mode 100644 contrib/registry/etcd/go.sum create mode 100644 contrib/registry/etcd/registry.go create mode 100644 contrib/registry/etcd/registry_test.go create mode 100644 contrib/registry/etcd/service.go create mode 100644 contrib/registry/etcd/watcher.go create mode 100644 contrib/registry/eureka/client.go create mode 100644 contrib/registry/eureka/eureka.go create mode 100644 contrib/registry/eureka/go.mod create mode 100644 contrib/registry/eureka/go.sum create mode 100644 contrib/registry/eureka/register.go create mode 100644 contrib/registry/eureka/register_test.go create mode 100644 contrib/registry/eureka/watcher.go create mode 100644 contrib/registry/kubernetes/go.mod create mode 100644 contrib/registry/kubernetes/go.sum create mode 100644 contrib/registry/kubernetes/registry.go create mode 100644 contrib/registry/kubernetes/registry_test.go create mode 100644 contrib/registry/nacos/README.md create mode 100644 contrib/registry/nacos/fake_client_test.go create mode 100644 contrib/registry/nacos/go.mod create mode 100644 contrib/registry/nacos/go.sum create mode 100644 contrib/registry/nacos/mock_nacos_test.go create mode 100644 contrib/registry/nacos/registry.go create mode 100644 contrib/registry/nacos/registry_test.go create mode 100644 contrib/registry/nacos/util.go create mode 100644 contrib/registry/nacos/watcher.go create mode 100644 contrib/registry/polaris/go.mod create mode 100644 contrib/registry/polaris/go.sum create mode 100644 contrib/registry/polaris/registry.go create mode 100644 contrib/registry/servicecomb/README.md create mode 100644 contrib/registry/servicecomb/go.mod create mode 100644 contrib/registry/servicecomb/go.sum create mode 100644 contrib/registry/servicecomb/registry.go create mode 100644 contrib/registry/servicecomb/registry_test.go create mode 100644 contrib/registry/servicecomb/watcher.go create mode 100644 contrib/registry/zookeeper/go.mod create mode 100644 contrib/registry/zookeeper/go.sum create mode 100644 contrib/registry/zookeeper/register.go create mode 100644 contrib/registry/zookeeper/register_test.go create mode 100644 contrib/registry/zookeeper/service.go create mode 100644 contrib/registry/zookeeper/watcher.go create mode 100644 contrib/transport/mcp/README.md create mode 100644 contrib/transport/mcp/go.mod create mode 100644 contrib/transport/mcp/go.sum create mode 100644 contrib/transport/mcp/server.go create mode 100644 contrib/transport/mcp/server_test.go create mode 100644 docs/README.md create mode 100644 docs/design/kratos-v2.md create mode 100644 docs/images/alipay.png create mode 100644 docs/images/kratos-large.png create mode 100644 docs/images/kratos.png create mode 100644 docs/images/wechat.png create mode 100644 docs/migration/v2-to-v3.md create mode 100644 docs/migration/v2-to-v3_zh.md create mode 100644 encoding/README.md create mode 100644 encoding/encoding.go create mode 100644 encoding/encoding_test.go create mode 100644 encoding/form/form.go create mode 100644 encoding/form/form_test.go create mode 100644 encoding/form/proto_decode.go create mode 100644 encoding/form/proto_decode_test.go create mode 100644 encoding/form/proto_encode.go create mode 100644 encoding/form/proto_encode_test.go create mode 100644 encoding/form/well_known_types.go create mode 100644 encoding/form/well_known_types_test.go create mode 100644 encoding/json/json.go create mode 100644 encoding/json/json_test.go create mode 100644 encoding/proto/proto.go create mode 100644 encoding/proto/proto_test.go create mode 100644 encoding/protojson/protojson.go create mode 100644 encoding/protojson/protojson_test.go create mode 100644 encoding/xml/xml.go create mode 100644 encoding/xml/xml_test.go create mode 100644 encoding/yaml/yaml.go create mode 100644 encoding/yaml/yaml_test.go create mode 100644 errors/errors.go create mode 100644 errors/errors.pb.go create mode 100644 errors/errors.proto create mode 100644 errors/errors_test.go create mode 100644 errors/types.go create mode 100644 errors/types_test.go create mode 100644 errors/wrap.go create mode 100644 errors/wrap_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/.lintcheck_failures create mode 100644 hack/.test_ignored_files create mode 100755 hack/resolve-modules.sh create mode 100755 hack/tools.sh create mode 100755 hack/util.sh create mode 100644 internal/README.md create mode 100644 internal/circuitbreaker/circuitbreaker.go create mode 100644 internal/circuitbreaker/sre.go create mode 100644 internal/circuitbreaker/sre_test.go create mode 100644 internal/context/context.go create mode 100644 internal/context/context_test.go create mode 100644 internal/endpoint/endpoint.go create mode 100644 internal/endpoint/endpoint_test.go create mode 100644 internal/group/example_test.go create mode 100644 internal/group/group.go create mode 100644 internal/group/group_test.go create mode 100644 internal/host/host.go create mode 100644 internal/host/host_test.go create mode 100644 internal/httputil/http.go create mode 100644 internal/httputil/http_test.go create mode 100644 internal/matcher/middleware.go create mode 100644 internal/matcher/middleware_test.go create mode 100644 internal/ratelimit/bbr.go create mode 100644 internal/ratelimit/bbr_test.go create mode 100644 internal/ratelimit/ratelimit.go create mode 100644 internal/subset/subset.go create mode 100644 internal/subset/subset_test.go create mode 100644 internal/testdata/binding/generate.go create mode 100644 internal/testdata/binding/test.pb.go create mode 100644 internal/testdata/binding/test.proto create mode 100644 internal/testdata/complex/complex.pb.go create mode 100644 internal/testdata/complex/complex.proto create mode 100644 internal/testdata/complex/generate.go create mode 100644 internal/testdata/encoding/test.pb.go create mode 100644 internal/testdata/encoding/test.proto create mode 100644 internal/testdata/helloworld/generate.go create mode 100644 internal/testdata/helloworld/helloworld.pb.go create mode 100644 internal/testdata/helloworld/helloworld.proto create mode 100644 internal/testdata/helloworld/helloworld_grpc.pb.go create mode 100644 internal/testdata/helloworld/helloworld_http.pb.go create mode 100644 log/README.md create mode 100644 log/builder.go create mode 100644 log/builder_test.go create mode 100644 log/context.go create mode 100644 log/context_test.go create mode 100644 log/filter.go create mode 100644 log/filter_test.go create mode 100644 log/handler.go create mode 100644 log/level.go create mode 100644 log/level_test.go create mode 100644 log/log.go create mode 100644 log/log_benchmark_test.go create mode 100644 log/log_test.go create mode 100644 metadata/metadata.go create mode 100644 metadata/metadata_test.go create mode 100644 middleware/circuitbreaker/circuitbreaker.go create mode 100644 middleware/circuitbreaker/circuitbreaker_test.go create mode 100644 middleware/logging/logging.go create mode 100644 middleware/logging/logging_test.go create mode 100644 middleware/metadata/metadata.go create mode 100644 middleware/metadata/metadata_test.go create mode 100644 middleware/middleware.go create mode 100644 middleware/middleware_test.go create mode 100644 middleware/ratelimit/ratelimit.go create mode 100644 middleware/ratelimit/ratelimit_test.go create mode 100644 middleware/recovery/recovery.go create mode 100644 middleware/recovery/recovery_test.go create mode 100644 middleware/selector/selector.go create mode 100644 middleware/selector/selector_test.go create mode 100644 middleware/validate/validate.go create mode 100644 middleware/validate/validate_test.go create mode 100644 options.go create mode 100644 options_test.go create mode 100644 registry/README.md create mode 100644 registry/registry.go create mode 100644 selector/balancer.go create mode 100644 selector/default_node.go create mode 100644 selector/default_selector.go create mode 100644 selector/filter.go create mode 100644 selector/filter/version.go create mode 100644 selector/filter/version_test.go create mode 100644 selector/global.go create mode 100644 selector/node/direct/direct.go create mode 100644 selector/node/direct/direct_test.go create mode 100644 selector/node/ewma/node.go create mode 100644 selector/node/ewma/node_test.go create mode 100644 selector/options.go create mode 100644 selector/p2c/p2c.go create mode 100644 selector/p2c/p2c_test.go create mode 100644 selector/peer.go create mode 100644 selector/peer_test.go create mode 100644 selector/random/random.go create mode 100644 selector/random/random_test.go create mode 100644 selector/selector.go create mode 100644 selector/selector_test.go create mode 100644 selector/wrr/wrr.go create mode 100644 selector/wrr/wrr_test.go create mode 100644 third_party/README.md create mode 100644 third_party/buf.yaml create mode 100644 third_party/buf/validate/README.md create mode 100644 third_party/buf/validate/validate.proto create mode 100644 third_party/errors/errors.proto create mode 100644 third_party/google/api/annotations.proto create mode 100644 third_party/google/api/client.proto create mode 100644 third_party/google/api/field_behavior.proto create mode 100644 third_party/google/api/http.proto create mode 100644 third_party/google/api/httpbody.proto create mode 100644 third_party/google/protobuf/descriptor.proto create mode 100644 third_party/validate/README.md create mode 100644 third_party/validate/validate.proto create mode 100644 transport/grpc/balancer.go create mode 100644 transport/grpc/balancer_test.go create mode 100644 transport/grpc/client.go create mode 100644 transport/grpc/client_test.go create mode 100644 transport/grpc/codec.go create mode 100644 transport/grpc/codec_test.go create mode 100644 transport/grpc/interceptor.go create mode 100644 transport/grpc/resolver/direct/builder.go create mode 100644 transport/grpc/resolver/direct/builder_test.go create mode 100644 transport/grpc/resolver/direct/resolver.go create mode 100644 transport/grpc/resolver/direct/resolver_test.go create mode 100644 transport/grpc/resolver/discovery/builder.go create mode 100644 transport/grpc/resolver/discovery/builder_test.go create mode 100644 transport/grpc/resolver/discovery/resolver.go create mode 100644 transport/grpc/resolver/discovery/resolver_test.go create mode 100644 transport/grpc/server.go create mode 100644 transport/grpc/server_test.go create mode 100644 transport/grpc/transport.go create mode 100644 transport/grpc/transport_test.go create mode 100644 transport/http/binding.go create mode 100644 transport/http/binding_test.go create mode 100644 transport/http/calloption.go create mode 100644 transport/http/calloption_test.go create mode 100644 transport/http/client.go create mode 100644 transport/http/client_test.go create mode 100644 transport/http/codec.go create mode 100644 transport/http/codec_go1.20.go create mode 100644 transport/http/codec_test.go create mode 100644 transport/http/context.go create mode 100644 transport/http/context_test.go create mode 100644 transport/http/filter.go create mode 100644 transport/http/path.go create mode 100644 transport/http/path_test.go create mode 100644 transport/http/pprof/pprof.go create mode 100644 transport/http/redirect.go create mode 100644 transport/http/redirect_test.go create mode 100644 transport/http/resolver.go create mode 100644 transport/http/resolver_test.go create mode 100644 transport/http/router.go create mode 100644 transport/http/router_test.go create mode 100644 transport/http/server.go create mode 100644 transport/http/server_test.go create mode 100644 transport/http/status/status.go create mode 100644 transport/http/status/status_test.go create mode 100644 transport/http/stream.go create mode 100644 transport/http/stream_test.go create mode 100644 transport/http/transport.go create mode 100644 transport/http/transport_test.go create mode 100644 transport/transport.go create mode 100644 transport/transport_test.go create mode 100644 version.go diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3aecfaf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +open_collective: go-kratos +github: [go-kratos] diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..fc37f82 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,26 @@ +--- +name: "\U0001F41B Bug Report" +about: Report something that's broken. Please ensure your kratos version is still supported. +title: '' +labels: bug +assignees: '' +--- + + + +#### What happened: + +#### What you expected to happen: + +#### How to reproduce it (as minimally and precisely as possible): + +#### Anything else we need to know?: + +#### Environment: +- Kratos version (use `kratos -v`): +- Go version (use `go version`): +- OS (e.g: `cat /etc/os-release`): +- Others: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a0450f2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation issue + url: https://go-kratos.dev/docs/ + about: For documentation issues, open a pull request at the go-kratos/go-kratos.dev repository diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..d193e70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,55 @@ +--- +name: "\U0001F4A1 Feature Request" +about: For ideas or feature requests, start a new discussion. +title: "[Feature]" +labels: feature +assignees: '' +--- + +Please see the FAQ in our main README.md before submitting your issue. + + + +### What problem is the feature used to solve? + + +### Requirements description of the feature + + +### References + diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 0000000..67e0982 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,75 @@ +--- +name: "\U0001F9F1 Proposal Request" +about: Implementation draft of feature. +title: "[Proposal]" +labels: proposal +assignees: '' +--- + +Please see the FAQ in our main README.md before submitting your issue. + + + +### Proposal description + + +### Implementation mode + + +### Usage demonstration + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..fd1a2ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,10 @@ +--- +name: "\U0001F680 Question" +about: Ask a question about Kratos. +title: "[Question]" +labels: question +assignees: '' + +--- + +Please see the FAQ in our main README.md before submitting your issue. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..64284b9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..524b188 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,37 @@ + + + + +#### Description (what this PR does / why we need it): + + +#### Which issue(s) this PR fixes (resolves / be part of): + + +#### Other special notes for the reviewers: + diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 0000000..68c2b04 --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,33 @@ +titleOnly: true +commitOnly: false +titleAndCommits: false +scopes: + - api + - cmd + - config + - contrib + - docs + - encoding + - hack + - internal + - log + - metadata + - metrics + - middleware + - registry + - selector + - third_party + - transport +types: + - deps + - feat + - fix + - docs + - style + - refactor + - perf + - test + - build + - ci + - chore + - revert diff --git a/.github/stable.yml b/.github/stable.yml new file mode 100644 index 0000000..43688ff --- /dev/null +++ b/.github/stable.yml @@ -0,0 +1,12 @@ +daysUntilStale: 30 +daysUntilClose: 3 +exemptLabels: + - pinned + - security + - bug +staleLabel: wontfix +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +closeComment: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..bd296a2 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,22 @@ +name: "CodeQL" + +on: + push: + branches: [ main, v1.0.x ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: go + + - name: CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/comment-check.yml b/.github/workflows/comment-check.yml new file mode 100644 index 0000000..d2b48cb --- /dev/null +++ b/.github/workflows/comment-check.yml @@ -0,0 +1,155 @@ +name: Non-English Comments Check + +on: + pull_request_target: + types: [opened, synchronize, reopened] + branches: + - main + # workflow_dispatch: + +jobs: + non-english-comments-check: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + env: + # Directories to be excluded + EXCLUDE_DIRS: ".git docs tests scripts assets node_modules build" + # Files to be excluded + EXCLUDE_FILES: ".md .txt .html .css .min.js .mdx" + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + + # - name: Search for Non-English comments in the entire repository + # run: | + # set -e + # # Define the regex pattern to match Chinese characters + # pattern='[\p{Han}]' + + # # Use find to get all files in the repository + # all_files=$(find . -type f) + + # # Loop over each file in the repository + # for file in $all_files; do + # # Skip files in excluded directories + # skip_file=false + # for dir in ${EXCLUDE_DIRS}; do + # if [[ "$file" == ./$dir/* ]]; then + # skip_file=true + # break + # fi + # done + + # # Skip files matching excluded patterns + # for file_pattern in ${EXCLUDE_FILES}; do + # if [[ "$file" == *$file_pattern ]]; then + # skip_file=true + # break + # fi + # done + + # # If the file matches any exclude pattern, skip it + # if [ "$skip_file" = true ]; then + # continue + # fi + + # # Use grep to find all comments containing Non-English characters in filtered files + # grep_output=$(grep -PnH "$pattern" "$file" || true) + # if [ -n "$grep_output" ]; then + # # Insert a tab after the line number, keeping the colon between the file path and line number + # formatted_output=$(echo "$grep_output" | sed 's/^\(.*:[0-9]\+\):/\1\t/') + # echo "$formatted_output" >> non_english_comments.txt # Save to file + # fi + # done + + - name: Search for Non-English comments in PR diff files + run: | + set -e + # Define the regex pattern to match Chinese characters + pattern='[\p{Han}]' + + # Get the list of files changed in this PR compared to the base branch + changed_files=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) + + # Loop over each changed file + for file in $changed_files; do + # Skip files in excluded directories + skip_file=false + for dir in ${EXCLUDE_DIRS}; do + if [[ "$file" == ./$dir/* ]]; then + skip_file=true + break + fi + done + + # Skip files matching excluded patterns + for file_pattern in ${EXCLUDE_FILES}; do + if [[ "$file" == *$file_pattern ]]; then + skip_file=true + break + fi + done + + # If the file matches any exclude pattern, skip it + if [ "$skip_file" = true ]; then + continue + fi + + # Use grep to find all comments containing Non-English characters in filtered files + grep_output=$(grep -PnH "$pattern" "$file" || true) + if [ -n "$grep_output" ]; then + # Insert a tab after the line number, keeping the colon between the file path and line number + formatted_output=$(echo "$grep_output" | sed 's/^\(.*:[0-9]\+\):/\1\t/') + echo "$formatted_output" >> non_english_comments.txt # Save to file + fi + done + + - name: Store non-English comments in ENV + run: | + # Store the entire content of non_english_comments.txt into an environment variable + if [ -f non_english_comments.txt ]; then + NON_ENGLISH_COMMENTS=$(cat non_english_comments.txt) + echo "NON_ENGLISH_COMMENTS<> $GITHUB_ENV + echo "$NON_ENGLISH_COMMENTS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + fi + + - name: Output non-English comments if found + run: | + if [ -s non_english_comments.txt ]; then + echo "Non-English comments found in the following locations:" + cat non_english_comments.txt + exit 1 # terminate the workflow + else + echo "No Non-English comments found." + fi + + - name: Find Comment + if: failure() && github.event_name != 'workflow_dispatch' + uses: peter-evans/find-comment@v4.0.0 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "kratos-ci-bot" + body-includes: Non-English comments were found in the following locations + + - name: Comment on PR if errors found + if: failure() && github.event_name != 'workflow_dispatch' # This step runs only if the previous step fails + uses: peter-evans/create-or-update-comment@v5.0.0 + with: + token: ${{ secrets.BOT_GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + comment-id: ${{ steps.fc.outputs.comment-id }} + edit-mode: replace + body: | + ⚠️ Non-English comments were found in the following locations: + ``` + ${{ env.NON_ENGLISH_COMMENTS }} + ``` diff --git a/.github/workflows/gitee-sync.yml b/.github/workflows/gitee-sync.yml new file mode 100644 index 0000000..79d1e02 --- /dev/null +++ b/.github/workflows/gitee-sync.yml @@ -0,0 +1,27 @@ +on: + push: + branches: + - main + tags: + - "*" + +name: Sync to Gitee +jobs: + run: + name: Run + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v6 + - name: Mirror Github to Gitee + uses: Yikun/hub-mirror-action@v1.5 + with: + src: github/go-kratos + dst: gitee/go-kratos + dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} + dst_token: ${{ secrets.GITEE_TOKEN }} + account_type: org + timeout: 600 + debug: true + force_update: true + static_list: "kratos" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..2c687b0 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,92 @@ +name: Go + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + go: [1.25.x, 1.26.x] + name: build & test + runs-on: ubuntu-latest + services: + etcd: + image: gcr.io/etcd-development/etcd:v3.5.0 + ports: + - 2379:2379 + env: + ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379 + ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379 + consul: + image: hashicorp/consul:1.20 + ports: + - 8500:8500 + nacos: + image: nacos/nacos-server:v2.1.0 + env: + MODE: standalone + ports: + - "8848:8848" + - "9848:9848" + polaris: + image: polarismesh/polaris-standalone:latest + ports: + - 8090:8090 + - 8091:8091 + - 8093:8093 + steps: + - uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go }} + + - name: Setup Environment + run: | + echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV + echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + + - name: Module cache + uses: actions/cache@v5 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go + + - name: Build + run: go build ./... + env: + GOTOOLCHAIN: auto + + - name: Test + run: make test-coverage + env: + GOTOOLCHAIN: auto + + - name: Upload coverage to Codecov + run: bash <(curl -s https://codecov.io/bash) + + - name: Kratos + run: | + cd cmd/kratos + go build ./... + env: + GOTOOLCHAIN: auto + + - name: HTTP + run: | + cd cmd/protoc-gen-go-http + go build ./... + go test ./... + env: + GOTOOLCHAIN: auto diff --git a/.github/workflows/issue-translator.yml b/.github/workflows/issue-translator.yml new file mode 100644 index 0000000..9cab6d0 --- /dev/null +++ b/.github/workflows/issue-translator.yml @@ -0,0 +1,16 @@ +name: 'issue-translator' +on: + issue_comment: + types: [created] + issues: + types: [opened] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: usthe/issues-translate-action@v2.7 + with: + IS_MODIFY_TITLE: true + CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿 + BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..5987eb7 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: Lint + +on: + push: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + resolve-modules: + name: resolve module + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Checkout Repo + uses: actions/checkout@v6 + + - id: set-matrix + run: ./hack/resolve-modules.sh + + lint: + name: lint module + runs-on: ubuntu-latest + needs: resolve-modules + strategy: + matrix: ${{ fromJson(needs.resolve-modules.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + - name: Lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.12.2 + working-directory: ${{ matrix.workdir }} + skip-cache: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c3101a --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Reference https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work +go.work.sum + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# OS General +Thumbs.db +.DS_Store +.cache + +# project +*.cert +*.key +*.log +bin/ + +# Develop tools +.vscode/ +.idea/ +*.swp diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..145559c --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,71 @@ +version: "2" +run: + modules-download-mode: readonly +linters: + default: none + enable: + - bodyclose + - dogsled + - durationcheck + - errcheck + - goconst + - gocyclo + - govet + - ineffassign + - lll + - misspell + - mnd + - prealloc + - revive + - staticcheck + - unconvert + - unused + - wastedassign + - whitespace + settings: + gocyclo: + min-complexity: 50 + govet: + enable: + - shadow + lll: + line-length: 160 + misspell: + locale: US + mnd: + checks: + - case + - condition + - return + whitespace: + multi-func: true + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - goconst + path: (.+)_test\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - gofumpt + - goimports + settings: + goimports: + local-prefixes: + - github.com/go-kratos + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e3eebb7 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +iammao@vip.qq.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7603706 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,121 @@ +The kratos community wants to be helped by a wide range of developers, so you'd like to take a few minutes to read this guide before you mention the problem or pull request. + +## Reporting Bug or Fixing Bugs +We use GitHub issues to manage issues. If you want to submit , first make sure you've searched for existing issues, pull requests and read our [FAQ](https://go-kratos.dev/docs/intro/faq). + +When submitting a bug report, use the issue template we provide to clearly describe the problems encountered and how to reproduce, and if convenient it is best to provide a minimal reproduce repository. + +## Adding new features + +In order to accurately distinguish whether the needs put forward by users are the needs or reasonable needs of most users, solicit opinions from the community through the proposal process, and the proposals adopted by the community will be realized as new feature. +In order to make the proposal process as simple as possible, the process includes three stages: proposal, feature and PR, in which proposal, feature is issue and PR is the specific function implementation. +In order to facilitate the community to correctly understand the requirements of the proposal, the proposal issue needs to describe the functional requirements and relevant references or literature in detail. +When most community users agree with this proposal, they will create a feature issue associated with the proposal issue. +The feature issue needs to describe the implementation method and function demonstration in detail as a reference for the final function implementation. +After the function is implemented, a merge request will be initiated to associate the proposal issue and feature issue. +After the merge is completed, Close all issues. + +## How to submit code +If you've never submitted code on GitHub, follow these steps: + +- First, please fork items to your GitHub account +- Then create a new feature branch based on the main branch and name it features such as feature-log +- Write code +- Submit code to the far end branch +- Submit a PR request in github +- Wait for review and merge to the main branch + +**Note That when you submit a PR request, you first ensure that the code uses the correct coding specifications and that there are complete test cases, and that the information in the submission of the PR is best associated with the relevant issue to ease the workload of the auditor.** + +## Conventional Commits + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +> More: [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) + +### type + +There are the following types of commit: + +#### Main + +- **fix**: A bug fix +- **feat**: A new feature +- **deps**: Changes external dependencies +- **break**: Changes has break change + +#### Other + +- **docs**: Documentation only changes +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) +- **test**: Adding missing tests or correcting existing tests +- **chore** Daily work, examples, etc. +- **ci**: Changes to our CI configuration files and scripts + +### scope + +The following is the list of supported scopes: + +- transport +- examples +- middleware +- config +- cmd +- etc. + +### description + +The description contains a succinct description of the change + +- use the imperative, present tense: "change" not "changed" nor "changes" +- don't capitalize the first letter +- no dot (.) at the end + +### body + +The body should include the motivation for the change and contrast this with previous behavior. + +### footer + +The footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit Closes. + +### examples + +#### Only commit message +``` +fix: The log debug level should be -1 +``` + +#### Attention +``` +refactor!(transport/http): replacement underlying implementation +``` + +#### Full commit message +``` +fix(log): [BREAKING-CHANGE] unable to meet the requirement of log Library + +Explain the reason, purpose, realization method, etc. + +Close #777 +Doc change on doc/#111 +BREAKING CHANGE: + Breaks log.info api, log.log should be used instead +``` +## Release + +You can use `kratos changelog dev` to generate a change log during. + +The following is the list of supported types: + +- Breaking Change +- Dependencies +- Bug Fixes +- Others diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..684318c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 go-kratos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6276171 --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +user := $(shell whoami) +rev := $(shell git rev-parse --short HEAD) +os := $(shell uname) + +# GOBIN > GOPATH > INSTALLDIR +# Mac OS X +ifeq ($(os),Darwin) +GOBIN := $(shell echo $(GOBIN) | cut -d':' -f1) +GOPATH := $(shell echo $(GOPATH) | cut -d':' -f1) +endif + +# Linux +ifeq ($(os),Linux) +GOBIN := $(shell echo $(GOBIN) | cut -d':' -f1) +GOPATH := $(shell echo $(GOPATH) | cut -d':' -f1) +endif + +# Windows +ifneq ($(findstring MINGW,$(shell uname -s)),) +GOBIN := $(shell echo "$(GOBIN)" | sed 's|\\|/|g' | cut -d';' -f1 | sed 's|^\([A-Za-z]\):|/\1|') +GOPATH := $(shell echo "$(GOPATH)" | sed 's|\\|/|g' | cut -d';' -f1 | sed 's|^\([A-Za-z]\):|/\1|') +endif +BIN := "" + +TOOLS_SHELL="./hack/tools.sh" +# golangci-lint +LINTER := bin/golangci-lint + +# check GOBIN +ifneq ($(GOBIN),) + BIN=$(GOBIN) +else +# check GOPATH + ifneq ($(GOPATH),) + BIN=$(GOPATH)/bin + endif +endif + +$(LINTER): + curl -SL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s latest + +all: + @cd cmd/kratos && go build && cd - &> /dev/null + @cd cmd/protoc-gen-go-errors && go build && cd - &> /dev/null + @cd cmd/protoc-gen-go-http && go build && cd - &> /dev/null + +.PHONY: install +install: all +ifeq ($(user),root) +#root, install for all user + @cp ./cmd/kratos/kratos /usr/bin + @cp ./cmd/protoc-gen-go-errors/protoc-gen-go-errors /usr/bin + @cp ./cmd/protoc-gen-go-http/protoc-gen-go-http /usr/bin +else +#!root, install for current user + $(shell if [ -z '$(BIN)' ]; then read -p "Please select installdir: " REPLY; mkdir -p $${REPLY};\ + cp ./cmd/kratos/kratos $${REPLY}/;cp ./cmd/protoc-gen-go-errors/protoc-gen-go-errors $${REPLY}/;cp ./cmd/protoc-gen-go-http/protoc-gen-go-http $${REPLY}/;else mkdir -p '$(BIN)';\ + cp ./cmd/kratos/kratos '$(BIN)';cp ./cmd/protoc-gen-go-errors/protoc-gen-go-errors '$(BIN)';cp ./cmd/protoc-gen-go-http/protoc-gen-go-http '$(BIN)'; fi) +endif + @which protoc-gen-go &> /dev/null || go get google.golang.org/protobuf/cmd/protoc-gen-go + @which protoc-gen-go-grpc &> /dev/null || go get google.golang.org/grpc/cmd/protoc-gen-go-grpc + @which protoc-gen-validate &> /dev/null || go get github.com/envoyproxy/protoc-gen-validate + @echo "install finished" + +.PHONY: uninstall +uninstall: + $(shell for i in `which -a kratos | grep -v '/usr/bin/kratos' 2>/dev/null | sort | uniq`; do read -p "Press to remove $${i} (y/n): " REPLY; if [ $${REPLY} = "y" ]; then rm -f $${i}; fi; done) + $(shell for i in `which -a protoc-gen-go-grpc | grep -v '/usr/bin/protoc-gen-go-errors' 2>/dev/null | sort | uniq`; do read -p "Press to remove $${i} (y/n): " REPLY; if [ $${REPLY} = "y" ]; then rm -f $${i}; fi; done) + $(shell for i in `which -a protoc-gen-validate | grep -v '/usr/bin/protoc-gen-go-errors' 2>/dev/null | sort | uniq`; do read -p "Press to remove $${i} (y/n): " REPLY; if [ $${REPLY} = "y" ]; then rm -f $${i}; fi; done) + @echo "uninstall finished" + +.PHONY: clean +clean: + @${TOOLS_SHELL} tidy + @echo "clean finished" + +.PHONY: fix +fix: $(LINTER) + @${TOOLS_SHELL} fix + @echo "lint fix finished" + +.PHONY: test +test: + @${TOOLS_SHELL} test + @echo "go test finished" + +.PHONY: test-coverage +test-coverage: + @${TOOLS_SHELL} test_coverage + @echo "go test with coverage finished" + +.PHONY: lint +lint: $(LINTER) + @${TOOLS_SHELL} lint + @echo "lint check finished" + +.PHONY: proto +proto: + buf generate --path errors/errors.proto diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bad31c --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +

+ +

+Build Status +GoDoc +DeepWiki +codeCov +Go Report Card +License +Awesome Go +Discord +

+ +

+go-kratos%2Fkratos | Trendshift +Go Kratos - A Go framework for microservices. | Product Hunt +

+ +Translations: [English](README.md) | [简体中文](README_zh.md) + +# Kratos + +Kratos is a lightweight Go framework for building cloud-native microservices. It provides small, explicit APIs for transport, middleware, registry, configuration, logging, encoding, and code generation so applications can focus on business logic. + +## Features + +- API-first development with Protobuf and generated HTTP/gRPC code. +- Unified [transport](https://go-kratos.dev/docs/component/transport/overview) layer for [HTTP](https://go-kratos.dev/docs/component/transport/http) and [gRPC](https://go-kratos.dev/docs/component/transport/grpc). +- Composable [middleware](https://go-kratos.dev/docs/component/middleware/overview) for recovery, logging, validation, tracing, metrics, auth, and more. +- Pluggable [registry](https://go-kratos.dev/docs/component/registry), [configuration](https://go-kratos.dev/docs/component/config), and [encoding](https://go-kratos.dev/docs/component/encoding) components. +- Standard-library `log/slog` based logging with OpenTelemetry extensions in contrib packages. +- Consistent metadata, errors, validation, OpenAPI, and code-generation workflows. +- A contrib ecosystem for optional integrations such as registries, config stores, middleware, encodings, and observability. + +## Installation + +### Requirements + +- [Go](https://go.dev/dl/) 1.25 or later +- [protoc](https://github.com/protocolbuffers/protobuf) +- [protoc-gen-go](https://github.com/protocolbuffers/protobuf-go) + +### Install the CLI + +```shell +go install github.com/go-kratos/kratos/cmd/kratos/v3@latest +kratos upgrade +``` + +## Create a Service + +```shell +kratos new helloworld +cd helloworld +go mod tidy +kratos run +``` + +Visit `http://localhost:8000/helloworld/kratos` after the service starts. + +For a fuller generated service flow: + +```shell +kratos proto add api/helloworld/helloworld.proto +kratos proto client api/helloworld/helloworld.proto +kratos proto server api/helloworld/helloworld.proto -t internal/service +go generate ./... +kratos run +``` + +## Usage Example + +```go +package main + +import ( + "github.com/go-kratos/kratos/v3" + "github.com/go-kratos/kratos/v3/transport/grpc" + "github.com/go-kratos/kratos/v3/transport/http" +) + +func main() { + httpSrv := http.NewServer(http.Address(":8000")) + grpcSrv := grpc.NewServer(grpc.Address(":9000")) + + app := kratos.New( + kratos.Name("helloworld"), + kratos.Version("v1.0.0"), + kratos.Server(httpSrv, grpcSrv), + ) + if err := app.Run(); err != nil { + panic(err) + } +} +``` + +## v3 Migration + +Kratos v3 reduces core dependencies and makes previously implicit behavior explicit. Review the [v2 to v3 migration guide](docs/migration/v2-to-v3.md) before upgrading production services. + +## Further Reading + +- [Documentation](https://go-kratos.dev/docs/getting-started/start) +- [Examples](https://github.com/go-kratos/examples) +- [Project Layout](https://github.com/go-kratos/kratos-layout) +- [v2 to v3 Migration Guide](docs/migration/v2-to-v3.md) +- [Community Contribution Guide](https://go-kratos.dev/docs/community/contribution) + +## Development + +```shell +make test +make lint +``` + +## Community + +- [Documentation](https://go-kratos.dev/en) +- [WeChat Group](https://github.com/go-kratos/kratos/issues/682) +- [Discord Group](https://discord.gg/BWzJsUJ) +- [Discussions](https://github.com/go-kratos/kratos/discussions) + +## Security + +If you discover a security vulnerability in Kratos, please contact go-kratos@googlegroups.com. Security reports are handled privately before disclosure. + +## Contributors + +Thank you for contributing to Kratos. The contribution guide is available in the [Kratos documentation](https://go-kratos.dev/docs/community/contribution). + + + Kratos contributors + + +## Acknowledgments + +The following projects influenced Kratos design: + +- [go-kit/kit](https://github.com/go-kit/kit) +- [go-micro](https://github.com/asim/go-micro) +- [google/go-cloud](https://github.com/google/go-cloud) +- [go-zero](https://github.com/zeromicro/go-zero) +- [beego](https://github.com/beego/beego) + +## License + +Kratos is open-sourced software licensed under the [MIT license](./LICENSE). diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..2e6c385 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`go-kratos/kratos` +- 原始仓库:https://github.com/go-kratos/kratos +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..55be4bc --- /dev/null +++ b/README_zh.md @@ -0,0 +1,148 @@ +

+ +

+Build Status +GoDoc +DeepWiki +codeCov +Go Report Card +License +Awesome Go +Discord +

+ +

+go-kratos%2Fkratos | Trendshift +Go Kratos - A Go framework for microservices. | Product Hunt +

+ +Translations: [English](README.md) | [简体中文](README_zh.md) + +# Kratos + +Kratos 是一套轻量级 Go 微服务框架,围绕传输、Middleware、注册发现、配置、日志、编码和代码生成提供清晰的基础能力,让业务代码保持聚焦。 + +## 功能特性 + +- 以 Protobuf 为中心定义 API,并生成 HTTP/gRPC 代码。 +- 统一的 [Transport](https://go-kratos.dev/zh-cn/docs/component/transport/overview) 抽象,支持 [HTTP](https://go-kratos.dev/zh-cn/docs/component/transport/http) 和 [gRPC](https://go-kratos.dev/zh-cn/docs/component/transport/grpc)。 +- 可组合的 [Middleware](https://go-kratos.dev/zh-cn/docs/component/middleware/overview),覆盖 Recovery、Logging、Validation、Tracing、Metrics、Auth 等场景。 +- 插件化的 [Registry](https://go-kratos.dev/zh-cn/docs/component/registry)、[Config](https://go-kratos.dev/zh-cn/docs/component/config) 和 [Encoding](https://go-kratos.dev/zh-cn/docs/component/encoding) 能力。 +- 基于标准库 `log/slog` 的日志能力,OpenTelemetry 扩展由 contrib 包提供。 +- 统一的 Metadata、Errors、Validation、OpenAPI 和代码生成工作流。 +- contrib 生态提供注册中心、配置中心、Middleware、编码和可观测性等可选集成。 + +## 安装 + +### 环境要求 + +- [Go](https://go.dev/dl/) 1.25 或更高版本 +- [protoc](https://github.com/protocolbuffers/protobuf) +- [protoc-gen-go](https://github.com/protocolbuffers/protobuf-go) + +### 安装 CLI + +```shell +go install github.com/go-kratos/kratos/cmd/kratos/v3@latest +kratos upgrade +``` + +## 创建服务 + +```shell +kratos new helloworld +cd helloworld +go mod tidy +kratos run +``` + +服务启动后访问 `http://localhost:8000/helloworld/kratos`。 + +如果需要从 proto 开始生成服务代码: + +```shell +kratos proto add api/helloworld/helloworld.proto +kratos proto client api/helloworld/helloworld.proto +kratos proto server api/helloworld/helloworld.proto -t internal/service +go generate ./... +kratos run +``` + +## 使用示例 + +```go +package main + +import ( + "github.com/go-kratos/kratos/v3" + "github.com/go-kratos/kratos/v3/transport/grpc" + "github.com/go-kratos/kratos/v3/transport/http" +) + +func main() { + httpSrv := http.NewServer(http.Address(":8000")) + grpcSrv := grpc.NewServer(grpc.Address(":9000")) + + app := kratos.New( + kratos.Name("helloworld"), + kratos.Version("v1.0.0"), + kratos.Server(httpSrv, grpcSrv), + ) + if err := app.Run(); err != nil { + panic(err) + } +} +``` + +## v3 迁移 + +Kratos v3 进一步降低核心依赖,并将历史上隐式的行为显式化。升级生产服务前请先阅读 [v2 到 v3 迁移指南](docs/migration/v2-to-v3_zh.md)。 + +## 扩展阅读 + +- [文档](https://go-kratos.dev/zh-cn/docs/getting-started/start) +- [示例](https://github.com/go-kratos/examples) +- [项目模板](https://github.com/go-kratos/kratos-layout) +- [v2 到 v3 迁移指南](docs/migration/v2-to-v3_zh.md) +- [贡献指南](https://go-kratos.dev/zh-cn/docs/community/contribution) + +## 开发 + +```shell +make test +make lint +``` + +## 社区 + +- [文档站点](https://go-kratos.dev/zh-cn) +- [微信群](https://github.com/go-kratos/kratos/issues/682) +- [Discord](https://discord.gg/BWzJsUJ) +- [GitHub Discussions](https://github.com/go-kratos/kratos/discussions) +- QQ 群:716486124 + +## 安全 + +如果你发现 Kratos 存在安全漏洞,请发送邮件到 go-kratos@googlegroups.com。安全问题会在公开披露前以私密方式处理。 + +## 贡献者 + +感谢所有为 Kratos 做出贡献的开发者。贡献流程请参考 [Kratos 贡献指南](https://go-kratos.dev/zh-cn/docs/community/contribution)。 + + + Kratos contributors + + +## 致谢 + +以下项目对 Kratos 的设计有重要影响: + +- [go-kit/kit](https://github.com/go-kit/kit) +- [go-micro](https://github.com/asim/go-micro) +- [google/go-cloud](https://github.com/google/go-cloud) +- [go-zero](https://github.com/zeromicro/go-zero) +- [beego](https://github.com/beego/beego) + +## License + +Kratos 基于 [MIT license](./LICENSE) 开源。 diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..aec5ac9 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,82 @@ +# Kratos + +This document defines the roadmap for Kratos development. + +## Features +- [x] Config + - [x] Local Files + - [x] K8s ConfigMap + - [x] Consul + - [x] Etcd + - [x] Nacos +- [x] Registry + - [x] Consul + - [x] Etcd + - [x] K8s + - [x] Nacos +- [x] Encoding + - [x] JSON + - [x] Protobuf +- [x] Transport + - [x] HTTP + - [x] gRPC +- [x] Middleware + - [x] Logging + - [x] metrics + - [x] recovery + - [x] gRPC status + - [x] transport tracing + - [x] Validator + - [x] Authentication + - [x] Ratelimit + - [x] CircuitBreaker +- [x] Metrics + - [x] Prometheus + - [x] DataDog +- [x] Tracing + - [x] HTTP + - [x] TLS + - [x] Client + - [x] Service Registrar + - [ ] javascript/typescript clients + - [x] gRPC + - [x] TLS + - [x] Unary Handler + - [x] Streaming Handler +- [ ] Cache + - [ ] go-redis +- [x] Event + - [x] Pub/Sub + - [x] Kafka + - [ ] Nats +- [x] Database + - [x] Ent + - [ ] Gorm + +## Platform +- [ ] Kratos API + - [ ] Auth + - [ ] Config + - [ ] Registry + - [ ] Events +- [ ] Kratos Runtime + - [ ] Secrets + - [ ] Service-to-Service + - [ ] Publish and Subscribe + - [ ] Observability + - [ ] Controllable +- [ ] Kratos UI + - [ ] Auth + - [ ] Config + - [ ] Services + - [ ] Endpoints + - [ ] Ratelimit + - [ ] CircuitBreaker + - [ ] FaultInjection + - [ ] TrafficPolicy + +## Tools +- [x] Kratos +- [x] HTTP Generator + - [ ] API YAML +- [x] Errors Generator diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6e28dd4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +|-------------|--------------------| +| 2.0.rc1 | :white_check_mark: | +| < 2.0.beta3 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/app.go b/app.go new file mode 100644 index 0000000..5f28ea8 --- /dev/null +++ b/app.go @@ -0,0 +1,212 @@ +package kratos + +import ( + "context" + "errors" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/google/uuid" + "golang.org/x/sync/errgroup" + + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/transport" +) + +// AppInfo is application context value. +type AppInfo interface { + ID() string + Name() string + Version() string + Metadata() map[string]string + Endpoint() []string +} + +// App is an application components lifecycle manager. +type App struct { + opts options + ctx context.Context + cancel context.CancelFunc + mu sync.Mutex + instance *registry.ServiceInstance +} + +// New create an application lifecycle manager. +func New(opts ...Option) *App { + o := options{ + ctx: context.Background(), + sigs: []os.Signal{syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT}, + registrarTimeout: 10 * time.Second, + } + if id, err := uuid.NewUUID(); err == nil { + o.id = id.String() + } + for _, opt := range opts { + opt(&o) + } + if o.logger != nil { + log.SetDefault(o.logger) + } + ctx, cancel := context.WithCancel(o.ctx) + return &App{ + ctx: ctx, + cancel: cancel, + opts: o, + } +} + +// ID returns app instance id. +func (a *App) ID() string { return a.opts.id } + +// Name returns service name. +func (a *App) Name() string { return a.opts.name } + +// Version returns app version. +func (a *App) Version() string { return a.opts.version } + +// Metadata returns service metadata. +func (a *App) Metadata() map[string]string { return a.opts.metadata } + +// Endpoint returns endpoints. +func (a *App) Endpoint() []string { + if a.instance != nil { + return a.instance.Endpoints + } + return nil +} + +// Run executes all OnStart hooks registered with the application's Lifecycle. +func (a *App) Run() error { + instance, err := a.buildInstance() + if err != nil { + return err + } + a.mu.Lock() + a.instance = instance + a.mu.Unlock() + sctx := NewContext(a.ctx, a) + eg, ctx := errgroup.WithContext(sctx) + wg := sync.WaitGroup{} + + for _, fn := range a.opts.beforeStart { + if err = fn(sctx); err != nil { + return err + } + } + octx := NewContext(a.opts.ctx, a) + for _, srv := range a.opts.servers { + server := srv + eg.Go(func() error { + <-ctx.Done() // wait for stop signal + stopCtx := context.WithoutCancel(octx) + if a.opts.stopTimeout > 0 { + var cancel context.CancelFunc + stopCtx, cancel = context.WithTimeout(stopCtx, a.opts.stopTimeout) + defer cancel() + } + return server.Stop(stopCtx) + }) + wg.Add(1) + eg.Go(func() error { + wg.Done() // here is to ensure server start has begun running before register, so defer is not needed + return server.Start(octx) + }) + } + wg.Wait() + if a.opts.registrar != nil { + rctx, rcancel := context.WithTimeout(ctx, a.opts.registrarTimeout) + defer rcancel() + if err = a.opts.registrar.Register(rctx, instance); err != nil { + return err + } + } + for _, fn := range a.opts.afterStart { + if err = fn(sctx); err != nil { + return err + } + } + + c := make(chan os.Signal, 1) + signal.Notify(c, a.opts.sigs...) + eg.Go(func() error { + select { + case <-ctx.Done(): + return nil + case <-c: + return a.Stop() + } + }) + if err = eg.Wait(); err != nil && !errors.Is(err, context.Canceled) { + return err + } + err = nil + for _, fn := range a.opts.afterStop { + err = fn(sctx) + } + return err +} + +// Stop gracefully stops the application. +func (a *App) Stop() (err error) { + sctx := NewContext(a.ctx, a) + for _, fn := range a.opts.beforeStop { + err = fn(sctx) + } + + a.mu.Lock() + instance := a.instance + a.mu.Unlock() + if a.opts.registrar != nil && instance != nil { + ctx, cancel := context.WithTimeout(NewContext(a.ctx, a), a.opts.registrarTimeout) + defer cancel() + if err = a.opts.registrar.Deregister(ctx, instance); err != nil { + return err + } + } + if a.cancel != nil { + a.cancel() + } + return err +} + +func (a *App) buildInstance() (*registry.ServiceInstance, error) { + endpoints := make([]string, 0, len(a.opts.endpoints)) + for _, e := range a.opts.endpoints { + endpoints = append(endpoints, e.String()) + } + if len(endpoints) == 0 { + for _, srv := range a.opts.servers { + if r, ok := srv.(transport.Endpointer); ok { + e, err := r.Endpoint() + if err != nil { + return nil, err + } + endpoints = append(endpoints, e.String()) + } + } + } + return ®istry.ServiceInstance{ + ID: a.opts.id, + Name: a.opts.name, + Version: a.opts.version, + Metadata: a.opts.metadata, + Endpoints: endpoints, + }, nil +} + +type appKey struct{} + +// NewContext returns a new Context that carries value. +func NewContext(ctx context.Context, s AppInfo) context.Context { + return context.WithValue(ctx, appKey{}, s) +} + +// FromContext returns the Transport value stored in ctx, if any. +func FromContext(ctx context.Context) (s AppInfo, ok bool) { + s, ok = ctx.Value(appKey{}).(AppInfo) + return +} diff --git a/app_test.go b/app_test.go new file mode 100644 index 0000000..ed71d5c --- /dev/null +++ b/app_test.go @@ -0,0 +1,300 @@ +package kratos + +import ( + "context" + "errors" + "net/url" + "reflect" + "sync" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/transport/grpc" + "github.com/go-kratos/kratos/v3/transport/http" +) + +type mockRegistry struct { + lk sync.Mutex + service map[string]*registry.ServiceInstance +} + +func (r *mockRegistry) Register(_ context.Context, service *registry.ServiceInstance) error { + if service == nil || service.ID == "" { + return errors.New("no service id") + } + r.lk.Lock() + defer r.lk.Unlock() + r.service[service.ID] = service + return nil +} + +// Deregister the registration. +func (r *mockRegistry) Deregister(_ context.Context, service *registry.ServiceInstance) error { + r.lk.Lock() + defer r.lk.Unlock() + if r.service[service.ID] == nil { + return errors.New("deregister service not found") + } + delete(r.service, service.ID) + return nil +} + +func TestApp(t *testing.T) { + hs := http.NewServer() + gs := grpc.NewServer() + app := New( + Name("kratos"), + Version("v1.0.0"), + Server(hs, gs), + BeforeStart(func(_ context.Context) error { + t.Log("BeforeStart...") + return nil + }), + BeforeStop(func(_ context.Context) error { + t.Log("BeforeStop...") + return nil + }), + AfterStart(func(_ context.Context) error { + t.Log("AfterStart...") + return nil + }), + AfterStop(func(_ context.Context) error { + t.Log("AfterStop...") + return nil + }), + Registrar(&mockRegistry{service: make(map[string]*registry.ServiceInstance)}), + ) + time.AfterFunc(time.Second, func() { + _ = app.Stop() + }) + if err := app.Run(); err != nil { + t.Fatal(err) + } +} + +func TestApp_ID(t *testing.T) { + v := "123" + o := New(ID(v)) + if !reflect.DeepEqual(v, o.ID()) { + t.Fatalf("o.ID():%s is not equal to v:%s", o.ID(), v) + } +} + +func TestApp_Name(t *testing.T) { + v := "123" + o := New(Name(v)) + if !reflect.DeepEqual(v, o.Name()) { + t.Fatalf("o.Name():%s is not equal to v:%s", o.Name(), v) + } +} + +func TestApp_Version(t *testing.T) { + v := "123" + o := New(Version(v)) + if !reflect.DeepEqual(v, o.Version()) { + t.Fatalf("o.Version():%s is not equal to v:%s", o.Version(), v) + } +} + +func TestApp_Metadata(t *testing.T) { + v := map[string]string{ + "a": "1", + "b": "2", + } + o := New(Metadata(v)) + if !reflect.DeepEqual(v, o.Metadata()) { + t.Fatalf("o.Metadata():%s is not equal to v:%s", o.Metadata(), v) + } +} + +func TestApp_Endpoint(t *testing.T) { + v := []string{"https://go-kratos.dev", "localhost"} + var endpoints []*url.URL + for _, urlStr := range v { + if endpoint, err := url.Parse(urlStr); err != nil { + t.Errorf("invalid endpoint:%v", urlStr) + } else { + endpoints = append(endpoints, endpoint) + } + } + o := New(Endpoint(endpoints...)) + if instance, err := o.buildInstance(); err != nil { + t.Error("build instance failed") + } else { + o.instance = instance + } + if !reflect.DeepEqual(o.Endpoint(), v) { + t.Errorf("Endpoint() = %v, want %v", o.Endpoint(), v) + } +} + +func TestApp_buildInstance(t *testing.T) { + want := struct { + id string + name string + version string + metadata map[string]string + endpoints []string + }{ + id: "1", + name: "kratos", + version: "v1.0.0", + metadata: map[string]string{ + "a": "1", + "b": "2", + }, + endpoints: []string{"https://go-kratos.dev", "localhost"}, + } + var endpoints []*url.URL + for _, urlStr := range want.endpoints { + if endpoint, err := url.Parse(urlStr); err != nil { + t.Errorf("invalid endpoint:%v", urlStr) + } else { + endpoints = append(endpoints, endpoint) + } + } + app := New( + ID(want.id), + Name(want.name), + Version(want.version), + Metadata(want.metadata), + Endpoint(endpoints...), + ) + if got, err := app.buildInstance(); err != nil { + t.Error("build got failed") + } else { + if got.ID != want.id { + t.Errorf("ID() = %v, want %v", got.ID, want.id) + } + if got.Name != want.name { + t.Errorf("Name() = %v, want %v", got.Name, want.name) + } + if got.Version != want.version { + t.Errorf("Version() = %v, want %v", got.Version, want.version) + } + if !reflect.DeepEqual(got.Endpoints, want.endpoints) { + t.Errorf("Endpoint() = %v, want %v", got.Endpoints, want.endpoints) + } + if !reflect.DeepEqual(got.Metadata, want.metadata) { + t.Errorf("Metadata() = %v, want %v", got.Metadata, want.metadata) + } + } +} + +func TestApp_Context(t *testing.T) { + type fields struct { + id string + version string + name string + instance *registry.ServiceInstance + metadata map[string]string + want struct { + id string + version string + name string + endpoint []string + metadata map[string]string + } + } + tests := []fields{ + { + id: "1", + name: "kratos-v1", + instance: ®istry.ServiceInstance{Endpoints: []string{"https://go-kratos.dev", "localhost"}}, + metadata: map[string]string{}, + version: "v1", + want: struct { + id string + version string + name string + endpoint []string + metadata map[string]string + }{ + id: "1", version: "v1", name: "kratos-v1", endpoint: []string{"https://go-kratos.dev", "localhost"}, + metadata: map[string]string{}, + }, + }, + { + id: "2", + name: "kratos-v2", + instance: ®istry.ServiceInstance{Endpoints: []string{"test"}}, + metadata: map[string]string{"kratos": "https://github.com/go-kratos/kratos"}, + version: "v2", + want: struct { + id string + version string + name string + endpoint []string + metadata map[string]string + }{ + id: "2", version: "v2", name: "kratos-v2", endpoint: []string{"test"}, + metadata: map[string]string{"kratos": "https://github.com/go-kratos/kratos"}, + }, + }, + { + id: "3", + name: "kratos-v3", + instance: nil, + metadata: make(map[string]string), + version: "v3", + want: struct { + id string + version string + name string + endpoint []string + metadata map[string]string + }{ + id: "3", version: "v3", name: "kratos-v3", endpoint: nil, + metadata: map[string]string{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &App{ + opts: options{id: tt.id, name: tt.name, metadata: tt.metadata, version: tt.version}, + ctx: context.Background(), + cancel: nil, + instance: tt.instance, + } + + ctx := NewContext(context.Background(), a) + + if got, ok := FromContext(ctx); ok { + if got.ID() != tt.want.id { + t.Errorf("ID() = %v, want %v", got.ID(), tt.want.id) + } + if got.Name() != tt.want.name { + t.Errorf("Name() = %v, want %v", got.Name(), tt.want.name) + } + if got.Version() != tt.want.version { + t.Errorf("Version() = %v, want %v", got.Version(), tt.want.version) + } + if !reflect.DeepEqual(got.Endpoint(), tt.want.endpoint) { + t.Errorf("Endpoint() = %v, want %v", got.Endpoint(), tt.want.endpoint) + } + if !reflect.DeepEqual(got.Metadata(), tt.want.metadata) { + t.Errorf("Metadata() = %v, want %v", got.Metadata(), tt.want.metadata) + } + } else { + t.Errorf("ok() = %v, want %v", ok, true) + } + }) + } +} + +func TestApp_ContextCanceled(t *testing.T) { + ctx, stop := context.WithCancel(context.Background()) + stopFn := func(ctx context.Context) error { + select { + case <-ctx.Done(): + t.Fatal("context should not be done yet") + default: + } + return nil + } + app := New(Context(ctx), Server(&mockServer{stopFn: stopFn}), StopTimeout(time.Hour)) + time.AfterFunc(time.Millisecond*10, stop) + _ = app.Run() +} diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..5586498 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,9 @@ +version: v2 +plugins: + - local: + - go + - run + - google.golang.org/protobuf/cmd/protoc-gen-go + out: . + opt: + - paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..af30280 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,12 @@ +version: v2 +modules: + - path: . + excludes: + - cmd/protoc-gen-go-errors + - third_party +lint: + use: + - DEFAULT +breaking: + use: + - FILE diff --git a/cmd/kratos/go.mod b/cmd/kratos/go.mod new file mode 100644 index 0000000..4f883c3 --- /dev/null +++ b/cmd/kratos/go.mod @@ -0,0 +1,55 @@ +module github.com/go-kratos/kratos/cmd/kratos/v3 + +go 1.25.0 + +require ( + github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/charmbracelet/huh v1.0.0 + github.com/emicklei/proto v1.14.3 + github.com/fatih/color v1.19.0 + github.com/spf13/cobra v1.10.2 + golang.org/x/mod v0.36.0 + golang.org/x/text v0.37.0 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymanbagabas/go-udiff v0.4.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/conpty v0.2.0 // indirect + github.com/charmbracelet/x/exp/golden v0.0.0-20260511125431-fe5d686e0c99 // indirect + github.com/charmbracelet/x/exp/strings v0.1.0 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/xpty v0.1.3 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/term v0.43.0 // indirect +) diff --git a/cmd/kratos/go.sum b/cmd/kratos/go.sum new file mode 100644 index 0000000..4b547f0 --- /dev/null +++ b/cmd/kratos/go.sum @@ -0,0 +1,146 @@ +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= +github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/conpty v0.2.0 h1:eKtA2hm34qNfgJCDp/M6Dc0gLy7e07YEK4qAdNGOvVY= +github.com/charmbracelet/x/conpty v0.2.0/go.mod h1:fexgUnVrZgw8scD49f6VSi0Ggj9GWYIrpedRthAwW/8= +github.com/charmbracelet/x/exp/golden v0.0.0-20260511125431-fe5d686e0c99 h1:v7S98u3M7JkXxTRKRsLPG/07YE7gPspQVmhpSpfofqo= +github.com/charmbracelet/x/exp/golden v0.0.0-20260511125431-fe5d686e0c99/go.mod h1:6fMpcW6iwN/kX+xJ52eqVWsDiBTe0UJD24JLoHFe+P0= +github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= +github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw= +github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/proto v1.14.3 h1:zEhlzNkpP8kN6utonKMzlPfIvy82t5Kb9mufaJxSe1Q= +github.com/emicklei/proto v1.14.3/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= diff --git a/cmd/kratos/internal/base/install.go b/cmd/kratos/internal/base/install.go new file mode 100644 index 0000000..6ec9ac3 --- /dev/null +++ b/cmd/kratos/internal/base/install.go @@ -0,0 +1,25 @@ +package base + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +// GoInstall go get path. +func GoInstall(path ...string) error { + for _, p := range path { + if !strings.Contains(p, "@") { + p += "@latest" + } + fmt.Printf("go install %s\n", p) + cmd := exec.Command("go", "install", p) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + } + return nil +} diff --git a/cmd/kratos/internal/base/mod.go b/cmd/kratos/internal/base/mod.go new file mode 100644 index 0000000..944c86c --- /dev/null +++ b/cmd/kratos/internal/base/mod.go @@ -0,0 +1,62 @@ +package base + +import ( + "bufio" + "bytes" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/mod/modfile" +) + +// ModulePath returns go module path. +func ModulePath(filename string) (string, error) { + modBytes, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return modfile.ModulePath(modBytes), nil +} + +// ModuleVersion returns module version. +func ModuleVersion(path string) (string, error) { + stdout := &bytes.Buffer{} + fd := exec.Command("go", "mod", "graph") + fd.Stdout = stdout + fd.Stderr = stdout + if err := fd.Run(); err != nil { + return "", err + } + rd := bufio.NewReader(stdout) + for { + line, _, err := rd.ReadLine() + if err != nil { + return "", err + } + str := string(line) + i := strings.Index(str, "@") + if strings.Contains(str, path+"@") && i != -1 { + return path + str[i:], nil + } + } +} + +// KratosMod returns kratos mod. +func KratosMod() string { + // go 1.15+ read from env GOMODCACHE + cacheOut, _ := exec.Command("go", "env", "GOMODCACHE").Output() + cachePath := strings.Trim(string(cacheOut), "\n") + pathOut, _ := exec.Command("go", "env", "GOPATH").Output() + gopath := strings.Trim(string(pathOut), "\n") + if cachePath == "" { + cachePath = filepath.Join(gopath, "pkg", "mod") + } + if path, err := ModuleVersion("github.com/go-kratos/kratos/v3"); err == nil { + // $GOPATH/pkg/mod/github.com/go-kratos/kratos/v3@v3.0.0 + return filepath.Join(cachePath, path) + } + // $GOPATH/src/github.com/go-kratos/kratos + return filepath.Join(gopath, "src", "github.com", "go-kratos", "kratos") +} diff --git a/cmd/kratos/internal/base/mod_test.go b/cmd/kratos/internal/base/mod_test.go new file mode 100644 index 0000000..3ef9b62 --- /dev/null +++ b/cmd/kratos/internal/base/mod_test.go @@ -0,0 +1,43 @@ +package base + +import ( + "os" + "testing" +) + +func TestModuleVersion(t *testing.T) { + v, err := ModuleVersion("golang.org/x/mod") + if err != nil { + t.Fatal(err) + } + t.Log(v) +} + +func TestModulePath(t *testing.T) { + if err := os.Mkdir("/tmp/test_mod", os.ModePerm); err != nil { + t.Fatal(err) + } + + f, err := os.Create("/tmp/test_mod/go.mod") + if err != nil { + t.Fatal(err) + } + + mod := `module github.com/go-kratos/kratos/v3 + +go 1.21` + _, err = f.WriteString(mod) + if err != nil { + t.Fatal(err) + } + + p, err := ModulePath("/tmp/test_mod/go.mod") + if err != nil { + t.Fatal(err) + } + if p != "github.com/go-kratos/kratos/v3" { + t.Fatalf("want: %s, got: %s", "github.com/go-kratos/kratos/v3", p) + } + + t.Cleanup(func() { os.RemoveAll("/tmp/test_mod") }) +} diff --git a/cmd/kratos/internal/base/path.go b/cmd/kratos/internal/base/path.go new file mode 100644 index 0000000..c7f900d --- /dev/null +++ b/cmd/kratos/internal/base/path.go @@ -0,0 +1,108 @@ +package base + +import ( + "bytes" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/fatih/color" +) + +func kratosHome() string { + dir, err := os.UserHomeDir() + if err != nil { + log.Fatalf("Failed to get user home directory: %v", err) + } + home := filepath.Join(dir, ".kratos") + if _, err := os.Stat(home); os.IsNotExist(err) { + if err := os.MkdirAll(home, 0o700); err != nil { + log.Fatalf("Failed to create kratos home %q: %v", home, err) + } + } + return home +} + +func kratosHomeWithDir(dir string) string { + home := filepath.Join(kratosHome(), dir) + if _, err := os.Stat(home); os.IsNotExist(err) { + if err := os.MkdirAll(home, 0o700); err != nil { + log.Fatalf("Failed to create kratos home directory %q: %v", home, err) + } + } + return home +} + +func copyFile(src, dst string, replaces []string) error { + srcinfo, err := os.Stat(src) + if err != nil { + return err + } + buf, err := os.ReadFile(src) + if err != nil { + return err + } + var old string + for i, next := range replaces { + if i%2 == 0 { + old = next + continue + } + buf = bytes.ReplaceAll(buf, []byte(old), []byte(next)) + } + return os.WriteFile(dst, buf, srcinfo.Mode()) +} + +func copyDir(src, dst string, replaces, ignores []string) error { + srcinfo, err := os.Stat(src) + if err != nil { + return err + } + + err = os.MkdirAll(dst, srcinfo.Mode()) + if err != nil { + return err + } + + fds, err := os.ReadDir(src) + if err != nil { + return err + } + for _, fd := range fds { + if hasSets(fd.Name(), ignores) { + continue + } + srcfp := filepath.Join(src, fd.Name()) + dstfp := filepath.Join(dst, fd.Name()) + var e error + if fd.IsDir() { + e = copyDir(srcfp, dstfp, replaces, ignores) + } else { + e = copyFile(srcfp, dstfp, replaces) + } + if e != nil { + return e + } + } + return nil +} + +func hasSets(name string, sets []string) bool { + for _, ig := range sets { + if ig == name { + return true + } + } + return false +} + +func Tree(path string, dir string) { + _ = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err == nil && info != nil && !info.IsDir() { + fmt.Printf("%s %s (%v bytes)\n", color.GreenString("CREATED"), strings.ReplaceAll(path, dir+"/", ""), info.Size()) + } + return nil + }) +} diff --git a/cmd/kratos/internal/base/repo.go b/cmd/kratos/internal/base/repo.go new file mode 100644 index 0000000..fe1ba18 --- /dev/null +++ b/cmd/kratos/internal/base/repo.go @@ -0,0 +1,126 @@ +package base + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "path" + "path/filepath" + "strings" +) + +var unExpandVarPath = []string{"~", ".", ".."} + +// Repo is git repository manager. +type Repo struct { + url string + home string + branch string +} + +func repoDir(url string) string { + vcsURL, err := ParseVCSUrl(url) + if err != nil { + return url + } + // check host contains port + host, _, err := net.SplitHostPort(vcsURL.Host) + if err != nil { + host = vcsURL.Host + } + for _, p := range unExpandVarPath { + host = strings.TrimLeft(host, p) + } + dir := path.Base(path.Dir(vcsURL.Path)) + url = fmt.Sprintf("%s/%s", host, dir) + return url +} + +// NewRepo new a repository manager. +func NewRepo(url string, branch string) *Repo { + return &Repo{ + url: url, + home: kratosHomeWithDir("repo/" + repoDir(url)), + branch: branch, + } +} + +// Path returns the repository cache path. +func (r *Repo) Path() string { + start := strings.LastIndex(r.url, "/") + end := strings.LastIndex(r.url, ".git") + if end == -1 { + end = len(r.url) + } + var branch string + if r.branch == "" { + branch = "@main" + } else { + branch = "@" + r.branch + } + return path.Join(r.home, r.url[start+1:end]+branch) +} + +// Pull fetch the repository from remote url. +func (r *Repo) Pull(ctx context.Context) error { + cmd := exec.CommandContext(ctx, "git", "symbolic-ref", "HEAD") + cmd.Dir = r.Path() + _, err := cmd.CombinedOutput() + if err != nil { + return err + } + cmd = exec.CommandContext(ctx, "git", "pull") + cmd.Dir = r.Path() + out, err := cmd.CombinedOutput() + fmt.Println(string(out)) + if err != nil { + return err + } + return err +} + +// Clone clones the repository to cache path. +func (r *Repo) Clone(ctx context.Context) error { + if _, err := os.Stat(r.Path()); !os.IsNotExist(err) { + return r.Pull(ctx) + } + var cmd *exec.Cmd + if r.branch == "" { + cmd = exec.CommandContext(ctx, "git", "clone", r.url, r.Path()) + } else { + cmd = exec.CommandContext(ctx, "git", "clone", "-b", r.branch, r.url, r.Path()) + } + out, err := cmd.CombinedOutput() + fmt.Println(string(out)) + if err != nil { + return err + } + return nil +} + +// CopyTo copies the repository to project path. +func (r *Repo) CopyTo(ctx context.Context, to string, modPath string, ignores []string) error { + if err := r.Clone(ctx); err != nil { + return err + } + mod, err := ModulePath(filepath.Join(r.Path(), "go.mod")) + if err != nil { + return err + } + return copyDir(r.Path(), to, []string{mod, modPath}, ignores) +} + +// CopyToV2 copies the repository to project path +func (r *Repo) CopyToV2(ctx context.Context, to string, modPath string, ignores, replaces []string) error { + if err := r.Clone(ctx); err != nil { + return err + } + mod, err := ModulePath(filepath.Join(r.Path(), "go.mod")) + if err != nil { + return err + } + replaces = append([]string{mod, modPath}, replaces...) + return copyDir(r.Path(), to, replaces, ignores) +} diff --git a/cmd/kratos/internal/base/repo_test.go b/cmd/kratos/internal/base/repo_test.go new file mode 100644 index 0000000..dfcdbb9 --- /dev/null +++ b/cmd/kratos/internal/base/repo_test.go @@ -0,0 +1,51 @@ +package base + +import ( + "context" + "os" + "testing" +) + +func TestRepo(t *testing.T) { + urls := []string{ + // ssh://[user@]host.xz[:port]/path/to/repo.git/ + "ssh://git@github.com:7875/go-kratos/kratos.git", + // git://host.xz[:port]/path/to/repo.git/ + "git://github.com:7875/go-kratos/kratos.git", + // http[s]://host.xz[:port]/path/to/repo.git/ + "https://github.com:7875/go-kratos/kratos.git", + // ftp[s]://host.xz[:port]/path/to/repo.git/ + "ftps://github.com:7875/go-kratos/kratos.git", + //[user@]host.xz:path/to/repo.git/ + "git@github.com:go-kratos/kratos.git", + // ssh://[user@]host.xz[:port]/~[user]/path/to/repo.git/ + "ssh://git@github.com:7875/go-kratos/kratos.git", + // git://host.xz[:port]/~[user]/path/to/repo.git/ + "git://github.com:7875/go-kratos/kratos.git", + //[user@]host.xz:/~[user]/path/to/repo.git/ + "git@github.com:go-kratos/kratos.git", + ///path/to/repo.git/ + "//github.com/go-kratos/kratos.git", + // file:///path/to/repo.git/ + "file://./github.com/go-kratos/kratos.git", + } + for _, url := range urls { + dir := repoDir(url) + if dir != "github.com/go-kratos" && dir != "/go-kratos" { + t.Fatal(url, "repoDir test failed", dir) + } + } +} + +func TestRepoClone(t *testing.T) { + r := NewRepo("https://github.com/go-kratos/service-layout.git", "") + if err := r.Clone(context.Background()); err != nil { + t.Fatal(err) + } + if err := r.CopyTo(context.Background(), "/tmp/test_repo", "github.com/go-kratos/kratos-layout", nil); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + os.RemoveAll("/tmp/test_repo") + }) +} diff --git a/cmd/kratos/internal/base/vcs_url.go b/cmd/kratos/internal/base/vcs_url.go new file mode 100644 index 0000000..6d3aef1 --- /dev/null +++ b/cmd/kratos/internal/base/vcs_url.go @@ -0,0 +1,58 @@ +package base + +import ( + "errors" + "net/url" + "regexp" + "strings" +) + +var ( + scpSyntaxRe = regexp.MustCompile(`^(\w+)@([\w.-]+):(.*)$`) + scheme = []string{"git", "https", "http", "git+ssh", "ssh", "file", "ftp", "ftps"} +) + +// ParseVCSUrl ref https://github.com/golang/go/blob/master/src/cmd/go/internal/vcs/vcs.go +// see https://go-review.googlesource.com/c/go/+/12226/ +// git url define https://git-scm.com/docs/git-clone#_git_urls +func ParseVCSUrl(repo string) (*url.URL, error) { + var ( + repoURL *url.URL + err error + ) + + if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil { + // Match SCP-like syntax and convert it to a URL. + // Eg, "git@github.com:user/repo" becomes + // "ssh://git@github.com/user/repo". + repoURL = &url.URL{ + Scheme: "ssh", + User: url.User(m[1]), + Host: m[2], + Path: m[3], + } + } else { + if !strings.Contains(repo, "//") { + repo = "//" + repo + } + if strings.HasPrefix(repo, "//git@") { + repo = "ssh:" + repo + } else if strings.HasPrefix(repo, "//") { + repo = "https:" + repo + } + repoURL, err = url.Parse(repo) + if err != nil { + return nil, err + } + } + + // Iterate over insecure schemes too, because this function simply + // reports the state of the repo. If we can't see insecure schemes then + // we can't report the actual repo URL. + for _, s := range scheme { + if repoURL.Scheme == s { + return repoURL, nil + } + } + return nil, errors.New("unable to parse repo url") +} diff --git a/cmd/kratos/internal/base/vcs_url_test.go b/cmd/kratos/internal/base/vcs_url_test.go new file mode 100644 index 0000000..09b8d56 --- /dev/null +++ b/cmd/kratos/internal/base/vcs_url_test.go @@ -0,0 +1,55 @@ +package base + +import ( + "net" + "strings" + "testing" +) + +func TestParseVCSUrl(t *testing.T) { + repos := []string{ + // ssh://[user@]host.xz[:port]/path/to/repo.git/ + "ssh://git@github.com:7875/go-kratos/kratos.git", + // git://host.xz[:port]/path/to/repo.git/ + "git://github.com:7875/go-kratos/kratos.git", + // http[s]://host.xz[:port]/path/to/repo.git/ + "https://github.com:7875/go-kratos/kratos.git", + // ftp[s]://host.xz[:port]/path/to/repo.git/ + "ftps://github.com:7875/go-kratos/kratos.git", + //[user@]host.xz:path/to/repo.git/ + "git@github.com:go-kratos/kratos.git", + // ssh://[user@]host.xz[:port]/~[user]/path/to/repo.git/ + "ssh://git@github.com:7875/go-kratos/kratos.git", + // git://host.xz[:port]/~[user]/path/to/repo.git/ + "git://github.com:7875/go-kratos/kratos.git", + //[user@]host.xz:/~[user]/path/to/repo.git/ + "git@github.com:go-kratos/kratos.git", + ///path/to/repo.git/ + "~/go-kratos/kratos.git", + // file:///path/to/repo.git/ + "file://~/go-kratos/kratos.git", + } + for _, repo := range repos { + url, err := ParseVCSUrl(repo) + if err != nil { + t.Fatal(repo, err) + } + urlPath := strings.TrimLeft(url.Path, "/") + if urlPath != "go-kratos/kratos.git" { + t.Fatal(repo, "parse url failed", urlPath) + } + } +} + +func TestParseSsh(t *testing.T) { + repo := "ssh://git@github.com:7875/go-kratos/kratos.git" + url, err := ParseVCSUrl(repo) + if err != nil { + t.Fatal(err) + } + host, _, err := net.SplitHostPort(url.Host) + if err != nil { + host = url.Host + } + t.Log(host, url.Path) +} diff --git a/cmd/kratos/internal/change/change.go b/cmd/kratos/internal/change/change.go new file mode 100644 index 0000000..2e92bee --- /dev/null +++ b/cmd/kratos/internal/change/change.go @@ -0,0 +1,45 @@ +package change + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// CmdChange is kratos change log tool +var CmdChange = &cobra.Command{ + Use: "changelog", + Short: "Get a kratos change log", + Long: "Get a kratos release or commits info. Example: kratos changelog dev or kratos changelog {version}", + Run: run, +} + +var ( + token string + repoURL string +) + +func init() { + if repoURL = os.Getenv("KRATOS_REPO"); repoURL == "" { + repoURL = "https://github.com/go-kratos/kratos.git" + } + CmdChange.Flags().StringVarP(&repoURL, "repo-url", "r", repoURL, "github repo") + token = os.Getenv("GITHUB_TOKEN") +} + +func run(_ *cobra.Command, args []string) { + owner, repo := ParseGithubURL(repoURL) + api := GithubAPI{Owner: owner, Repo: repo, Token: token} + version := "latest" + if len(args) > 0 { + version = args[0] + } + if version == "dev" { + info := api.GetCommitsInfo() + fmt.Print(ParseCommitsInfo(info)) + return + } + info := api.GetReleaseInfo(version) + fmt.Print(ParseReleaseInfo(info)) +} diff --git a/cmd/kratos/internal/change/get.go b/cmd/kratos/internal/change/get.go new file mode 100644 index 0000000..038c5e4 --- /dev/null +++ b/cmd/kratos/internal/change/get.go @@ -0,0 +1,234 @@ +package change + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + "time" +) + +type ReleaseInfo struct { + Author struct { + Login string `json:"login"` + } `json:"author"` + PublishedAt string `json:"published_at"` + Body string `json:"body"` + HTMLURL string `json:"html_url"` +} + +type CommitInfo struct { + Commit struct { + Message string `json:"message"` + } `json:"commit"` +} + +type ErrorInfo struct { + Message string +} + +type GithubAPI struct { + Owner string + Repo string + Token string +} + +const ( + commitTypeFix = "fix" + commitTypeFeat = "feat" + commitTypeDeps = "deps" + commitTypeBuild = "build" + commitTypeBreak = "break" + commitTypeChore = "chore" + commitTypeOther = "other" +) + +// GetReleaseInfo for getting kratos release info. +func (g *GithubAPI) GetReleaseInfo(version string) ReleaseInfo { + api := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", g.Owner, g.Repo) + if version != "latest" { + api = fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", g.Owner, g.Repo, version) + } + resp, code := requestGithubAPI(api, http.MethodGet, nil, g.Token) + if code != http.StatusOK { + printGithubErrorInfo(resp) + } + releaseInfo := ReleaseInfo{} + err := json.Unmarshal(resp, &releaseInfo) + if err != nil { + fatal(err) + } + return releaseInfo +} + +// GetCommitsInfo for getting kratos commits info. +func (g *GithubAPI) GetCommitsInfo() []CommitInfo { + info := g.GetReleaseInfo("latest") + page := 1 + prePage := 100 + var list []CommitInfo + for { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits?pre_page=%d&page=%d&since=%s", g.Owner, g.Repo, prePage, page, info.PublishedAt) + resp, code := requestGithubAPI(url, http.MethodGet, nil, g.Token) + if code != http.StatusOK { + printGithubErrorInfo(resp) + } + var res []CommitInfo + err := json.Unmarshal(resp, &res) + if err != nil { + fatal(err) + } + list = append(list, res...) + if len(res) < prePage { + break + } + page++ + } + return list +} + +func printGithubErrorInfo(body []byte) { + errorInfo := &ErrorInfo{} + err := json.Unmarshal(body, errorInfo) + if err != nil { + fatal(err) + } + fatal(errors.New(errorInfo.Message)) +} + +func requestGithubAPI(url string, method string, body io.Reader, token string) ([]byte, int) { + cli := &http.Client{Timeout: 60 * time.Second} + request, err := http.NewRequest(method, url, body) + if err != nil { + fatal(err) + } + if token != "" { + request.Header.Add("Authorization", token) + } + resp, err := cli.Do(request) + if err != nil { + fatal(err) + } + defer resp.Body.Close() + resBody, err := io.ReadAll(resp.Body) + if err != nil { + fatal(err) + } + return resBody, resp.StatusCode +} + +func ParseCommitsInfo(info []CommitInfo) string { + group := map[string][]string{ + commitTypeFix: {}, + commitTypeFeat: {}, + commitTypeDeps: {}, + commitTypeBuild: {}, + commitTypeBreak: {}, + commitTypeChore: {}, + commitTypeOther: {}, + } + + for _, commitInfo := range info { + msg := commitInfo.Commit.Message + index := strings.Index(fmt.Sprintf("%q", msg), `\n`) + if index != -1 { + msg = msg[:index-1] + } + prefix := []string{commitTypeFix, commitTypeFeat, commitTypeBuild, commitTypeDeps, commitTypeBreak, commitTypeChore} + var matched bool + for _, v := range prefix { + msg = strings.TrimPrefix(msg, " ") + if strings.HasPrefix(msg, v) { + group[v] = append(group[v], msg) + matched = true + } + } + if !matched { + group[commitTypeOther] = append(group[commitTypeOther], msg) + } + } + + md := make(map[string]string) + for key, value := range group { + var text string + switch key { + case commitTypeBreak: + text = "### Breaking Changes\n" + case commitTypeDeps: + text = "### Dependencies\n" + case commitTypeFeat: + text = "### New Features\n" + case commitTypeFix: + text = "### Bug Fixes\n" + case commitTypeBuild: + text = "### Builds\n" + case commitTypeChore: + text = "### Chores\n" + case commitTypeOther: + text = "### Others\n" + } + if len(value) == 0 { + continue + } + md[key] += text + for _, value := range value { + md[key] += fmt.Sprintf("- %s\n", value) + } + } + return fmt.Sprint( + md[commitTypeBreak], + md[commitTypeDeps], + md[commitTypeFeat], + md[commitTypeFix], + md[commitTypeBuild], + md[commitTypeChore], + md[commitTypeOther], + ) +} + +func ParseReleaseInfo(info ReleaseInfo) string { + reg := regexp.MustCompile(`(?m)^\s*$[\r\n]*|[\r\n]+\s+\z|<[\S\s]+?>`) + body := reg.ReplaceAll([]byte(info.Body), []byte("")) + if string(body) == "" { + body = []byte("no release info") + } + splitters := "--------------------------------------------" + return fmt.Sprintf( + "Author: %s\nDate: %s\nUrl: %s\n\n%s\n\n%s\n\n%s\n", + info.Author.Login, + info.PublishedAt, + info.HTMLURL, + splitters, + body, + splitters, + ) +} + +func ParseGithubURL(url string) (owner string, repo string) { + var start int + start = strings.Index(url, "//") + if start == -1 { + start = strings.Index(url, ":") + 1 + } else { + start += 2 + } + end := strings.LastIndex(url, "/") + gitIndex := strings.LastIndex(url, ".git") + if gitIndex == -1 { + repo = url[strings.LastIndex(url, "/")+1:] + } else { + repo = url[strings.LastIndex(url, "/")+1 : gitIndex] + } + tmp := url[start:end] + owner = tmp[strings.Index(tmp, "/")+1:] + return +} + +func fatal(err error) { + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err) + os.Exit(1) +} diff --git a/cmd/kratos/internal/change/get_test.go b/cmd/kratos/internal/change/get_test.go new file mode 100644 index 0000000..a6a9d03 --- /dev/null +++ b/cmd/kratos/internal/change/get_test.go @@ -0,0 +1,25 @@ +package change + +import "testing" + +func TestParseGithubURL(t *testing.T) { + urls := []struct { + url string + owner string + repo string + }{ + {"https://github.com/go-kratos/kratos.git", "go-kratos", "kratos"}, + {"https://github.com/go-kratos/kratos", "go-kratos", "kratos"}, + {"git@github.com:go-kratos/kratos.git", "go-kratos", "kratos"}, + {"https://github.com/go-kratos/go-kratos.dev.git", "go-kratos", "go-kratos.dev"}, + } + for _, url := range urls { + owner, repo := ParseGithubURL(url.url) + if owner != url.owner { + t.Fatalf("owner want: %s, got: %s", owner, url.owner) + } + if repo != url.repo { + t.Fatalf("repo want: %s, got: %s", repo, url.repo) + } + } +} diff --git a/cmd/kratos/internal/project/add.go b/cmd/kratos/internal/project/add.go new file mode 100644 index 0000000..5669ab9 --- /dev/null +++ b/cmd/kratos/internal/project/add.go @@ -0,0 +1,76 @@ +package project + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/AlecAivazis/survey/v2" + "github.com/fatih/color" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/base" +) + +const ( + goModFileName = "go.mod" + goSumFileName = "go.sum" + readmeFileName = "README.md" +) + +var repoAddIgnores = []string{ + ".git", ".github", "api", readmeFileName, "LICENSE", goModFileName, goSumFileName, "third_party", "openapi.yaml", ".gitignore", +} + +func (p *Project) Add(ctx context.Context, dir string, layout string, branch string, mod string, pkgPath string) error { + to := filepath.Join(dir, p.Name) + + if _, err := os.Stat(to); !os.IsNotExist(err) { + fmt.Printf("🚫 %s already exists\n", p.Name) + override := false + prompt := &survey.Confirm{ + Message: "📂 Do you want to override the folder ?", + Help: "Delete the existing folder and create the project.", + } + e := survey.AskOne(prompt, &override) + if e != nil { + return e + } + if !override { + return err + } + os.RemoveAll(to) + } + + fmt.Printf("🚀 Add service %s, layout repo is %s, please wait a moment.\n\n", p.Name, layout) + + pkgPath = fmt.Sprintf("%s/%s", mod, pkgPath) + repo := base.NewRepo(layout, branch) + err := repo.CopyToV2(ctx, to, pkgPath, repoAddIgnores, []string{filepath.Join(p.Path, "api"), "api"}) + if err != nil { + return err + } + + e := os.Rename( + filepath.Join(to, "cmd", "server"), + filepath.Join(to, "cmd", p.Name), + ) + if e != nil { + if !os.IsNotExist(e) { + return e + } + } + + base.Tree(to, dir) + + fmt.Printf("\n🍺 Repository creation succeeded %s\n", color.GreenString(p.Name)) + fmt.Print("💻 Use the following command to add a project 👇:\n\n") + + fmt.Println(color.WhiteString("$ cd %s", p.Name)) + fmt.Println(color.WhiteString("$ go generate ./...")) + fmt.Println(color.WhiteString("$ go build -o ./bin/ ./... ")) + fmt.Println(color.WhiteString("$ ./bin/%s -conf ./configs\n", p.Name)) + fmt.Println(" 🤝 Thanks for using Kratos") + fmt.Println(" 📚 Tutorial: https://go-kratos.dev/docs/getting-started/start") + return nil +} diff --git a/cmd/kratos/internal/project/new.go b/cmd/kratos/internal/project/new.go new file mode 100644 index 0000000..523bbe0 --- /dev/null +++ b/cmd/kratos/internal/project/new.go @@ -0,0 +1,66 @@ +package project + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/AlecAivazis/survey/v2" + "github.com/fatih/color" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/base" +) + +// Project is a project template. +type Project struct { + Name string + Path string +} + +// New new a project from remote repo. +func (p *Project) New(ctx context.Context, dir string, layout string, branch string) error { + to := filepath.Join(dir, p.Name) + if _, err := os.Stat(to); !os.IsNotExist(err) { + fmt.Printf("🚫 %s already exists\n", p.Name) + prompt := &survey.Confirm{ + Message: "📂 Do you want to override the folder ?", + Help: "Delete the existing folder and create the project.", + } + var override bool + e := survey.AskOne(prompt, &override) + if e != nil { + return e + } + if !override { + return err + } + os.RemoveAll(to) + } + fmt.Printf("🚀 Creating service %s, layout repo is %s, please wait a moment.\n\n", p.Name, layout) + repo := base.NewRepo(layout, branch) + if err := repo.CopyTo(ctx, to, p.Name, []string{".git", ".github"}); err != nil { + return err + } + e := os.Rename( + filepath.Join(to, "cmd", "server"), + filepath.Join(to, "cmd", p.Name), + ) + if e != nil { + if !os.IsNotExist(e) { + return e + } + } + base.Tree(to, dir) + + fmt.Printf("\n🍺 Project creation succeeded %s\n", color.GreenString(p.Name)) + fmt.Print("💻 Use the following command to start the project 👇:\n\n") + + fmt.Println(color.WhiteString("$ cd %s", p.Name)) + fmt.Println(color.WhiteString("$ go generate ./...")) + fmt.Println(color.WhiteString("$ go build -o ./bin/ ./... ")) + fmt.Println(color.WhiteString("$ ./bin/%s -conf ./configs\n", p.Name)) + fmt.Println(" 🤝 Thanks for using Kratos") + fmt.Println(" 📚 Tutorial: https://go-kratos.dev/docs/getting-started/start") + return nil +} diff --git a/cmd/kratos/internal/project/project.go b/cmd/kratos/internal/project/project.go new file mode 100644 index 0000000..e0f8274 --- /dev/null +++ b/cmd/kratos/internal/project/project.go @@ -0,0 +1,205 @@ +package project + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/base" +) + +var projects = map[string]string{ + "service": "https://github.com/go-kratos/kratos-layout.git", + "admin": "https://github.com/go-kratos/kratos-admin.git", +} + +// CmdNew represents the new command. +var CmdNew = &cobra.Command{ + Use: "new", + Short: "Create a service template", + Long: "Create a service project using the repository template. Example: kratos new helloworld", + Run: run, +} + +var ( + nomod bool + repo string + branch string + timeout = "60s" +) + +func init() { + CmdNew.Flags().StringVarP(&repo, "repo", "r", repo, "custom repo url") + CmdNew.Flags().StringVarP(&branch, "branch", "b", branch, "repo branch") + CmdNew.Flags().StringVarP(&timeout, "timeout", "t", timeout, "time out") + CmdNew.Flags().BoolVarP(&nomod, "nomod", "", nomod, "retain go mod") +} + +func run(_ *cobra.Command, args []string) { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + t, err := time.ParseDuration(timeout) + if err != nil { + panic(err) + } + ctx, cancel := context.WithTimeout(context.Background(), t) + defer cancel() + name := "" + if len(args) == 0 { + prompt := &survey.Input{ + Message: "What is project name ?", + Help: "Created project name.", + } + err = survey.AskOne(prompt, &name) + if err != nil || name == "" { + return + } + } else { + name = args[0] + } + projectName, workingDir := processProjectParams(name, wd) + p := &Project{Name: projectName} + done := make(chan error, 1) + var repoURL string + if repo != "" { + repoURL = repo + } else { + repoURL, err = selectRepo() + if err != nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: failed to select repo(%s)\033[m\n", err.Error()) + return + } + } + go func() { + if !nomod { + done <- p.New(ctx, workingDir, repoURL, branch) + return + } + projectRoot := getgomodProjectRoot(workingDir) + if gomodIsNotExistIn(projectRoot) { + done <- fmt.Errorf("🚫 go.mod don't exists in %s", projectRoot) + return + } + + packagePath, e := filepath.Rel(projectRoot, filepath.Join(workingDir, projectName)) + if e != nil { + done <- fmt.Errorf("🚫 failed to get relative path: %v", e) + return + } + packagePath = strings.ReplaceAll(packagePath, "\\", "/") + + mod, e := base.ModulePath(filepath.Join(projectRoot, "go.mod")) + if e != nil { + done <- fmt.Errorf("🚫 failed to parse `go.mod`: %v", e) + return + } + // Get the relative path for adding a project based on Go modules + p.Path = filepath.Join(strings.TrimPrefix(workingDir, projectRoot+"/"), p.Name) + done <- p.Add(ctx, workingDir, repoURL, branch, mod, packagePath) + }() + select { + case <-ctx.Done(): + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + fmt.Fprint(os.Stderr, "\033[31mERROR: project creation timed out\033[m\n") + return + } + fmt.Fprintf(os.Stderr, "\033[31mERROR: failed to create project(%s)\033[m\n", ctx.Err().Error()) + case err = <-done: + if err != nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: Failed to create project(%s)\033[m\n", err.Error()) + } + } +} + +func processProjectParams(projectName string, workingDir string) (projectNameResult, workingDirResult string) { + _projectDir := projectName + _workingDir := workingDir + // Process ProjectName with system variable + if strings.HasPrefix(projectName, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + // cannot get user home return fallback place dir + return _projectDir, _workingDir + } + _projectDir = filepath.Join(homeDir, projectName[2:]) + } + + // check path is relative + if !filepath.IsAbs(projectName) { + absPath, err := filepath.Abs(projectName) + if err != nil { + return _projectDir, _workingDir + } + _projectDir = absPath + } + + return filepath.Base(_projectDir), filepath.Dir(_projectDir) +} + +func getgomodProjectRoot(dir string) string { + if dir == filepath.Dir(dir) { + return dir + } + if gomodIsNotExistIn(dir) { + return getgomodProjectRoot(filepath.Dir(dir)) + } + return dir +} + +func gomodIsNotExistIn(dir string) bool { + _, e := os.Stat(filepath.Join(dir, "go.mod")) + return os.IsNotExist(e) +} + +func selectRepo() (string, error) { + var ( + choice string + customURL string + ) + form := huh.NewForm( + // 1) Select group (always visible) + huh.NewGroup( + huh.NewSelect[string](). + Title("Select a template"). + Options( + huh.NewOption("Service", "service"), + huh.NewOption("Admin", "admin"), + huh.NewOption("Custom (enter repo URL)", "custom"), + ). + Value(&choice), + ), + // 2) Input group (only visible when choice == "custom") + huh.NewGroup( + huh.NewInput(). + Title("Enter custom repository URL"). + Placeholder("https://github.com/owner/repo.git"). + Value(&customURL). + Validate(func(s string) error { + s = strings.TrimSpace(s) + if s == "" { + return fmt.Errorf("repo URL cannot be empty") + } + return nil + }), + ).WithHideFunc(func() bool { + return choice != "custom" + }), + ) + if err := form.Run(); err != nil { + panic(err) + } + if choice == "custom" { + return strings.TrimSpace(customURL), nil + } + return projects[choice], nil +} diff --git a/cmd/kratos/internal/project/project_linux_test.go b/cmd/kratos/internal/project/project_linux_test.go new file mode 100644 index 0000000..8e4ef23 --- /dev/null +++ b/cmd/kratos/internal/project/project_linux_test.go @@ -0,0 +1,28 @@ +//go:build linux + +package project + +import ( + "testing" +) + +func Test_processProjectParams(t *testing.T) { + type args struct { + projectName string + fallbackPlaceDir string + } + tests := []struct { + name string + args args + want string + }{ + {"absLinux", args{projectName: "/home/kratos/awesome/go/demo", fallbackPlaceDir: ""}, "/home/kratos/awesome/go"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, got := processProjectParams(tt.args.projectName, tt.args.fallbackPlaceDir); got != tt.want { + t.Errorf("processProjectParams() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/kratos/internal/project/project_test.go b/cmd/kratos/internal/project/project_test.go new file mode 100644 index 0000000..8de6c13 --- /dev/null +++ b/cmd/kratos/internal/project/project_test.go @@ -0,0 +1,144 @@ +package project + +import ( + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + "testing" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/base" +) + +// TestCmdNew tests the `kratos new` command. +func TestCmdNew(t *testing.T) { + cwd := changeCurrentDir(t) + projectName := "helloworld" + + // create a new project + CmdNew.SetArgs([]string{projectName}) + if err := CmdNew.Execute(); err != nil { + t.Fatalf("executing command: %v", err) + } + + // check that the expected files were created + for _, file := range []string{ + goModFileName, + goSumFileName, + readmeFileName, + "cmd/helloworld/main.go", + } { + if _, err := os.Stat(filepath.Join(cwd, projectName, file)); err != nil { + t.Errorf("expected file %s to exist", file) + } + } + + // check that the go.mod file contains the expected module name + assertGoMod(t, filepath.Join(cwd, projectName, goModFileName), projectName) + + assertImportsInclude(t, filepath.Join(cwd, projectName, "cmd", projectName, "wire.go"), fmt.Sprintf(`"%s/internal/biz"`, projectName)) +} + +// TestCmdNewNoMod tests the `kratos new` command with the --nomod flag. +func TestCmdNewNoMod(t *testing.T) { + cwd := changeCurrentDir(t) + + // create a new project + CmdNew.SetArgs([]string{"project"}) + if err := CmdNew.Execute(); err != nil { + t.Fatalf("executing command: %v", err) + } + + // add new app with --nomod flag + CmdNew.SetArgs([]string{"--nomod", "project/app/user"}) + if err := CmdNew.Execute(); err != nil { + t.Fatalf("executing command: %v", err) + } + + // check that the expected files were created + for _, file := range []string{ + goModFileName, + goSumFileName, + readmeFileName, + "cmd/project/main.go", + "app/user/cmd/user/main.go", + } { + if _, err := os.Stat(filepath.Join(cwd, "project", file)); err != nil { + t.Errorf("expected file %s to exist", file) + } + } + + assertImportsInclude(t, filepath.Join(cwd, "project/app/user/cmd/user/wire.go"), `"project/app/user/internal/biz"`) +} + +// assertImportsInclude checks that the file at path contains the expected import. +func assertImportsInclude(t *testing.T, path, expected string) { + t.Helper() + + got, err := imports(path) + if err != nil { + t.Fatalf("getting imports: %v", err) + } + + for _, imp := range got { + if imp == expected { + return + } + } + + t.Errorf("expected imports to include %s, got %v", expected, got) +} + +// imports returns the imports in the file at path. +func imports(path string) ([]string, error) { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly) + if err != nil { + return nil, err + } + + imports := make([]string, 0, len(f.Imports)) + for _, s := range f.Imports { + imports = append(imports, s.Path.Value) + } + + return imports, nil +} + +// assertGoMod checks that the go.mod file contains the expected module name. +func assertGoMod(t *testing.T, path, expected string) { + t.Helper() + + got, err := base.ModulePath(path) + if err != nil { + t.Fatalf("getting module path: %v", err) + } + + if got != expected { + t.Errorf("expected module name %s, got %s", expected, got) + } +} + +// change the working directory to the tempdir +func changeCurrentDir(t *testing.T) string { + t.Helper() + + tmp := t.TempDir() + + oldCWD, err := os.Getwd() + if err != nil { + t.Fatalf("getting working directory: %v", err) + } + + if err := os.Chdir(tmp); err != nil { + t.Fatalf("changing working directory: %v", err) + } + t.Cleanup(func() { + if err := os.Chdir(oldCWD); err != nil { + t.Fatalf("restoring working directory: %v", err) + } + }) + + return tmp +} diff --git a/cmd/kratos/internal/project/project_windows_test.go b/cmd/kratos/internal/project/project_windows_test.go new file mode 100644 index 0000000..3ae1d23 --- /dev/null +++ b/cmd/kratos/internal/project/project_windows_test.go @@ -0,0 +1,29 @@ +//go:build windows + +package project + +import ( + "testing" +) + +func Test_processProjectParams(t *testing.T) { + type args struct { + projectName string + fallbackPlaceDir string + } + tests := []struct { + name string + args args + want string + }{ + {"absWindows", args{projectName: "c:\\kratos\\awesome\\go\\demo", fallbackPlaceDir: ""}, "c:\\kratos\\awesome\\go"}, + //{"relativeWindows", args{projectName: "/home/kratos/awesome/go/demo", fallbackPlaceDir: ""}, "/home/kratos/awesome/go"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, got := processProjectParams(tt.args.projectName, tt.args.fallbackPlaceDir); got != tt.want { + t.Errorf("getProjectPlaceDir() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/kratos/internal/proto/add/add.go b/cmd/kratos/internal/proto/add/add.go new file mode 100644 index 0000000..2415ab9 --- /dev/null +++ b/cmd/kratos/internal/proto/add/add.go @@ -0,0 +1,78 @@ +package add + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/mod/modfile" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// CmdAdd represents the add command. +var CmdAdd = &cobra.Command{ + Use: "add", + Short: "Add a proto API template", + Long: "Add a proto API template. Example: kratos proto add helloworld/v1/hello.proto", + Run: run, +} + +func run(_ *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Println("Please enter the proto file or directory") + return + } + input := args[0] + n := strings.LastIndex(input, "/") + if n == -1 { + fmt.Println("The proto path needs to be hierarchical.") + return + } + path := input[:n] + fileName := input[n+1:] + pkgName := strings.ReplaceAll(path, "/", ".") + + p := &Proto{ + Name: fileName, + Path: path, + Package: pkgName, + GoPackage: goPackage(path), + JavaPackage: javaPackage(pkgName), + Service: serviceName(fileName), + } + if err := p.Generate(); err != nil { + fmt.Println(err) + return + } +} + +func modName() string { + modBytes, err := os.ReadFile("go.mod") + if err != nil { + if modBytes, err = os.ReadFile("../go.mod"); err != nil { + return "" + } + } + return modfile.ModulePath(modBytes) +} + +func goPackage(path string) string { + s := strings.Split(path, "/") + return modName() + "/" + path + ";" + s[len(s)-1] +} + +func javaPackage(name string) string { + return name +} + +func serviceName(name string) string { + return toUpperCamelCase(strings.Split(name, ".")[0]) +} + +func toUpperCamelCase(s string) string { + s = strings.ReplaceAll(s, "_", " ") + s = cases.Title(language.Und, cases.NoLower).String(s) + return strings.ReplaceAll(s, " ", "") +} diff --git a/cmd/kratos/internal/proto/add/add_test.go b/cmd/kratos/internal/proto/add/add_test.go new file mode 100644 index 0000000..eda6b41 --- /dev/null +++ b/cmd/kratos/internal/proto/add/add_test.go @@ -0,0 +1,38 @@ +package add + +import "testing" + +func TestUnderscoreToUpperCamelCase(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "hello_world", + want: "HelloWorld", + }, + { + name: "v2_kratos_dev", + want: "V2KratosDev", + }, + { + name: "www_Google_com", + want: "WwwGoogleCom", + }, + { + name: "wwwBaidu_com", + want: "WwwBaiduCom", + }, + { + name: "HelloWorld", + want: "HelloWorld", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toUpperCamelCase(tt.name); got != tt.want { + t.Errorf("toUpperCamelCase() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/kratos/internal/proto/add/proto.go b/cmd/kratos/internal/proto/add/proto.go new file mode 100644 index 0000000..88184a2 --- /dev/null +++ b/cmd/kratos/internal/proto/add/proto.go @@ -0,0 +1,40 @@ +package add + +import ( + "fmt" + "os" + "path/filepath" +) + +// Proto is a proto generator. +type Proto struct { + Name string + Path string + Service string + Package string + GoPackage string + JavaPackage string +} + +// Generate generate a proto template. +func (p *Proto) Generate() error { + body, err := p.execute() + if err != nil { + return err + } + wd, err := os.Getwd() + if err != nil { + panic(err) + } + to := filepath.Join(wd, p.Path) + if _, err := os.Stat(to); os.IsNotExist(err) { + if err := os.MkdirAll(to, 0o700); err != nil { + return err + } + } + name := filepath.Join(to, p.Name) + if _, err := os.Stat(name); !os.IsNotExist(err) { + return fmt.Errorf("%s already exists", p.Name) + } + return os.WriteFile(name, body, 0o644) +} diff --git a/cmd/kratos/internal/proto/add/template.go b/cmd/kratos/internal/proto/add/template.go new file mode 100644 index 0000000..c518a17 --- /dev/null +++ b/cmd/kratos/internal/proto/add/template.go @@ -0,0 +1,52 @@ +package add + +import ( + "bytes" + "strings" + "text/template" +) + +const protoTemplate = ` +syntax = "proto3"; + +package {{.Package}}; + +option go_package = "{{.GoPackage}}"; +option java_multiple_files = true; +option java_package = "{{.JavaPackage}}"; + +service {{.Service}} { + rpc Create{{.Service}} (Create{{.Service}}Request) returns (Create{{.Service}}Reply); + rpc Update{{.Service}} (Update{{.Service}}Request) returns (Update{{.Service}}Reply); + rpc Delete{{.Service}} (Delete{{.Service}}Request) returns (Delete{{.Service}}Reply); + rpc Get{{.Service}} (Get{{.Service}}Request) returns (Get{{.Service}}Reply); + rpc List{{.Service}} (List{{.Service}}Request) returns (List{{.Service}}Reply); +} + +message Create{{.Service}}Request {} +message Create{{.Service}}Reply {} + +message Update{{.Service}}Request {} +message Update{{.Service}}Reply {} + +message Delete{{.Service}}Request {} +message Delete{{.Service}}Reply {} + +message Get{{.Service}}Request {} +message Get{{.Service}}Reply {} + +message List{{.Service}}Request {} +message List{{.Service}}Reply {} +` + +func (p *Proto) execute() ([]byte, error) { + buf := new(bytes.Buffer) + tmpl, err := template.New("proto").Parse(strings.TrimSpace(protoTemplate)) + if err != nil { + return nil, err + } + if err := tmpl.Execute(buf, p); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/cmd/kratos/internal/proto/client/client.go b/cmd/kratos/internal/proto/client/client.go new file mode 100644 index 0000000..17ac6ca --- /dev/null +++ b/cmd/kratos/internal/proto/client/client.go @@ -0,0 +1,130 @@ +package client + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/spf13/cobra" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/base" +) + +// CmdClient represents the source command. +var CmdClient = &cobra.Command{ + Use: "client", + Short: "Generate the proto client code", + Long: "Generate the proto client code. Example: kratos proto client helloworld.proto", + Run: run, +} + +var protoPath string + +func init() { + if protoPath = os.Getenv("KRATOS_PROTO_PATH"); protoPath == "" { + protoPath = "./third_party" + } + CmdClient.Flags().StringVarP(&protoPath, "proto_path", "p", protoPath, "proto path") +} + +func run(_ *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Println("Please enter the proto file or directory") + return + } + var ( + err error + proto = strings.TrimSpace(args[0]) + ) + if err = look("protoc-gen-go", "protoc-gen-go-grpc", "protoc-gen-go-http", "protoc-gen-go-errors", "protoc-gen-openapi"); err != nil { + // update the kratos plugins + cmd := exec.Command("kratos", "upgrade") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err = cmd.Run(); err != nil { + fmt.Println(err) + return + } + } + if strings.HasSuffix(proto, ".proto") { + err = generate(proto, args) + } else { + err = walk(proto, args) + } + if err != nil { + fmt.Println(err) + } +} + +func look(name ...string) error { + for _, n := range name { + if _, err := exec.LookPath(n); err != nil { + return err + } + } + return nil +} + +func walk(dir string, args []string) error { + if dir == "" { + dir = "." + } + return filepath.Walk(dir, func(path string, _ os.FileInfo, _ error) error { + if ext := filepath.Ext(path); ext != ".proto" || strings.HasPrefix(path, "third_party") { + return nil + } + return generate(path, args) + }) +} + +// generate is used to execute the generate command for the specified proto file +func generate(proto string, args []string) error { + input := []string{ + "--proto_path=.", + } + if pathExists(protoPath) { + input = append(input, "--proto_path="+protoPath) + } + inputExt := []string{ + "--proto_path=" + base.KratosMod(), + "--proto_path=" + filepath.Join(base.KratosMod(), "third_party"), + "--go_out=paths=source_relative:.", + "--go-grpc_out=paths=source_relative:.", + "--go-http_out=paths=source_relative:.", + "--go-errors_out=paths=source_relative:.", + "--openapi_out=paths=source_relative:.", + } + input = append(input, inputExt...) + protoBytes, err := os.ReadFile(proto) + if err == nil && len(protoBytes) > 0 { + if ok, _ := regexp.Match(`\n[^/]*(import)\s+"validate/validate.proto"`, protoBytes); ok { + input = append(input, "--validate_out=lang=go,paths=source_relative:.") + } + } + input = append(input, proto) + for _, a := range args { + if strings.HasPrefix(a, "-") { + input = append(input, a) + } + } + fd := exec.Command("protoc", input...) + fd.Stdout = os.Stdout + fd.Stderr = os.Stderr + fd.Dir = "." + if err := fd.Run(); err != nil { + return err + } + fmt.Printf("proto: %s\n", proto) + return nil +} + +func pathExists(path string) bool { + _, err := os.Stat(path) + if err != nil { + return os.IsExist(err) + } + return true +} diff --git a/cmd/kratos/internal/proto/proto.go b/cmd/kratos/internal/proto/proto.go new file mode 100644 index 0000000..d025185 --- /dev/null +++ b/cmd/kratos/internal/proto/proto.go @@ -0,0 +1,22 @@ +package proto + +import ( + "github.com/spf13/cobra" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/proto/add" + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/proto/client" + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/proto/server" +) + +// CmdProto represents the proto command. +var CmdProto = &cobra.Command{ + Use: "proto", + Short: "Generate the proto files", + Long: "Generate the proto files.", +} + +func init() { + CmdProto.AddCommand(add.CmdAdd) + CmdProto.AddCommand(client.CmdClient) + CmdProto.AddCommand(server.CmdServer) +} diff --git a/cmd/kratos/internal/proto/server/server.go b/cmd/kratos/internal/proto/server/server.go new file mode 100644 index 0000000..ebe3dc3 --- /dev/null +++ b/cmd/kratos/internal/proto/server/server.go @@ -0,0 +1,124 @@ +package server + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/emicklei/proto" + "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// CmdServer the service command. +var CmdServer = &cobra.Command{ + Use: "server", + Short: "Generate the proto server implementations", + Long: "Generate the proto server implementations. Example: kratos proto server api/xxx.proto --target-dir=internal/service", + Run: run, +} +var targetDir string + +func init() { + CmdServer.Flags().StringVarP(&targetDir, "target-dir", "t", "internal/service", "generate target directory") +} + +func run(_ *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "Please specify the proto file. Example: kratos proto server api/xxx.proto") + return + } + reader, err := os.Open(args[0]) + if err != nil { + log.Fatalf("Failed to open proto file %q: %v", args[0], err) + } + defer reader.Close() + + parser := proto.NewParser(reader) + definition, err := parser.Parse() + if err != nil { + log.Fatalf("Failed to parse proto file %q: %v", args[0], err) + } + + var ( + pkg string + res []*Service + ) + proto.Walk(definition, + proto.WithOption(func(o *proto.Option) { + if o.Name == "go_package" { + pkg = strings.Split(o.Constant.Source, ";")[0] + } + }), + proto.WithService(func(s *proto.Service) { + cs := &Service{ + Package: pkg, + Service: serviceName(s.Name), + } + for _, e := range s.Elements { + r, ok := e.(*proto.RPC) + if !ok { + continue + } + cs.Methods = append(cs.Methods, &Method{ + Service: serviceName(s.Name), Name: rpcName(r.Name), Request: parametersName(r.RequestType), + Reply: parametersName(r.ReturnsType), Type: getMethodType(r.StreamsRequest, r.StreamsReturns), + }) + } + res = append(res, cs) + }), + ) + if _, err := os.Stat(targetDir); os.IsNotExist(err) { + fmt.Printf("Target directory: %s does not exist\n", targetDir) + return + } + for _, s := range res { + to := filepath.Join(targetDir, strings.ToLower(s.Service)+".go") + if _, err := os.Stat(to); !os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "%s already exists: %s\n", s.Service, to) + continue + } + b, err := s.execute() + if err != nil { + log.Fatalf("Failed to generate server implementation for %s: %v", s.Service, err) + } + if err := os.WriteFile(to, b, 0o644); err != nil { + log.Fatalf("Failed to write server file %q: %v", to, err) + } + fmt.Println(to) + } +} + +func getMethodType(streamsRequest, streamsReturns bool) MethodType { + if !streamsRequest && !streamsReturns { + return unaryType + } else if streamsRequest && streamsReturns { + return twoWayStreamsType + } else if streamsRequest { + return requestStreamsType + } else if streamsReturns { + return returnsStreamsType + } + return unaryType +} + +func parametersName(name string) string { + return strings.ReplaceAll(name, ".", "_") +} + +func serviceName(name string) string { + return strings.TrimSuffix(toUpperCamelCase(strings.Split(name, ".")[0]), "Service") +} + +func rpcName(name string) string { + return toUpperCamelCase(strings.Split(name, ".")[0]) +} + +func toUpperCamelCase(s string) string { + s = strings.ReplaceAll(s, "_", " ") + s = cases.Title(language.Und, cases.NoLower).String(s) + return strings.ReplaceAll(s, " ", "") +} diff --git a/cmd/kratos/internal/proto/server/server_test.go b/cmd/kratos/internal/proto/server/server_test.go new file mode 100644 index 0000000..3c190a9 --- /dev/null +++ b/cmd/kratos/internal/proto/server/server_test.go @@ -0,0 +1,117 @@ +package server + +import "testing" + +func Test_serviceName(t *testing.T) { + type args struct { + str string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "serviceName on lowercase words", + args: args{str: "helloworld"}, + want: "Helloworld", + }, + { + name: "serviceName on uppercase words", + args: args{str: "HELLOWORLD"}, + want: "HELLOWORLD", + }, + { + name: "serviceName on lowercase words with spaces", + args: args{str: "hello world"}, + want: "HelloWorld", + }, + { + name: "serviceName on uppercase words with spaces", + args: args{str: "HELLO WORLD"}, + want: "HELLOWORLD", + }, + { + name: "serviceName on Lower Camel Case words", + args: args{str: "helloWorld"}, + want: "HelloWorld", + }, + { + name: "serviceName on Lower Camel Case words", + args: args{str: "helloWorld"}, + want: "HelloWorld", + }, + { + name: "serviceName on Upper Camel Case words", + args: args{str: "HelloWorld"}, + want: "HelloWorld", + }, + { + name: "serviceName on Upper Camel Case words", + args: args{str: "hello_world"}, + want: "HelloWorld", + }, + { + name: "serviceName with service suffix", + args: args{str: "HelloWorldService"}, + want: "HelloWorld", + }, + { + name: "serviceName with space and service suffix", + args: args{str: "Hello world service"}, + want: "HelloWorld", + }, + { + name: "serviceName with snake case and service suffix", + args: args{str: "hello_world_service"}, + want: "HelloWorld", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := serviceName(tt.args.str); got != tt.want { + t.Errorf("serviceName() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parametersName(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "parametersName on not nested", + args: args{ + name: "MessageResponse", + }, + want: "MessageResponse", + }, + { + name: "parametersName on One layer of nesting", + args: args{ + name: "Message.Response", + }, + want: "Message_Response", + }, + { + name: "parametersName on Two layer of nesting", + args: args{ + name: "Message.Message2.Response", + }, + want: "Message_Message2_Response", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parametersName(tt.args.name); got != tt.want { + t.Errorf("parametersName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/kratos/internal/proto/server/template.go b/cmd/kratos/internal/proto/server/template.go new file mode 100644 index 0000000..32f723a --- /dev/null +++ b/cmd/kratos/internal/proto/server/template.go @@ -0,0 +1,147 @@ +package server + +import ( + "bytes" + "html/template" +) + +//nolint:lll +var serviceTemplate = ` +{{- /* delete empty line */ -}} +package service + +import ( + {{- if .UseContext }} + "context" + {{- end }} + {{- if .UseIO }} + "io" + {{- end }} + + pb "{{ .Package }}" + {{- if .GoogleEmpty }} + "google.golang.org/protobuf/types/known/emptypb" + {{- end }} +) + +type {{ .Service }}Service struct { + pb.Unimplemented{{ .Service }}Server +} + +func New{{ .Service }}Service() *{{ .Service }}Service { + return &{{ .Service }}Service{} +} + +{{- $s1 := "google.protobuf.Empty" }} +{{- $s2 := "google_protobuf_Empty" }} +{{ range .Methods }} +{{- if eq .Type 1 }} +func (s *{{ .Service }}Service) {{ .Name }}(ctx context.Context, req {{ if or (eq .Request $s1) (eq .Request $s2) }}*emptypb.Empty{{ else }}*pb.{{ .Request }}{{ end }}) ({{ if or (eq .Reply $s1) (eq .Reply $s2) }}*emptypb.Empty{{ else }}*pb.{{ .Reply }}{{ end }}, error) { + return {{ if or (eq .Reply $s1) (eq .Reply $s2) }}&emptypb.Empty{}{{ else }}&pb.{{ .Reply }}{}{{ end }}, nil +} + +{{- else if eq .Type 2 }} +func (s *{{ .Service }}Service) {{ .Name }}(conn pb.{{ .Service }}_{{ .Name }}Server) error { + for { + req, err := conn.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + err = conn.Send(&pb.{{ .Reply }}{}) + if err != nil { + return err + } + } +} + +{{- else if eq .Type 3 }} +func (s *{{ .Service }}Service) {{ .Name }}(conn pb.{{ .Service }}_{{ .Name }}Server) error { + for { + req, err := conn.Recv() + if err == io.EOF { + return conn.SendAndClose(&pb.{{ .Reply }}{}) + } + if err != nil { + return err + } + } +} + +{{- else if eq .Type 4 }} +func (s *{{ .Service }}Service) {{ .Name }}(req {{ if or (eq .Request $s1) (eq .Request $s2) }}*emptypb.Empty{{ else }}*pb.{{ .Request }}{{ end }}, conn pb.{{ .Service }}_{{ .Name }}Server) error { + for { + err := conn.Send(&pb.{{ .Reply }}{}) + if err != nil { + return err + } + } +} + +{{- end }} +{{- end }} +` + +type MethodType uint8 + +const ( + unaryType MethodType = 1 + twoWayStreamsType MethodType = 2 + requestStreamsType MethodType = 3 + returnsStreamsType MethodType = 4 +) + +// Service is a proto service. +type Service struct { + Package string + Service string + Methods []*Method + GoogleEmpty bool + + UseIO bool + UseContext bool +} + +// Method is a proto method. +type Method struct { + Service string + Name string + Request string + Reply string + + // type: unary or stream + Type MethodType +} + +func (s *Service) execute() ([]byte, error) { + const empty = "google.protobuf.Empty" + // another empty style + const emptyV2 = "google_protobuf_Empty" + buf := new(bytes.Buffer) + for _, method := range s.Methods { + isReqEmpty := method.Request == empty || method.Request == emptyV2 + isReplyEmpty := method.Reply == empty || method.Reply == emptyV2 + + if (method.Type == unaryType && (isReqEmpty || isReplyEmpty)) || + (method.Type == returnsStreamsType && isReqEmpty) { + s.GoogleEmpty = true + } + if method.Type == twoWayStreamsType || method.Type == requestStreamsType { + s.UseIO = true + } + if method.Type == unaryType { + s.UseContext = true + } + } + tmpl, err := template.New("service").Parse(serviceTemplate) + if err != nil { + return nil, err + } + if err := tmpl.Execute(buf, s); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/cmd/kratos/internal/run/run.go b/cmd/kratos/internal/run/run.go new file mode 100644 index 0000000..3623c24 --- /dev/null +++ b/cmd/kratos/internal/run/run.go @@ -0,0 +1,145 @@ +package run + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/spf13/cobra" +) + +// CmdRun run project command. +var CmdRun = &cobra.Command{ + Use: "run", + Short: "Run project", + Long: "Run project. Example: kratos run", + Run: Run, +} +var targetDir string + +func init() { + CmdRun.Flags().StringVarP(&targetDir, "work", "w", "", "target working directory") +} + +// Run run project. +func Run(cmd *cobra.Command, args []string) { + var dir string + cmdArgs, programArgs := splitArgs(cmd, args) + if len(cmdArgs) > 0 { + dir = cmdArgs[0] + } + base, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err) + return + } + if dir == "" { + // find the directory containing the cmd/* + cmdPath, err := findCMD(base) + if err != nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err) + return + } + switch len(cmdPath) { + case 0: + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", "The cmd directory cannot be found in the current directory") + return + case 1: + for _, v := range cmdPath { + dir = v + } + default: + var cmdPaths []string + for k := range cmdPath { + cmdPaths = append(cmdPaths, k) + } + prompt := &survey.Select{ + Message: "Which directory do you want to run?", + Options: cmdPaths, + PageSize: 10, + } + e := survey.AskOne(prompt, &dir) + if e != nil || dir == "" { + return + } + dir = cmdPath[dir] + } + } + fd := exec.Command("go", append([]string{"run", dir}, programArgs...)...) + fd.Stdout = os.Stdout + fd.Stderr = os.Stderr + fd.Dir = dir + changeWorkingDirectory(fd, targetDir) + if err := fd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err.Error()) + return + } +} + +func splitArgs(cmd *cobra.Command, args []string) (cmdArgs, programArgs []string) { + dashAt := cmd.ArgsLenAtDash() + if dashAt >= 0 { + return args[:dashAt], args[dashAt:] + } + return args, []string{} +} + +func findCMD(base string) (map[string]string, error) { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + if !strings.HasSuffix(wd, "/") { + wd += "/" + } + var root bool + next := func(dir string) (map[string]string, error) { + cmdPath := make(map[string]string) + err := filepath.Walk(dir, func(walkPath string, info os.FileInfo, _ error) error { + // multi level directory is not allowed under the cmdPath directory, so it is judged that the path ends with cmdPath. + if strings.HasSuffix(walkPath, "cmd") { + paths, err := os.ReadDir(walkPath) + if err != nil { + return err + } + for _, fileInfo := range paths { + if fileInfo.IsDir() { + abs := filepath.Join(walkPath, fileInfo.Name()) + cmdPath[strings.TrimPrefix(abs, wd)] = abs + } + } + return nil + } + if info.Name() == "go.mod" { + root = true + } + return nil + }) + return cmdPath, err + } + for i := 0; i < 5; i++ { + tmp := base + cmd, err := next(tmp) + if err != nil { + return nil, err + } + if len(cmd) > 0 { + return cmd, nil + } + if root { + break + } + base = filepath.Join(base, "..") + } + return map[string]string{"": base}, nil +} + +func changeWorkingDirectory(cmd *exec.Cmd, targetDir string) { + targetDir = strings.TrimSpace(targetDir) + if targetDir != "" { + cmd.Dir = targetDir + } +} diff --git a/cmd/kratos/internal/upgrade/upgrade.go b/cmd/kratos/internal/upgrade/upgrade.go new file mode 100644 index 0000000..c2268c8 --- /dev/null +++ b/cmd/kratos/internal/upgrade/upgrade.go @@ -0,0 +1,32 @@ +package upgrade + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/base" +) + +// CmdUpgrade represents the upgrade command. +var CmdUpgrade = &cobra.Command{ + Use: "upgrade", + Short: "Upgrade the kratos tools", + Long: "Upgrade the kratos tools. Example: kratos upgrade", + Run: Run, +} + +// Run upgrade the kratos tools. +func Run(_ *cobra.Command, _ []string) { + err := base.GoInstall( + "github.com/go-kratos/kratos/cmd/kratos/v3@latest", + "github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v3@latest", + "github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v3@latest", + "google.golang.org/protobuf/cmd/protoc-gen-go@latest", + "google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest", + "github.com/google/gnostic/cmd/protoc-gen-openapi@latest", + ) + if err != nil { + fmt.Println(err) + } +} diff --git a/cmd/kratos/main.go b/cmd/kratos/main.go new file mode 100644 index 0000000..d4c9073 --- /dev/null +++ b/cmd/kratos/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + + "github.com/spf13/cobra" + + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/change" + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/project" + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/proto" + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/run" + "github.com/go-kratos/kratos/cmd/kratos/v3/internal/upgrade" +) + +var rootCmd = &cobra.Command{ + Use: "kratos", + Short: "Kratos: An elegant toolkit for Go microservices.", + Long: `Kratos: An elegant toolkit for Go microservices.`, + Version: release, +} + +func init() { + rootCmd.AddCommand(project.CmdNew) + rootCmd.AddCommand(proto.CmdProto) + rootCmd.AddCommand(upgrade.CmdUpgrade) + rootCmd.AddCommand(change.CmdChange) + rootCmd.AddCommand(run.CmdRun) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + log.Fatalf("Failed to execute command: %v", err) + } +} diff --git a/cmd/kratos/version.go b/cmd/kratos/version.go new file mode 100644 index 0000000..ef16cd9 --- /dev/null +++ b/cmd/kratos/version.go @@ -0,0 +1,4 @@ +package main + +// release is the current kratos tool version. +const release = "v3.0.0" diff --git a/cmd/protoc-gen-go-errors/buf.gen.yaml b/cmd/protoc-gen-go-errors/buf.gen.yaml new file mode 100644 index 0000000..27ea54a --- /dev/null +++ b/cmd/protoc-gen-go-errors/buf.gen.yaml @@ -0,0 +1,6 @@ +version: v2 +plugins: + - remote: buf.build/protocolbuffers/go:v1.33.0 + out: . + opt: + - paths=source_relative diff --git a/cmd/protoc-gen-go-errors/buf.lock b/cmd/protoc-gen-go-errors/buf.lock new file mode 100644 index 0000000..4f98143 --- /dev/null +++ b/cmd/protoc-gen-go-errors/buf.lock @@ -0,0 +1,2 @@ +# Generated by buf. DO NOT EDIT. +version: v2 diff --git a/cmd/protoc-gen-go-errors/buf.yaml b/cmd/protoc-gen-go-errors/buf.yaml new file mode 100644 index 0000000..8c3d4dd --- /dev/null +++ b/cmd/protoc-gen-go-errors/buf.yaml @@ -0,0 +1,16 @@ +version: v2 +name: buf.build/go-kratos/protoc-gen-go-errors +lint: + use: + - DEFAULT + except: + - PACKAGE_DIRECTORY_MATCH + - ENUM_VALUE_UPPER_SNAKE_CASE + - ENUM_VALUE_PREFIX + - ENUM_ZERO_VALUE_SUFFIX + - FIELD_LOWER_SNAKE_CASE +deps: + - buf.build/googleapis/googleapis +breaking: + use: + - FILE diff --git a/cmd/protoc-gen-go-errors/errors.go b/cmd/protoc-gen-go-errors/errors.go new file mode 100644 index 0000000..a8b8584 --- /dev/null +++ b/cmd/protoc-gen-go-errors/errors.go @@ -0,0 +1,135 @@ +package main + +import ( + "fmt" + "strings" + "unicode" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v3/errors" +) + +const ( + errorsPackage = protogen.GoImportPath("github.com/go-kratos/kratos/v3/errors") + fmtPackage = protogen.GoImportPath("fmt") +) + +var enCases = cases.Title(language.AmericanEnglish, cases.NoLower) + +// generateFile generates a _errors.pb.go file containing kratos errors definitions. +func generateFile(gen *protogen.Plugin, file *protogen.File) *protogen.GeneratedFile { + if len(file.Enums) == 0 { + return nil + } + filename := file.GeneratedFilenamePrefix + "_errors.pb.go" + g := gen.NewGeneratedFile(filename, file.GoImportPath) + g.P("// Code generated by protoc-gen-go-errors. DO NOT EDIT.") + g.P() + g.P("package ", file.GoPackageName) + g.P() + g.QualifiedGoIdent(fmtPackage.Ident("")) + generateFileContent(gen, file, g) + return g +} + +// generateFileContent generates the kratos errors definitions, excluding the package statement. +func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { + if len(file.Enums) == 0 { + return + } + + g.P("// This is a compile-time assertion to ensure that this generated file") + g.P("// is compatible with the kratos package it is being compiled against.") + g.P("const _ = ", errorsPackage.Ident("SupportPackageIsVersion1")) + g.P() + index := 0 + for _, enum := range file.Enums { + if !genErrorsReason(gen, file, g, enum) { + index++ + } + } + // If all enums do not contain 'errors.code', the current file is skipped + if index == 0 { + g.Skip() + } +} + +func genErrorsReason(_ *protogen.Plugin, _ *protogen.File, g *protogen.GeneratedFile, enum *protogen.Enum) bool { + defaultCode := proto.GetExtension(enum.Desc.Options(), errors.E_DefaultCode) + code := 0 + if ok := defaultCode.(int32); ok != 0 { + code = int(ok) + } + if code > 600 || code < 0 { + panic(fmt.Sprintf("Enum '%s' range must be greater than 0 and less than or equal to 600", string(enum.Desc.Name()))) + } + var ew errorWrapper + for _, v := range enum.Values { + enumCode := code + eCode := proto.GetExtension(v.Desc.Options(), errors.E_Code) + if ok := eCode.(int32); ok != 0 { + enumCode = int(ok) + } + // If the current enumeration does not contain 'errors.code' + // or the code value exceeds the range, the current enum will be skipped + if enumCode > 600 || enumCode < 0 { + panic(fmt.Sprintf("Enum '%s' range must be greater than 0 and less than or equal to 600", string(v.Desc.Name()))) + } + if enumCode == 0 { + continue + } + + comment := v.Comments.Leading.String() + if comment == "" { + comment = v.Comments.Trailing.String() + } + + err := &errorInfo{ + Name: string(enum.Desc.Name()), + Value: string(v.Desc.Name()), + CamelValue: case2Camel(string(v.Desc.Name())), + HTTPCode: enumCode, + Comment: comment, + HasComment: len(comment) > 0, + } + ew.Errors = append(ew.Errors, err) + } + if len(ew.Errors) == 0 { + return true + } + g.P(ew.execute()) + + return false +} + +func case2Camel(name string) string { + if !strings.Contains(name, "_") { + if name == strings.ToUpper(name) { + name = strings.ToLower(name) + } + return enCases.String(name) + } + strs := strings.Split(name, "_") + words := make([]string, 0, len(strs)) + for _, w := range strs { + hasLower := false + for _, r := range w { + if unicode.IsLower(r) { + hasLower = true + break + } + } + if !hasLower { + w = strings.ToLower(w) + } + w = enCases.String(w) + words = append(words, w) + } + + return strings.Join(words, "") +} diff --git a/cmd/protoc-gen-go-errors/errors/errors.pb.go b/cmd/protoc-gen-go-errors/errors/errors.pb.go new file mode 100644 index 0000000..16eb19d --- /dev/null +++ b/cmd/protoc-gen-go-errors/errors/errors.pb.go @@ -0,0 +1,229 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.15.7 +// source: errors.proto + +package errors + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + descriptorpb "google.golang.org/protobuf/types/descriptorpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Error struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *Error) Reset() { + *x = Error{} + if protoimpl.UnsafeEnabled { + mi := &file_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_errors_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *Error) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Error) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *Error) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Error) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +var file_errors_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*descriptorpb.EnumOptions)(nil), + ExtensionType: (*int32)(nil), + Field: 1108, + Name: "errors.default_code", + Tag: "varint,1108,opt,name=default_code", + Filename: "errors.proto", + }, + { + ExtendedType: (*descriptorpb.EnumValueOptions)(nil), + ExtensionType: (*int32)(nil), + Field: 1109, + Name: "errors.code", + Tag: "varint,1109,opt,name=code", + Filename: "errors.proto", + }, +} + +// Extension fields to descriptorpb.EnumOptions. +var ( + // optional int32 default_code = 1108; + E_DefaultCode = &file_errors_proto_extTypes[0] +) + +// Extension fields to descriptorpb.EnumValueOptions. +var ( + // optional int32 code = 1109; + E_Code = &file_errors_proto_extTypes[1] +) + +var File_errors_proto protoreflect.FileDescriptor + +var file_errors_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc3, 0x01, 0x0a, 0x05, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x3a, 0x40, + 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x1c, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd4, 0x08, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x64, 0x65, + 0x3a, 0x36, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd5, 0x08, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x42, 0x59, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, + 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x3b, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0xa2, 0x02, 0x0c, 0x4b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_errors_proto_rawDescOnce sync.Once + file_errors_proto_rawDescData = file_errors_proto_rawDesc +) + +func file_errors_proto_rawDescGZIP() []byte { + file_errors_proto_rawDescOnce.Do(func() { + file_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_errors_proto_rawDescData) + }) + return file_errors_proto_rawDescData +} + +var file_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_errors_proto_goTypes = []interface{}{ + (*Error)(nil), // 0: errors.Error + nil, // 1: errors.Error.MetadataEntry + (*descriptorpb.EnumOptions)(nil), // 2: google.protobuf.EnumOptions + (*descriptorpb.EnumValueOptions)(nil), // 3: google.protobuf.EnumValueOptions +} +var file_errors_proto_depIdxs = []int32{ + 1, // 0: errors.Error.metadata:type_name -> errors.Error.MetadataEntry + 2, // 1: errors.default_code:extendee -> google.protobuf.EnumOptions + 3, // 2: errors.code:extendee -> google.protobuf.EnumValueOptions + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 1, // [1:3] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_errors_proto_init() } +func file_errors_proto_init() { + if File_errors_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Error); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_errors_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 2, + NumServices: 0, + }, + GoTypes: file_errors_proto_goTypes, + DependencyIndexes: file_errors_proto_depIdxs, + MessageInfos: file_errors_proto_msgTypes, + ExtensionInfos: file_errors_proto_extTypes, + }.Build() + File_errors_proto = out.File + file_errors_proto_rawDesc = nil + file_errors_proto_goTypes = nil + file_errors_proto_depIdxs = nil +} diff --git a/cmd/protoc-gen-go-errors/errors/errors.proto b/cmd/protoc-gen-go-errors/errors/errors.proto new file mode 100644 index 0000000..806d717 --- /dev/null +++ b/cmd/protoc-gen-go-errors/errors/errors.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package errors; + +option go_package = "github.com/go-kratos/kratos/v3/errors;errors"; +option java_multiple_files = true; +option java_package = "com.github.kratos.errors"; +option objc_class_prefix = "KratosErrors"; + +import "google/protobuf/descriptor.proto"; + +message Error { + int32 code = 1; + string reason = 2; + string message = 3; + map metadata = 4; +}; + +extend google.protobuf.EnumOptions { + int32 default_code = 1108; +} + +extend google.protobuf.EnumValueOptions { + int32 code = 1109; +} diff --git a/cmd/protoc-gen-go-errors/errorsTemplate.tpl b/cmd/protoc-gen-go-errors/errorsTemplate.tpl new file mode 100644 index 0000000..1c322eb --- /dev/null +++ b/cmd/protoc-gen-go-errors/errorsTemplate.tpl @@ -0,0 +1,17 @@ +{{ range .Errors }} + +{{ if .HasComment }}{{ .Comment }}{{ end -}} +func Is{{.CamelValue}}(err error) bool { + if err == nil { + return false + } + e := errors.FromError(err) + return e.Reason == {{ .Name }}_{{ .Value }}.String() && e.Code == {{ .HTTPCode }} +} + +{{ if .HasComment }}{{ .Comment }}{{ end -}} +func Error{{ .CamelValue }}(format string, args ...interface{}) *errors.Error { + return errors.New({{ .HTTPCode }}, {{ .Name }}_{{ .Value }}.String(), fmt.Sprintf(format, args...)) +} + +{{- end }} diff --git a/cmd/protoc-gen-go-errors/errors_test.go b/cmd/protoc-gen-go-errors/errors_test.go new file mode 100644 index 0000000..e7de565 --- /dev/null +++ b/cmd/protoc-gen-go-errors/errors_test.go @@ -0,0 +1,62 @@ +package main + +import "testing" + +func Test_case2Camel(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "snake1", + args: args{"SYSTEM_ERROR"}, + want: "SystemError", + }, + { + name: "snake2", + args: args{"System_Error"}, + want: "SystemError", + }, + { + name: "snake3", + args: args{"system_error"}, + want: "SystemError", + }, + { + name: "snake4", + args: args{"System_error"}, + want: "SystemError", + }, + { + name: "upper1", + args: args{"UNKNOWN"}, + want: "Unknown", + }, + { + name: "camel1", + args: args{"SystemError"}, + want: "SystemError", + }, + { + name: "camel2", + args: args{"systemError"}, + want: "SystemError", + }, + { + name: "lower1", + args: args{"system"}, + want: "System", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := case2Camel(tt.args.name); got != tt.want { + t.Errorf("case2Camel() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/protoc-gen-go-errors/go.mod b/cmd/protoc-gen-go-errors/go.mod new file mode 100644 index 0000000..daccead --- /dev/null +++ b/cmd/protoc-gen-go-errors/go.mod @@ -0,0 +1,8 @@ +module github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v3 + +go 1.25.0 + +require ( + golang.org/x/text v0.37.0 + google.golang.org/protobuf v1.36.11 +) diff --git a/cmd/protoc-gen-go-errors/go.sum b/cmd/protoc-gen-go-errors/go.sum new file mode 100644 index 0000000..33836cb --- /dev/null +++ b/cmd/protoc-gen-go-errors/go.sum @@ -0,0 +1,6 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/cmd/protoc-gen-go-errors/main.go b/cmd/protoc-gen-go-errors/main.go new file mode 100644 index 0000000..cb596af --- /dev/null +++ b/cmd/protoc-gen-go-errors/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "flag" + "fmt" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/types/pluginpb" +) + +var showVersion = flag.Bool("version", false, "print the version and exit") + +func main() { + flag.Parse() + if *showVersion { + fmt.Printf("protoc-gen-go-errors %v\n", release) + return + } + var flags flag.FlagSet + protogen.Options{ + ParamFunc: flags.Set, + }.Run(func(gen *protogen.Plugin) error { + gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + for _, f := range gen.Files { + if !f.Generate { + continue + } + generateFile(gen, f) + } + return nil + }) +} diff --git a/cmd/protoc-gen-go-errors/template.go b/cmd/protoc-gen-go-errors/template.go new file mode 100644 index 0000000..2485082 --- /dev/null +++ b/cmd/protoc-gen-go-errors/template.go @@ -0,0 +1,35 @@ +package main + +import ( + "bytes" + _ "embed" + "text/template" +) + +//go:embed errorsTemplate.tpl +var errorsTemplate string + +type errorInfo struct { + Name string + Value string + HTTPCode int + CamelValue string + Comment string + HasComment bool +} + +type errorWrapper struct { + Errors []*errorInfo +} + +func (e *errorWrapper) execute() string { + buf := new(bytes.Buffer) + tmpl, err := template.New("errors").Parse(errorsTemplate) + if err != nil { + panic(err) + } + if err := tmpl.Execute(buf, e); err != nil { + panic(err) + } + return buf.String() +} diff --git a/cmd/protoc-gen-go-errors/version.go b/cmd/protoc-gen-go-errors/version.go new file mode 100644 index 0000000..7e57eae --- /dev/null +++ b/cmd/protoc-gen-go-errors/version.go @@ -0,0 +1,4 @@ +package main + +// release is the current protoc-gen-go-errors version. +const release = "v3.0.0" diff --git a/cmd/protoc-gen-go-http/.gitignore b/cmd/protoc-gen-go-http/.gitignore new file mode 100644 index 0000000..1843eeb --- /dev/null +++ b/cmd/protoc-gen-go-http/.gitignore @@ -0,0 +1,2 @@ +# Compiled plugin binary produced by `go build` in this directory. +/protoc-gen-go-http diff --git a/cmd/protoc-gen-go-http/go.mod b/cmd/protoc-gen-go-http/go.mod new file mode 100644 index 0000000..c14e25b --- /dev/null +++ b/cmd/protoc-gen-go-http/go.mod @@ -0,0 +1,7 @@ +module github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v3 + +go 1.25.0 + +require google.golang.org/protobuf v1.36.11 + +require google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 diff --git a/cmd/protoc-gen-go-http/go.sum b/cmd/protoc-gen-go-http/go.sum new file mode 100644 index 0000000..bb6723e --- /dev/null +++ b/cmd/protoc-gen-go-http/go.sum @@ -0,0 +1,6 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/cmd/protoc-gen-go-http/http.go b/cmd/protoc-gen-go-http/http.go new file mode 100644 index 0000000..bb56e60 --- /dev/null +++ b/cmd/protoc-gen-go-http/http.go @@ -0,0 +1,395 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "regexp" + "strings" + + "google.golang.org/protobuf/reflect/protoreflect" + + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +const ( + contextPackage = protogen.GoImportPath("context") + transportHTTPPackage = protogen.GoImportPath("github.com/go-kratos/kratos/v3/transport/http") + httpBodyFullName = protoreflect.FullName("google.api.HttpBody") +) + +var methodSets = make(map[string]int) + +// generateFile generates a _http.pb.go file containing kratos errors definitions. +func generateFile(gen *protogen.Plugin, file *protogen.File, omitempty bool, omitemptyPrefix string) *protogen.GeneratedFile { + if len(file.Services) == 0 || (omitempty && !hasHTTPRule(file.Services)) { + return nil + } + filename := file.GeneratedFilenamePrefix + "_http.pb.go" + g := gen.NewGeneratedFile(filename, file.GoImportPath) + g.P("// Code generated by protoc-gen-go-http. DO NOT EDIT.") + g.P("// versions:") + g.P(fmt.Sprintf("// - protoc-gen-go-http %s", release)) + g.P("// - protoc ", protocVersion(gen)) + if file.Proto.GetOptions().GetDeprecated() { + g.P("// ", file.Desc.Path(), " is a deprecated file.") + } else { + g.P("// source: ", file.Desc.Path()) + } + g.P() + g.P("package ", file.GoPackageName) + g.P() + generateFileContent(gen, file, g, omitempty, omitemptyPrefix) + return g +} + +// generateFileContent generates the kratos errors definitions, excluding the package statement. +func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, omitempty bool, omitemptyPrefix string) { + if len(file.Services) == 0 { + return + } + g.P("// This is a compile-time assertion to ensure that this generated file") + g.P("// is compatible with the kratos package it is being compiled against.") + g.P("var _ = new(", contextPackage.Ident("Context"), ")") + g.P("const _ = ", transportHTTPPackage.Ident("SupportPackageIsVersion3")) + g.P() + + for _, service := range file.Services { + genService(gen, file, g, service, omitempty, omitemptyPrefix) + } +} + +func genService(_ *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, service *protogen.Service, omitempty bool, omitemptyPrefix string) { + if service.Desc.Options().(*descriptorpb.ServiceOptions).GetDeprecated() { + g.P("//") + g.P(deprecationComment) + } + // HTTP Server. + sd := &serviceDesc{ + ServiceType: service.GoName, + ServiceName: string(service.Desc.FullName()), + Metadata: file.Desc.Path(), + } + for _, method := range service.Methods { + rule, ok := proto.GetExtension(method.Desc.Options(), annotations.E_Http).(*annotations.HttpRule) + if rule != nil && ok { + for _, bind := range rule.AdditionalBindings { + sd.Methods = append(sd.Methods, buildHTTPRule(g, service, method, bind, omitemptyPrefix)) + } + sd.Methods = append(sd.Methods, buildHTTPRule(g, service, method, rule, omitemptyPrefix)) + } else if !omitempty && !method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { + path := fmt.Sprintf("%s/%s/%s", omitemptyPrefix, service.Desc.FullName(), method.Desc.Name()) + sd.Methods = append(sd.Methods, buildMethodDesc(g, method, http.MethodPost, path)) + } + } + if len(sd.Methods) != 0 { + g.P(sd.execute()) + } +} + +func hasHTTPRule(services []*protogen.Service) bool { + for _, service := range services { + for _, method := range service.Methods { + rule, ok := proto.GetExtension(method.Desc.Options(), annotations.E_Http).(*annotations.HttpRule) + if rule != nil && ok { + return true + } + } + } + return false +} + +func buildHTTPRule(g *protogen.GeneratedFile, service *protogen.Service, m *protogen.Method, rule *annotations.HttpRule, omitemptyPrefix string) *methodDesc { + var ( + path string + method string + body string + responseBody string + ) + + switch pattern := rule.Pattern.(type) { + case *annotations.HttpRule_Get: + path = pattern.Get + method = http.MethodGet + case *annotations.HttpRule_Put: + path = pattern.Put + method = http.MethodPut + case *annotations.HttpRule_Post: + path = pattern.Post + method = http.MethodPost + case *annotations.HttpRule_Delete: + path = pattern.Delete + method = http.MethodDelete + case *annotations.HttpRule_Patch: + path = pattern.Patch + method = http.MethodPatch + case *annotations.HttpRule_Custom: + path = pattern.Custom.Path + method = pattern.Custom.Kind + } + if method == "" { + method = http.MethodPost + } + if path == "" { + path = fmt.Sprintf("%s/%s/%s", omitemptyPrefix, service.Desc.FullName(), m.Desc.Name()) + } + body = rule.Body + responseBody = rule.ResponseBody + md := buildMethodDesc(g, m, method, path) + // Client-streaming RPCs are served over WebSocket, whose handshake is always an + // HTTP GET regardless of the declared verb. Declaring a body for them is legitimate + // (it identifies the streamed message field), so skip the GET/DELETE body warnings. + if !m.Desc.IsStreamingClient() { + if method == http.MethodGet || method == http.MethodDelete { + if body != "" { + _, _ = fmt.Fprintf(os.Stderr, "\u001B[31mWARN\u001B[m: %s %s body should not be declared.\n", method, path) + } + } else { + if body == "" { + _, _ = fmt.Fprintf(os.Stderr, "\u001B[31mWARN\u001B[m: %s %s does not declare a body.\n", method, path) + } + } + } + if body == "*" { + md.HasBody = true + md.Body = "" + md.BodyField = "*" + md.BodyHTTPBody = isHTTPBodyMessage(m.Input.Desc) + } else if body != "" { + fd := m.Input.Desc.Fields().ByName(protoreflect.Name(body)) + if fd == nil { + fmt.Fprintf( + os.Stderr, + "\u001B[31mERROR\u001B[m: The corresponding body field '%s' declaration in request message could not be found in '%s'\n", + body, + path, + ) + os.Exit(2) + } + md.HasBody = true + md.Body = "." + camelCaseVars(body) + md.BodyField = body + md.BodyQueryName = fd.JSONName() + md.BodyHTTPBody = isHTTPBodyField(fd) + // A singular message-kind body field can be streamed frame-by-frame for + // client-streaming RPCs (each frame carries just this field's payload). + md.BodyMessage = fd.Kind() == protoreflect.MessageKind && !fd.IsList() && !fd.IsMap() + } else { + md.HasBody = false + } + if responseBody == "*" { + md.ResponseBody = "" + } else if responseBody != "" { + fd := m.Output.Desc.Fields().ByName(protoreflect.Name(responseBody)) + if fd == nil { + fmt.Fprintf( + os.Stderr, + "\u001B[31mERROR\u001B[m: The corresponding response_body field '%s' declaration in response message could not be found in '%s'\n", + responseBody, + path, + ) + os.Exit(2) + } + md.ResponseBody = "." + camelCaseVars(responseBody) + md.ResponseBodyHTTPBody = isHTTPBodyField(fd) + } + return md +} + +func buildMethodDesc(g *protogen.GeneratedFile, m *protogen.Method, method, path string) *methodDesc { + defer func() { methodSets[m.GoName]++ }() + + pathTemplate := path + vars := buildPathVars(path) + + for v, s := range vars { + fields := m.Input.Desc.Fields() + + if s != nil { + path = replacePath(v, *s, path) + } + for _, field := range strings.Split(v, ".") { + if strings.TrimSpace(field) == "" { + continue + } + if strings.Contains(field, ":") { + field = strings.Split(field, ":")[0] + } + fd := fields.ByName(protoreflect.Name(field)) + if fd == nil { + fmt.Fprintf(os.Stderr, "\u001B[31mERROR\u001B[m: The corresponding field '%s' declaration in message could not be found in '%s'\n", v, path) + os.Exit(2) + } + if fd.IsMap() { + fmt.Fprintf(os.Stderr, "\u001B[31mWARN\u001B[m: The field in path:'%s' shouldn't be a map.\n", v) + } else if fd.IsList() { + fmt.Fprintf(os.Stderr, "\u001B[31mWARN\u001B[m: The field in path:'%s' shouldn't be a list.\n", v) + } else if fd.Kind() == protoreflect.MessageKind || fd.Kind() == protoreflect.GroupKind { + fields = fd.Message().Fields() + } + } + } + comment := m.Comments.Leading.String() + m.Comments.Trailing.String() + if comment != "" { + comment = "// " + m.GoName + strings.TrimPrefix(strings.TrimSuffix(comment, "\n"), "//") + } + if m.Desc.Options().(*descriptorpb.MethodOptions).GetDeprecated() { + if comment != "" { + comment += "\n" + } + comment += deprecationComment + } + return &methodDesc{ + Name: m.GoName, + OriginalName: string(m.Desc.Name()), + Num: methodSets[m.GoName], + Request: g.QualifiedGoIdent(m.Input.GoIdent), + Reply: g.QualifiedGoIdent(m.Output.GoIdent), + Comment: comment, + Path: path, + PathTemplate: pathTemplate, + Method: method, + HasVars: len(vars) > 0, + ReplyHTTPBody: isHTTPBodyMessage(m.Output.Desc), + ClientStreaming: m.Desc.IsStreamingClient(), + ServerStreaming: m.Desc.IsStreamingServer(), + } +} + +func isHTTPBodyField(fd protoreflect.FieldDescriptor) bool { + return fd != nil && fd.Kind() == protoreflect.MessageKind && isHTTPBodyMessage(fd.Message()) +} + +func isHTTPBodyMessage(md protoreflect.MessageDescriptor) bool { + return md != nil && md.FullName() == httpBodyFullName +} + +func buildPathVars(path string) (res map[string]*string) { + if strings.HasSuffix(path, "/") { + fmt.Fprintf(os.Stderr, "\u001B[31mWARN\u001B[m: Path %s should not end with \"/\" \n", path) + } + pattern := regexp.MustCompile(`(?i){([a-z.0-9_\s]*)=?([^{}]*)}`) + matches := pattern.FindAllStringSubmatch(path, -1) + res = make(map[string]*string, len(matches)) + for _, m := range matches { + name := strings.TrimSpace(m[1]) + if len(name) > 1 && len(m[2]) > 0 { + res[name] = &m[2] + } else { + res[name] = nil + } + } + return +} + +func replacePath(name string, value string, path string) string { + pattern := regexp.MustCompile(fmt.Sprintf(`(?i){([\s]*%s\b[\s]*)=?([^{}]*)}`, name)) + idx := pattern.FindStringIndex(path) + if len(idx) > 0 { + path = fmt.Sprintf("%s{%s:%s}%s", + path[:idx[0]], // The start of the match + name, + pathTemplateRegex(value), + path[idx[1]:], + ) + } + return path +} + +func pathTemplateRegex(value string) string { + segs := strings.Split(value, "/") + for i, seg := range segs { + switch seg { + case "*": + segs[i] = "[^/]+" + case "**": + segs[i] = ".*" + default: + segs[i] = regexp.QuoteMeta(seg) + } + } + return strings.Join(segs, "/") +} + +func camelCaseVars(s string) string { + subs := strings.Split(s, ".") + vars := make([]string, 0, len(subs)) + for _, sub := range subs { + vars = append(vars, camelCase(sub)) + } + return strings.Join(vars, ".") +} + +// camelCase returns the CamelCased name. +// If there is an interior underscore followed by a lower case letter, +// drop the underscore and convert the letter to upper case. +// There is a remote possibility of this rewrite causing a name collision, +// but it's so remote we're prepared to pretend it's nonexistent - since the +// C++ generator lowercase names, it's extremely unlikely to have two fields +// with different capitalization. +// In short, _my_field_name_2 becomes XMyFieldName_2. +func camelCase(s string) string { + if s == "" { + return "" + } + t := make([]byte, 0, 32) + i := 0 + if s[0] == '_' { + // Need a capital letter; drop the '_'. + t = append(t, 'X') + i++ + } + // Invariant: if the next letter is lower case, it must be converted + // to upper case. + // That is, we process a word at a time, where words are marked by _ or + // upper case letter. Digits are treated as words. + for ; i < len(s); i++ { + c := s[i] + if c == '_' && i+1 < len(s) && isASCIILower(s[i+1]) { + continue // Skip the underscore in s. + } + if isASCIIDigit(c) { + t = append(t, c) + continue + } + // Assume we have a letter now - if not, it's a bogus identifier. + // The next word is a sequence of characters that must start upper case. + if isASCIILower(c) { + c ^= ' ' // Make it a capital letter. + } + t = append(t, c) // Guaranteed not lower case. + // Accept lower case sequence that follows. + for i+1 < len(s) && isASCIILower(s[i+1]) { + i++ + t = append(t, s[i]) + } + } + return string(t) +} + +// Is c an ASCII lower-case letter? +func isASCIILower(c byte) bool { + return 'a' <= c && c <= 'z' +} + +// Is c an ASCII digit? +func isASCIIDigit(c byte) bool { + return '0' <= c && c <= '9' +} + +func protocVersion(gen *protogen.Plugin) string { + v := gen.Request.GetCompilerVersion() + if v == nil { + return "(unknown)" + } + var suffix string + if s := v.GetSuffix(); s != "" { + suffix = "-" + s + } + return fmt.Sprintf("v%d.%d.%d%s", v.GetMajor(), v.GetMinor(), v.GetPatch(), suffix) +} + +const deprecationComment = "// Deprecated: Do not use." diff --git a/cmd/protoc-gen-go-http/httpTemplate.tpl b/cmd/protoc-gen-go-http/httpTemplate.tpl new file mode 100644 index 0000000..69e8ae4 --- /dev/null +++ b/cmd/protoc-gen-go-http/httpTemplate.tpl @@ -0,0 +1,348 @@ +{{$svrType := .ServiceType}} +{{$svrName := .ServiceName}} + +{{- range .MethodSets}} +const Operation{{$svrType}}{{.OriginalName}} = "/{{$svrName}}/{{.OriginalName}}" +{{- end}} + +type {{.ServiceType}}HTTPServer interface { +{{- range .MethodSets}} + {{- if ne .Comment ""}} + {{.Comment}} + {{- end}} + {{- if .ClientStreaming}} + {{.Name}}({{$svrType}}_{{.Name}}Server) error + {{- else if .ServerStreaming}} + {{.Name}}(*{{.Request}}, {{$svrType}}_{{.Name}}Server) error + {{- else}} + {{.Name}}(context.Context, *{{.Request}}) (*{{.Reply}}, error) + {{- end}} +{{- end}} +} + +func Register{{.ServiceType}}HTTPServer(s *http.Server, srv {{.ServiceType}}HTTPServer) { + r := s.Route("/") + {{- range .Methods}} + {{- if .ClientStreaming}} + r.Handle("GET", "{{.Path}}", _{{$svrType}}_{{.Name}}{{.Num}}_HTTP_Handler(srv)) + {{- else}} + r.Handle("{{.Method}}", "{{.Path}}", _{{$svrType}}_{{.Name}}{{.Num}}_HTTP_Handler(srv)) + {{- end}} + {{- end}} +} + +{{range .MethodSets}} +{{- if or .ClientStreaming .ServerStreaming}} +type {{$svrType}}_{{.Name}}HTTPServer struct { + http.ServerStream +} + +{{- if .ServerStreaming}} +func (x *{{$svrType}}_{{.Name}}HTTPServer) Send(m *{{.Reply}}) error { + return x.ServerStream.Send(m) +} +{{- end}} + +{{- if .ClientStreaming}} +func (x *{{$svrType}}_{{.Name}}HTTPServer) Recv() (*{{.Request}}, error) { + m := new({{.Request}}) + if err := x.ServerStream.Recv(m); err != nil { + return nil, err + } + return m, nil +} +{{- end}} + +{{- if and .ClientStreaming (not .ServerStreaming)}} +func (x *{{$svrType}}_{{.Name}}HTTPServer) SendAndClose(m *{{.Reply}}) error { + return x.ServerStream.SendAndClose(m) +} +{{- end}} +{{- end}} +{{end}} + +{{range .Methods}} +func _{{$svrType}}_{{.Name}}{{.Num}}_HTTP_Handler(srv {{$svrType}}HTTPServer) func(ctx http.Context) error { + return func(ctx http.Context) error { + {{- if .ClientStreaming}} + stream, err := http.NewWebSocketServerStream(ctx{{if .BodyMessage}}, http.WithStreamBodyField("{{.BodyField}}"){{end}}) + if err != nil { + return err + } + http.SetOperation(ctx,Operation{{$svrType}}{{.OriginalName}}) + h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) { + stream.SetContext(ctx) + return nil, srv.{{.Name}}(&{{$svrType}}_{{.Name}}HTTPServer{ServerStream: stream}) + }) + _, err = h(ctx, nil) + return stream.Close(err) + {{- else if .ServerStreaming}} + var in {{.Request}} + {{- if .HasBody}} + if err := ctx.Bind(&in{{.Body}}); err != nil { + return err + } + {{- end}} + {{- if not .HasBody}} + if err := ctx.BindQuery(&in); err != nil { + return err + } + {{- else if ne .BodyField "*"}} + if err := ctx.BindQuery(&in); err != nil { + return err + } + {{- end}} + {{- if .HasVars}} + if err := ctx.BindVars(&in); err != nil { + return err + } + {{- end}} + stream := http.NewServerSentEventServerStream(ctx) + http.SetOperation(ctx,Operation{{$svrType}}{{.OriginalName}}) + h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) { + stream.SetContext(ctx) + return nil, srv.{{.Name}}(req.(*{{.Request}}), &{{$svrType}}_{{.Name}}HTTPServer{ServerStream: stream}) + }) + _, err := h(ctx, &in) + return stream.Close(err) + {{- else}} + var in {{.Request}} + {{- if .HasBody}} + if err := ctx.Bind(&in{{.Body}}); err != nil { + return err + } + {{- end}} + {{- if not .HasBody}} + if err := ctx.BindQuery(&in); err != nil { + return err + } + {{- else if ne .BodyField "*"}} + if err := ctx.BindQuery(&in); err != nil { + return err + } + {{- end}} + {{- if .HasVars}} + if err := ctx.BindVars(&in); err != nil { + return err + } + {{- end}} + http.SetOperation(ctx,Operation{{$svrType}}{{.OriginalName}}) + h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.{{.Name}}(ctx, req.(*{{.Request}})) + }) + out, err := h(ctx, &in) + if err != nil { + return err + } + reply := out.(*{{.Reply}}) + {{- if or .ReplyHTTPBody .ResponseBodyHTTPBody}} + return ctx.Blob(200, http.BodyContentType(reply{{.ResponseBody}}), reply{{.ResponseBody}}.GetData()) + {{- else}} + return ctx.Result(200, reply{{.ResponseBody}}) + {{- end}} + {{- end}} + } +} +{{end}} + +type {{.ServiceType}}HTTPClient interface { +{{- range .MethodSets}} + {{- if ne .Comment ""}} + {{.Comment}} + {{- end}} + {{- if .ClientStreaming}} + {{.Name}}(ctx context.Context, opts ...http.CallOption) ({{$svrType}}_{{.Name}}Client, error) + {{- else if .ServerStreaming}} + {{.Name}}(ctx context.Context, req *{{.Request}}, opts ...http.CallOption) ({{$svrType}}_{{.Name}}Client, error) + {{- else}} + {{.Name}}(ctx context.Context, req *{{.Request}}, opts ...http.CallOption) (rsp *{{.Reply}}, err error) + {{- end}} +{{- end}} +} + +type {{.ServiceType}}HTTPClientImpl struct{ + cc *http.Client +} + +func New{{.ServiceType}}HTTPClient (client *http.Client) {{.ServiceType}}HTTPClient { + return &{{.ServiceType}}HTTPClientImpl{client} +} + +{{range .MethodSets}} +{{- if or .ClientStreaming .ServerStreaming}} +type {{$svrType}}_{{.Name}}HTTPClient struct { + http.ClientStream + {{- if .ClientStreaming}} + ctx context.Context + cc *http.Client + pattern string + opts []http.CallOption + {{- end}} +} + +{{- if .ClientStreaming}} +func (x *{{$svrType}}_{{.Name}}HTTPClient) open(m *{{.Request}}) error { + if x.ClientStream != nil { + return nil + } + {{- if .BodyHTTPBody}} + opts := append([]http.CallOption{ + http.ContentType(http.BodyContentType(m)), + }, x.opts...) + {{- else}} + opts := x.opts + {{- end}} + {{- if .HasBody}} + {{- if or (eq .BodyField "*") (eq .BodyField "")}} + path := http.BuildPath(x.pattern, m) + {{- else}} + path := http.BuildPath(x.pattern, m, http.WithQueryParams(), http.WithOmitFields("{{.BodyQueryName}}")) + {{- end}} + {{- else}} + path := http.BuildPath(x.pattern, m, http.WithQueryParams()) + {{- end}} + stream, err := x.cc.WebSocket(x.ctx, path, opts...) + if err != nil { + return err + } + x.ClientStream = stream + return nil +} + +func (x *{{$svrType}}_{{.Name}}HTTPClient) CloseSend() error { + if err := x.open(nil); err != nil { + return err + } + return x.ClientStream.CloseSend() +} + +func (x *{{$svrType}}_{{.Name}}HTTPClient) Send(m *{{.Request}}) error { + if err := x.open(m); err != nil { + return err + } + return x.ClientStream.Send(m{{if .BodyMessage}}{{.Body}}{{end}}) +} +{{- end}} + +{{- if .ServerStreaming}} +func (x *{{$svrType}}_{{.Name}}HTTPClient) Recv() (*{{.Reply}}, error) { + {{- if .ClientStreaming}} + if err := x.open(nil); err != nil { + return nil, err + } + {{- end}} + m := new({{.Reply}}) + if err := x.ClientStream.Recv(m); err != nil { + return nil, err + } + return m, nil +} +{{- end}} + +{{- if and .ClientStreaming (not .ServerStreaming)}} +func (x *{{$svrType}}_{{.Name}}HTTPClient) CloseAndRecv() (*{{.Reply}}, error) { + if err := x.open(nil); err != nil { + return nil, err + } + m := new({{.Reply}}) + if err := x.ClientStream.CloseAndRecv(m); err != nil { + return nil, err + } + return m, nil +} +{{- end}} +{{- end}} +{{end}} + +{{range .MethodSets}} + {{- if ne .Comment ""}} + {{.Comment}} + {{- end}} +{{- if .ClientStreaming}} +func (c *{{$svrType}}HTTPClientImpl) {{.Name}}(ctx context.Context, opts ...http.CallOption) ({{$svrType}}_{{.Name}}Client, error) { + pattern := "{{.PathTemplate}}" + opts = append([]http.CallOption{ + http.Accept("application/protojson"), + {{- if not .BodyHTTPBody}} + http.ContentType("application/protojson"), + {{- end}} + http.Operation(Operation{{$svrType}}{{.OriginalName}}), + http.PathTemplate(pattern), + }, opts...) + return &{{$svrType}}_{{.Name}}HTTPClient{ctx: ctx, cc: c.cc, pattern: pattern, opts: opts}, nil +} +{{- else if .ServerStreaming}} +func (c *{{$svrType}}HTTPClientImpl) {{.Name}}(ctx context.Context, in *{{.Request}}, opts ...http.CallOption) ({{$svrType}}_{{.Name}}Client, error) { + pattern := "{{.PathTemplate}}" + {{- if .HasBody}} + {{- if or (eq .BodyField "*") (eq .BodyField "")}} + path := http.BuildPath(pattern, in) + {{- else}} + path := http.BuildPath(pattern, in, http.WithQueryParams(), http.WithOmitFields("{{.BodyQueryName}}")) + {{- end}} + opts = append([]http.CallOption{ + http.Accept("text/event-stream"), + {{- if .BodyHTTPBody}} + http.ContentType(http.BodyContentType(in{{.Body}})), + {{- else}} + http.ContentType("application/protojson"), + {{- end}} + http.Operation(Operation{{$svrType}}{{.OriginalName}}), + http.PathTemplate(pattern), + }, opts...) + stream, err := c.cc.ServerSentEvent(ctx, "{{.Method}}", path, in{{.Body}}, opts...) + {{- else}} + path := http.BuildPath(pattern, in, http.WithQueryParams()) + opts = append([]http.CallOption{ + http.Accept("text/event-stream"), + http.ContentType("application/protojson"), + http.Operation(Operation{{$svrType}}{{.OriginalName}}), + http.PathTemplate(pattern), + }, opts...) + stream, err := c.cc.ServerSentEvent(ctx, "{{.Method}}", path, nil, opts...) + {{- end}} + if err != nil { + return nil, err + } + return &{{$svrType}}_{{.Name}}HTTPClient{ClientStream: stream}, nil +} +{{- else}} +func (c *{{$svrType}}HTTPClientImpl) {{.Name}}(ctx context.Context, in *{{.Request}}, opts ...http.CallOption) (*{{.Reply}}, error) { + var out {{.Reply}} + pattern := "{{.PathTemplate}}" + {{- if .HasBody}} + {{- if or (eq .BodyField "*") (eq .BodyField "")}} + path := http.BuildPath(pattern, in) + {{- else}} + path := http.BuildPath(pattern, in, http.WithQueryParams(), http.WithOmitFields("{{.BodyQueryName}}")) + {{- end}} + opts = append([]http.CallOption{ + http.Accept("application/protojson"), + {{- if .BodyHTTPBody}} + http.ContentType(http.BodyContentType(in{{.Body}})), + {{- else}} + http.ContentType("application/protojson"), + {{- end}} + http.Operation(Operation{{$svrType}}{{.OriginalName}}), + http.PathTemplate(pattern), + }, opts...) + {{- else}} + path := http.BuildPath(pattern, in, http.WithQueryParams()) + opts = append([]http.CallOption{ + http.Accept("application/protojson"), + http.Operation(Operation{{$svrType}}{{.OriginalName}}), + http.PathTemplate(pattern), + }, opts...) + {{- end}} + {{if .HasBody -}} + err := c.cc.Invoke(ctx, "{{.Method}}", path, in{{.Body}}, &out{{.ResponseBody}}, opts...) + {{else -}} + err := c.cc.Invoke(ctx, "{{.Method}}", path, nil, &out{{.ResponseBody}}, opts...) + {{end -}} + if err != nil { + return nil, err + } + return &out, nil +} +{{- end}} +{{end}} diff --git a/cmd/protoc-gen-go-http/http_test.go b/cmd/protoc-gen-go-http/http_test.go new file mode 100644 index 0000000..0be528b --- /dev/null +++ b/cmd/protoc-gen-go-http/http_test.go @@ -0,0 +1,289 @@ +package main + +import ( + "reflect" + "strings" + "testing" +) + +func TestNoParameters(t *testing.T) { + path := "/test/noparams" + m := buildPathVars(path) + if !reflect.DeepEqual(m, map[string]*string{}) { + t.Fatalf("Map should be empty") + } +} + +func TestSingleParam(t *testing.T) { + path := "/test/{message.id}" + m := buildPathVars(path) + if !reflect.DeepEqual(len(m), 1) { + t.Fatalf("len(m) not is 1") + } + if m["message.id"] != nil { + t.Fatalf(`m["message.id"] should be empty`) + } +} + +func TestTwoParametersReplacement(t *testing.T) { + path := "/test/{message.id}/{message.name=messages/*}" + m := buildPathVars(path) + if len(m) != 2 { + t.Fatal("len(m) should be 2") + } + if m["message.id"] != nil { + t.Fatal(`m["message.id"] should be nil`) + } + if m["message.name"] == nil { + t.Fatal(`m["message.name"] should not be nil`) + } + if *m["message.name"] != "messages/*" { + t.Fatal(`m["message.name"] should be "messages/*"`) + } +} + +func TestNoReplacePath(t *testing.T) { + path := "/test/{message.id=test}" + if !reflect.DeepEqual(replacePath("message.id", "test", path), "/test/{message.id:test}") { + t.Fatal(`replacePath("message.id", "test", path) should be "/test/{message.id:test}"`) + } + path = "/test/{message.id=test/*}" + if !reflect.DeepEqual(replacePath("message.id", "test/*", path), "/test/{message.id:test/[^/]+}") { + t.Fatal(`replacePath("message.id", "test/*", path) should be "/test/{message.id:test/[^/]+}"`) + } +} + +func TestReplacePath(t *testing.T) { + path := "/test/{message.id}/{message.name=messages/*}" + newPath := replacePath("message.name", "messages/*", path) + if !reflect.DeepEqual("/test/{message.id}/{message.name:messages/[^/]+}", newPath) { + t.Fatal(`replacePath("message.name", "messages/*", path) should be "/test/{message.id}/{message.name:messages/[^/]+}"`) + } +} + +func TestIteration(t *testing.T) { + path := "/test/{message.id}/{message.name=messages/*}" + vars := buildPathVars(path) + for v, s := range vars { + if s != nil { + path = replacePath(v, *s, path) + } + } + if !reflect.DeepEqual("/test/{message.id}/{message.name:messages/[^/]+}", path) { + t.Fatal(`replacePath("message.name", "messages/*", path) should be "/test/{message.id}/{message.name:messages/[^/]+}"`) + } +} + +func TestIterationMiddle(t *testing.T) { + path := "/test/{message.name=messages/*}/books" + vars := buildPathVars(path) + for v, s := range vars { + if s != nil { + path = replacePath(v, *s, path) + } + } + if !reflect.DeepEqual("/test/{message.name:messages/[^/]+}/books", path) { + t.Fatal(`replacePath("message.name", "messages/*", path) should be "/test/{message.name:messages/[^/]+}/books"`) + } +} + +func TestReplaceBoundary(t *testing.T) { + path := "/test/{message.namespace=*}/name/{message.name=*}" + vars := buildPathVars(path) + for v, s := range vars { + if s != nil { + path = replacePath(v, *s, path) + } + } + if !reflect.DeepEqual("/test/{message.namespace:[^/]+}/name/{message.name:[^/]+}", path) { + t.Fatal(`"/test/{message.namespace=*}/name/{message.name=*}" should be "/test/{message.namespace:[^/]+}/name/{message.name:[^/]+}"`) + } +} + +func TestPathTemplateRegex(t *testing.T) { + tests := []struct { + name string + value string + want string + }{ + { + name: "single segment", + value: "messages/*", + want: "messages/[^/]+", + }, + { + name: "multi segment", + value: "messages/**", + want: "messages/.*", + }, + { + name: "literal", + value: "v1.0/*", + want: `v1\.0/[^/]+`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := pathTemplateRegex(tt.value); got != tt.want { + t.Errorf("expected %s got %s", tt.want, got) + } + }) + } +} + +func TestHTTPTemplateClientUsesBuildPathAndProtoJSONHeaders(t *testing.T) { + sd := &serviceDesc{ + ServiceType: "Greeter", + ServiceName: "helloworld.Greeter", + Methods: []*methodDesc{ + { + Name: "SayHello", + OriginalName: "SayHello", + Request: "HelloRequest", + Reply: "HelloReply", + Path: "/helloworld/{name}", + PathTemplate: "/helloworld/{name}", + Method: "GET", + HasVars: true, + }, + { + Name: "CreateHello", + OriginalName: "CreateHello", + Request: "CreateHelloRequest", + Reply: "HelloReply", + Path: "/helloworld", + PathTemplate: "/helloworld", + Method: "POST", + HasBody: true, + Body: "", + }, + }, + } + got := sd.execute() + for _, want := range []string{ + `path := http.BuildPath(pattern, in, http.WithQueryParams())`, + `path := http.BuildPath(pattern, in)`, + `http.Accept("application/protojson")`, + `http.ContentType("application/protojson")`, + } { + if !strings.Contains(got, want) { + t.Fatalf("generated template missing %q in:\n%s", want, got) + } + } + if strings.Contains(got, "binding.") { + t.Fatalf("generated template should not reference binding package:\n%s", got) + } +} + +func TestHTTPTemplateStreamsAndHTTPBody(t *testing.T) { + sd := &serviceDesc{ + ServiceType: "Greeter", + ServiceName: "helloworld.Greeter", + Methods: []*methodDesc{ + { + Name: "ListHello", + OriginalName: "ListHello", + Request: "ListHelloRequest", + Reply: "HelloReply", + Path: "/helloworld", + PathTemplate: "/helloworld", + Method: "GET", + ServerStreaming: true, + }, + { + Name: "ChatHello", + OriginalName: "ChatHello", + Request: "HelloRequest", + Reply: "HelloReply", + Path: "/helloworld/chat", + PathTemplate: "/helloworld/chat", + Method: "POST", + ClientStreaming: true, + ServerStreaming: true, + }, + { + Name: "UploadHello", + OriginalName: "UploadHello", + Request: "UploadHelloRequest", + Reply: "UploadHelloReply", + Path: "/helloworld/upload", + PathTemplate: "/helloworld/upload", + Method: "POST", + HasBody: true, + Body: ".Body", + BodyField: "body", + BodyQueryName: "body", + BodyHTTPBody: true, + ResponseBody: ".Body", + }, + { + Name: "ChatData", + OriginalName: "ChatData", + Request: "ChatDataRequest", + Reply: "ChatDataReply", + Path: "/v1/bitto/chat", + PathTemplate: "/v1/bitto/chat", + Method: "GET", + HasBody: true, + Body: ".Data", + BodyField: "data", + BodyQueryName: "data", + BodyMessage: true, + ClientStreaming: true, + ServerStreaming: true, + }, + { + // Client-streaming RPC whose named body is a scalar field: it is not + // streamable frame-by-frame, so the whole message is sent/decoded. + Name: "ChatText", + OriginalName: "ChatText", + Request: "ChatTextRequest", + Reply: "ChatTextReply", + Path: "/v1/bitto/text", + PathTemplate: "/v1/bitto/text", + Method: "GET", + HasBody: true, + Body: ".Text", + BodyField: "text", + BodyQueryName: "text", + BodyMessage: false, + ClientStreaming: true, + ServerStreaming: true, + }, + }, + } + got := sd.execute() + for _, want := range []string{ + `ListHello(*ListHelloRequest, Greeter_ListHelloServer) error`, + `stream := http.NewServerSentEventServerStream(ctx)`, + `stream, err := c.cc.ServerSentEvent(ctx, "GET", path, nil, opts...)`, + `ChatHello(Greeter_ChatHelloServer) error`, + `stream, err := http.NewWebSocketServerStream(ctx)`, + `func (x *Greeter_ChatHelloHTTPClient) open(m *HelloRequest) error`, + `path := http.BuildPath(x.pattern, m, http.WithQueryParams())`, + `stream, err := x.cc.WebSocket(x.ctx, path, opts...)`, + `http.ContentType("application/protojson")`, + `return &Greeter_ChatHelloHTTPClient{ctx: ctx, cc: c.cc, pattern: pattern, opts: opts}, nil`, + `http.ContentType(http.BodyContentType(in.Body))`, + `http.WithOmitFields("body")`, + `return ctx.Result(200, reply.Body)`, + // Client-streaming RPC with a streamable (message-kind) named body field. + `stream, err := http.NewWebSocketServerStream(ctx, http.WithStreamBodyField("data"))`, + `return x.ClientStream.Send(m.Data)`, + // Client-streaming RPC with a scalar named body: whole message is streamed. + `func (x *Greeter_ChatTextHTTPClient) Send(m *ChatTextRequest) error`, + } { + if !strings.Contains(got, want) { + t.Fatalf("generated template missing %q in:\n%s", want, got) + } + } + // The whole-message streaming client (ChatHello) and the scalar-body streaming + // client (ChatText) must both send the whole message, not a sub-field. + if !strings.Contains(got, "return x.ClientStream.Send(m)\n") { + t.Fatalf("generated template should send whole message for non-streamable body:\n%s", got) + } + // The scalar-body server handler must NOT receive a stream body-field option. + if strings.Contains(got, `http.WithStreamBodyField("text")`) { + t.Fatalf("scalar named body should not emit WithStreamBodyField:\n%s", got) + } +} diff --git a/cmd/protoc-gen-go-http/main.go b/cmd/protoc-gen-go-http/main.go new file mode 100644 index 0000000..3752019 --- /dev/null +++ b/cmd/protoc-gen-go-http/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/types/pluginpb" +) + +var ( + showVersion = flag.Bool("version", false, "print the version and exit") + omitempty = flag.Bool("omitempty", true, "omit if google.api is empty") + omitemptyPrefix = flag.String("omitempty_prefix", "", "omit if google.api is empty") +) + +func main() { + flag.Parse() + if *showVersion { + fmt.Printf("protoc-gen-go-http %v\n", release) + return + } + protogen.Options{ + ParamFunc: flag.CommandLine.Set, + }.Run(func(gen *protogen.Plugin) error { + gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + for _, f := range gen.Files { + if !f.Generate { + continue + } + generateFile(gen, f, *omitempty, *omitemptyPrefix) + } + return nil + }) +} diff --git a/cmd/protoc-gen-go-http/template.go b/cmd/protoc-gen-go-http/template.go new file mode 100644 index 0000000..c107f8f --- /dev/null +++ b/cmd/protoc-gen-go-http/template.go @@ -0,0 +1,61 @@ +package main + +import ( + "bytes" + _ "embed" + "strings" + "text/template" +) + +//go:embed httpTemplate.tpl +var httpTemplate string + +type serviceDesc struct { + ServiceType string // Greeter + ServiceName string // helloworld.Greeter + Metadata string // api/helloworld/helloworld.proto + Methods []*methodDesc + MethodSets map[string]*methodDesc +} + +type methodDesc struct { + // method + Name string + OriginalName string // The parsed original name + Num int + Request string + Reply string + Comment string + // http_rule + Path string + PathTemplate string + Method string + HasVars bool + HasBody bool + Body string + BodyField string + BodyQueryName string + BodyHTTPBody bool + BodyMessage bool + ResponseBody string + ResponseBodyHTTPBody bool + ReplyHTTPBody bool + ClientStreaming bool + ServerStreaming bool +} + +func (s *serviceDesc) execute() string { + s.MethodSets = make(map[string]*methodDesc) + for _, m := range s.Methods { + s.MethodSets[m.Name] = m + } + buf := new(bytes.Buffer) + tmpl, err := template.New("http").Parse(strings.TrimSpace(httpTemplate)) + if err != nil { + panic(err) + } + if err := tmpl.Execute(buf, s); err != nil { + panic(err) + } + return strings.Trim(buf.String(), "\r\n") +} diff --git a/cmd/protoc-gen-go-http/version.go b/cmd/protoc-gen-go-http/version.go new file mode 100644 index 0000000..2b88b84 --- /dev/null +++ b/cmd/protoc-gen-go-http/version.go @@ -0,0 +1,4 @@ +package main + +// release is the current protoc-gen-go-http version. +const release = "v3.0.0" diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..f122b5f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,3 @@ +ignore: + - "examples" + - "**/*.pb.go" diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..0d05857 --- /dev/null +++ b/config/README.md @@ -0,0 +1,25 @@ +# Config + +## kubernetes + +```shell +go get -u github.com/go-kratos/kratos/contrib/config/kubernetes/v3 +``` + +## apollo + +```shell +go get -u github.com/go-kratos/kratos/contrib/config/apollo/v3 +``` + +## etcd + +```shell +go get -u github.com/go-kratos/kratos/contrib/config/etcd/v3 +``` + +## nacos + +```shell +go get -u github.com/go-kratos/kratos/contrib/config/nacos/v3 +``` diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..fc3a1de --- /dev/null +++ b/config/config.go @@ -0,0 +1,185 @@ +package config + +import ( + "context" + "errors" + "reflect" + "sync" + "time" + + // init encoding + _ "github.com/go-kratos/kratos/v3/encoding/json" + _ "github.com/go-kratos/kratos/v3/encoding/proto" + _ "github.com/go-kratos/kratos/v3/encoding/xml" + _ "github.com/go-kratos/kratos/v3/encoding/yaml" + "github.com/go-kratos/kratos/v3/log" +) + +var _ Config = (*config)(nil) + +var ErrNotFound = errors.New("key not found") // ErrNotFound is key not found. + +// Observer is config observer. +type Observer func(string, Value) + +// Config is a config interface. +type Config interface { + Load() error + Scan(v any) error + Value(key string) Value + Watch(key string, o Observer) error + Close() error +} + +type config struct { + opts options + reader Reader + cached sync.Map + observers sync.Map + watchers []Watcher +} + +// New a config with options. +func New(opts ...Option) Config { + o := options{ + decoder: defaultDecoder, + resolver: defaultResolver, + merge: defaultMerge, + } + for _, opt := range opts { + opt(&o) + } + return &config{ + opts: o, + reader: newReader(o), + } +} + +func (c *config) watch(w Watcher) { + for { + kvs, err := w.Next() + if err != nil { + if errors.Is(err, context.Canceled) { + log.Info("watcher's ctx cancel", "error", err) + return + } + time.Sleep(time.Second) + log.Error("failed to watch next config", "error", err) + continue + } + if err := c.reader.Merge(kvs...); err != nil { + log.Error("failed to merge next config", "error", err) + continue + } + if err := c.reader.Resolve(); err != nil { + log.Error("failed to resolve next config", "error", err) + continue + } + c.cached.Range(func(key, value any) bool { + k := key.(string) + v := value.(Value) + if n, ok := c.reader.Value(k); ok && reflect.TypeOf(n.Load()) == reflect.TypeOf(v.Load()) && !reflect.DeepEqual(n.Load(), v.Load()) { + v.Store(n.Load()) + if o, ok := c.observers.Load(k); ok { + o.(Observer)(k, v) + } + } + return true + }) + } +} + +func (c *config) Load() error { + for _, src := range c.opts.sources { + kvs, err := src.Load() + if err != nil { + return err + } + for _, v := range kvs { + log.Debug("config loaded", "key", v.Key, "format", v.Format) + } + if err = c.reader.Merge(kvs...); err != nil { + log.Error("failed to merge config source", "error", err) + return err + } + w, err := src.Watch() + if err != nil { + log.Error("failed to watch config source", "error", err) + return err + } + c.watchers = append(c.watchers, w) + go c.watch(w) + } + if err := c.reader.Resolve(); err != nil { + log.Error("failed to resolve config source", "error", err) + return err + } + return nil +} + +func (c *config) Value(key string) Value { + if v, ok := c.cached.Load(key); ok { + return v.(Value) + } + if v, ok := c.reader.Value(key); ok { + c.cached.Store(key, v) + return v + } + return &errValue{err: ErrNotFound} +} + +func (c *config) Scan(v any) error { + data, err := c.reader.Source() + if err != nil { + return err + } + return unmarshalJSON(data, v) +} + +func (c *config) Watch(key string, o Observer) error { + if v := c.Value(key); v.Load() == nil { + return ErrNotFound + } + c.observers.Store(key, o) + return nil +} + +func (c *config) Close() error { + for _, w := range c.watchers { + if err := w.Stop(); err != nil { + return err + } + } + return nil +} + +// Get retrieves a config value by key and scans it into the target type. +func Get[T any](c Config, key string) (T, error) { + var t T + v := c.Value(key) + + if v.Load() == nil { + return t, ErrNotFound + } + + switch any(t).(type) { + case bool: + b, err := v.Bool() + return any(b).(T), err + case int64: + i, err := v.Int() + return any(i).(T), err + case int: + i, err := v.Int() + return any(int(i)).(T), err + case float64: + f, err := v.Float() + return any(f).(T), err + case string: + s, err := v.String() + return any(s).(T), err + } + + err := v.Scan(&t) + return t, err +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..822b086 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,205 @@ +package config + +import ( + "errors" + "testing" +) + +const ( + _testJSON = ` +{ + "server":{ + "http":{ + "addr":"0.0.0.0", + "port":80, + "timeout":0.5, + "enable_ssl":true + }, + "grpc":{ + "addr":"0.0.0.0", + "port":10080, + "timeout":0.2 + } + }, + "data":{ + "database":{ + "driver":"mysql", + "source":"root:root@tcp(127.0.0.1:3306)/karta_id?parseTime=true" + } + }, + "endpoints":[ + "www.aaa.com", + "www.bbb.org" + ] +}` +) + +type testConfigStruct struct { + Server struct { + HTTP struct { + Addr string `json:"addr"` + Port int `json:"port"` + Timeout float64 `json:"timeout"` + EnableSSL bool `json:"enable_ssl"` + } `json:"http"` + GRPC struct { + Addr string `json:"addr"` + Port int `json:"port"` + Timeout float64 `json:"timeout"` + } `json:"grpc"` + } `json:"server"` + Data struct { + Database struct { + Driver string `json:"driver"` + Source string `json:"source"` + } `json:"database"` + } `json:"data"` + Endpoints []string `json:"endpoints"` +} + +type testJSONSource struct { + data string + sig chan struct{} + err chan struct{} +} + +func newTestJSONSource(data string) *testJSONSource { + return &testJSONSource{data: data, sig: make(chan struct{}), err: make(chan struct{})} +} + +func (p *testJSONSource) Load() ([]*KeyValue, error) { + kv := &KeyValue{ + Key: "json", + Value: []byte(p.data), + Format: "json", + } + return []*KeyValue{kv}, nil +} + +func (p *testJSONSource) Watch() (Watcher, error) { + return newTestWatcher(p.sig, p.err), nil +} + +type testWatcher struct { + sig chan struct{} + err chan struct{} + exit chan struct{} +} + +func newTestWatcher(sig, err chan struct{}) Watcher { + return &testWatcher{sig: sig, err: err, exit: make(chan struct{})} +} + +func (w *testWatcher) Next() ([]*KeyValue, error) { + select { + case <-w.sig: + return nil, nil + case <-w.err: + return nil, errors.New("error") + case <-w.exit: + return nil, nil + } +} + +func (w *testWatcher) Stop() error { + close(w.exit) + return nil +} + +func TestConfig(t *testing.T) { + var ( + err error + httpAddr = "0.0.0.0" + httpTimeout = 0.5 + grpcPort = 10080 + endpoint1 = "www.aaa.com" + databaseDriver = "mysql" + ) + + c := New( + WithSource(newTestJSONSource(_testJSON)), + WithDecoder(defaultDecoder), + WithResolver(defaultResolver), + ) + err = c.Close() + if err != nil { + t.Fatal(err) + } + + jSource := newTestJSONSource(_testJSON) + opts := options{ + sources: []Source{jSource}, + decoder: defaultDecoder, + resolver: defaultResolver, + merge: defaultMerge, + } + cf := &config{} + cf.opts = opts + cf.reader = newReader(opts) + + err = cf.Load() + if err != nil { + t.Fatal(err) + } + + driver, err := cf.Value("data.database.driver").String() + if err != nil { + t.Fatal(err) + } + if databaseDriver != driver { + t.Fatal("databaseDriver is not equal to val") + } + + driverGet, err := Get[string](cf, "data.database.driver") + if err != nil { + t.Fatal(err) + } + if databaseDriver != driverGet { + t.Errorf("Get[string] want: %s, got: %s", databaseDriver, driverGet) + } + + type HTTPConfig struct { + Addr string `json:"addr"` + Port int `json:"port"` + } + v, err := Get[HTTPConfig](cf, "server.http") + if err != nil { + t.Fatal(err) + } else if v.Addr != httpAddr { + t.Errorf("Get[HttpConfig] Addr want: %s, got: %s", httpAddr, v.Addr) + } else if v.Port != 80 { + t.Errorf("Get[HttpConfig] Port want: 80, got: %d", v.Port) + } + + err = cf.Watch("endpoints", func(string, Value) {}) + if err != nil { + t.Fatal(err) + } + + jSource.sig <- struct{}{} + jSource.err <- struct{}{} + + var testConf testConfigStruct + err = cf.Scan(&testConf) + if err != nil { + t.Fatal(err) + } + if httpAddr != testConf.Server.HTTP.Addr { + t.Errorf("testConf.Server.HTTP.Addr want: %s, got: %s", httpAddr, testConf.Server.HTTP.Addr) + } + if httpTimeout != testConf.Server.HTTP.Timeout { + t.Errorf("testConf.Server.HTTP.Timeout want: %.1f, got: %.1f", httpTimeout, testConf.Server.HTTP.Timeout) + } + if !testConf.Server.HTTP.EnableSSL { + t.Error("testConf.Server.HTTP.EnableSSL is not equal to true") + } + if grpcPort != testConf.Server.GRPC.Port { + t.Errorf("testConf.Server.GRPC.Port want: %d, got: %d", grpcPort, testConf.Server.GRPC.Port) + } + if endpoint1 != testConf.Endpoints[0] { + t.Errorf("testConf.Endpoints[0] want: %s, got: %s", endpoint1, testConf.Endpoints[0]) + } + if len(testConf.Endpoints) != 2 { + t.Error("len(testConf.Endpoints) is not equal to 2") + } +} diff --git a/config/env/env.go b/config/env/env.go new file mode 100644 index 0000000..a9e3a0d --- /dev/null +++ b/config/env/env.go @@ -0,0 +1,62 @@ +package env + +import ( + "os" + "strings" + + "github.com/go-kratos/kratos/v3/config" +) + +type env struct { + prefixes []string +} + +func NewSource(prefixes ...string) config.Source { + return &env{prefixes: prefixes} +} + +func (e *env) Load() (kvs []*config.KeyValue, err error) { + return e.load(os.Environ()), nil +} + +func (e *env) load(envs []string) []*config.KeyValue { + var kvs []*config.KeyValue + for _, env := range envs { + k, v, _ := strings.Cut(env, "=") + if k == "" { + continue + } + if len(e.prefixes) > 0 { + prefix, ok := matchPrefix(e.prefixes, k) + if !ok || k == prefix { + continue + } + k = strings.TrimPrefix(k, prefix) + k = strings.TrimPrefix(k, "_") + } + if k != "" { + kvs = append(kvs, &config.KeyValue{ + Key: k, + Value: []byte(v), + }) + } + } + return kvs +} + +func (e *env) Watch() (config.Watcher, error) { + w, err := NewWatcher() + if err != nil { + return nil, err + } + return w, nil +} + +func matchPrefix(prefixes []string, s string) (string, bool) { + for _, p := range prefixes { + if strings.HasPrefix(s, p) { + return p, true + } + } + return "", false +} diff --git a/config/env/env_test.go b/config/env/env_test.go new file mode 100644 index 0000000..50cfcd8 --- /dev/null +++ b/config/env/env_test.go @@ -0,0 +1,429 @@ +package env + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/go-kratos/kratos/v3/config" + "github.com/go-kratos/kratos/v3/config/file" +) + +const _testJSON = ` +{ + "test":{ + "server":{ + "name":"${SERVICE_NAME}", + "addr":"${ADDR:127.0.0.1}", + "port":"${PORT:8080}" + } + }, + "foo":[ + { + "name":"Tom", + "age":"${AGE}" + } + ] +}` + +func TestEnvWithPrefix(t *testing.T) { + var ( + path = filepath.Join(t.TempDir(), "test_config") + filename = filepath.Join(path, "test.json") + data = []byte(_testJSON) + ) + defer os.Remove(path) + if err := os.MkdirAll(path, 0o700); err != nil { + t.Error(err) + } + if err := os.WriteFile(filename, data, 0o666); err != nil { + t.Error(err) + } + + // set env + prefix1, prefix2 := "KRATOS_", "FOO" + envs := map[string]string{ + prefix1 + "SERVICE_NAME": "kratos_app", + prefix2 + "ADDR": "192.168.0.1", + prefix1 + "AGE": "20", + // only prefix + prefix2: "foo", + prefix2 + "_": "foo_", + } + + for k, v := range envs { + os.Setenv(k, v) + } + + c := config.New(config.WithSource( + file.NewSource(path), + NewSource(prefix1, prefix2), + )) + + if err := c.Load(); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + path string + expect any + }{ + { + name: "test $KEY", + path: "test.server.name", + expect: "kratos_app", + }, + { + name: "test ${KEY:DEFAULT} without default", + path: "test.server.addr", + expect: "192.168.0.1", + }, + { + name: "test ${KEY:DEFAULT} with default", + path: "test.server.port", + expect: "8080", + }, + { + name: "test ${KEY} in array", + path: "foo", + expect: []any{ + map[string]any{ + "name": "Tom", + "age": "20", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var err error + v := c.Value(test.path) + if v.Load() != nil { + var actual any + switch test.expect.(type) { + case int: + if actual, err = v.Int(); err == nil { + if !reflect.DeepEqual(test.expect.(int), int(actual.(int64))) { + t.Errorf("expect %v, actual %v", test.expect, actual) + } + } + case string: + if actual, err = v.String(); err == nil { + if !reflect.DeepEqual(test.expect.(string), actual.(string)) { + t.Errorf(`expect %v, actual %v`, test.expect, actual) + } + } + case bool: + if actual, err = v.Bool(); err == nil { + if !reflect.DeepEqual(test.expect.(bool), actual.(bool)) { + t.Errorf(`expect %v, actual %v`, test.expect, actual) + } + } + case float64: + if actual, err = v.Float(); err == nil { + if !reflect.DeepEqual(test.expect.(float64), actual.(float64)) { + t.Errorf(`expect %v, actual %v`, test.expect, actual) + } + } + default: + actual = v.Load() + if !reflect.DeepEqual(test.expect, actual) { + t.Logf("\nexpect: %#v\nactural: %#v", test.expect, actual) + t.Fail() + } + } + if err != nil { + t.Error(err) + } + } else { + t.Error("value path not found") + } + }) + } +} + +func TestEnvWithoutPrefix(t *testing.T) { + var ( + path = filepath.Join(t.TempDir(), "test_config") + filename = filepath.Join(path, "test.json") + data = []byte(_testJSON) + ) + defer os.Remove(path) + if err := os.MkdirAll(path, 0o700); err != nil { + t.Error(err) + } + if err := os.WriteFile(filename, data, 0o666); err != nil { + t.Error(err) + } + + // set env + envs := map[string]string{ + "SERVICE_NAME": "kratos_app", + "ADDR": "192.168.0.1", + "AGE": "20", + } + + for k, v := range envs { + os.Setenv(k, v) + } + + c := config.New(config.WithSource( + NewSource(), + file.NewSource(path), + )) + + if err := c.Load(); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + path string + expect any + }{ + { + name: "test $KEY", + path: "test.server.name", + expect: "kratos_app", + }, + { + name: "test ${KEY:DEFAULT} without default", + path: "test.server.addr", + expect: "192.168.0.1", + }, + { + name: "test ${KEY:DEFAULT} with default", + path: "test.server.port", + expect: "8080", + }, + { + name: "test ${KEY} in array", + path: "foo", + expect: []any{ + map[string]any{ + "name": "Tom", + "age": "20", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var err error + v := c.Value(test.path) + if v.Load() != nil { + var actual any + switch test.expect.(type) { + case int: + if actual, err = v.Int(); err == nil { + if !reflect.DeepEqual(test.expect.(int), int(actual.(int64))) { + t.Errorf("expect %v, actual %v", test.expect, actual) + } + } + case string: + if actual, err = v.String(); err == nil { + if !reflect.DeepEqual(test.expect.(string), actual.(string)) { + t.Errorf(`expect %v, actual %v`, test.expect, actual) + } + } + case bool: + if actual, err = v.Bool(); err == nil { + if !reflect.DeepEqual(test.expect.(bool), actual.(bool)) { + t.Errorf(`expect %v, actual %v`, test.expect, actual) + } + } + case float64: + if actual, err = v.Float(); err == nil { + if !reflect.DeepEqual(test.expect.(float64), actual.(float64)) { + t.Errorf(`expect %v, actual %v`, test.expect, actual) + } + } + default: + actual = v.Load() + if !reflect.DeepEqual(test.expect, actual) { + t.Logf("\nexpect: %#v\nactural: %#v", test.expect, actual) + t.Fail() + } + } + if err != nil { + t.Error(err) + } + } else { + t.Error("value path not found") + } + }) + } +} + +func Test_env_load(t *testing.T) { + type fields struct { + prefixes []string + } + type args struct { + envStrings []string + } + tests := []struct { + name string + fields fields + args args + want []*config.KeyValue + }{ + { + name: "without prefixes", + fields: fields{ + prefixes: nil, + }, + args: args{ + envStrings: []string{ + "SERVICE_NAME=kratos_app", + "ADDR=192.168.0.1", + "AGE=20", + }, + }, + want: []*config.KeyValue{ + {Key: "SERVICE_NAME", Value: []byte("kratos_app"), Format: ""}, + {Key: "ADDR", Value: []byte("192.168.0.1"), Format: ""}, + {Key: "AGE", Value: []byte("20"), Format: ""}, + }, + }, + + { + name: "empty prefix", + fields: fields{ + prefixes: []string{""}, + }, + args: args{ + envStrings: []string{ + "__SERVICE_NAME=kratos_app", + "__ADDR=192.168.0.1", + "__AGE=20", + }, + }, + want: []*config.KeyValue{ + {Key: "_SERVICE_NAME", Value: []byte("kratos_app"), Format: ""}, + {Key: "_ADDR", Value: []byte("192.168.0.1"), Format: ""}, + {Key: "_AGE", Value: []byte("20"), Format: ""}, + }, + }, + + { + name: "underscore prefix", + fields: fields{ + prefixes: []string{"_"}, + }, + args: args{ + envStrings: []string{ + "__SERVICE_NAME=kratos_app", + "__ADDR=192.168.0.1", + "__AGE=20", + }, + }, + want: []*config.KeyValue{ + {Key: "SERVICE_NAME", Value: []byte("kratos_app"), Format: ""}, + {Key: "ADDR", Value: []byte("192.168.0.1"), Format: ""}, + {Key: "AGE", Value: []byte("20"), Format: ""}, + }, + }, + + { + name: "with prefixes", + fields: fields{ + prefixes: []string{"KRATOS_", "FOO"}, + }, + args: args{ + envStrings: []string{ + "KRATOS_SERVICE_NAME=kratos_app", + "KRATOS_ADDR=192.168.0.1", + "FOO_AGE=20", + }, + }, + want: []*config.KeyValue{ + {Key: "SERVICE_NAME", Value: []byte("kratos_app"), Format: ""}, + {Key: "ADDR", Value: []byte("192.168.0.1"), Format: ""}, + {Key: "AGE", Value: []byte("20"), Format: ""}, + }, + }, + + { + name: "should not panic #1", + fields: fields{ + prefixes: []string{"FOO"}, + }, + args: args{ + envStrings: []string{ + "FOO=123", + }, + }, + want: nil, + }, + + { + name: "should not panic #2", + fields: fields{ + prefixes: []string{"FOO=1"}, + }, + args: args{ + envStrings: []string{ + "FOO=123", + }, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &env{ + prefixes: tt.fields.prefixes, + } + got := e.load(tt.args.envStrings) + if !reflect.DeepEqual(tt.want, got) { + t.Errorf("env.load() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_matchPrefix(t *testing.T) { + type args struct { + prefixes []string + s string + } + tests := []struct { + name string + args args + want string + wantOk bool + }{ + {args: args{prefixes: nil, s: "foo=123"}, want: "", wantOk: false}, + {args: args{prefixes: []string{""}, s: "foo=123"}, want: "", wantOk: true}, + {args: args{prefixes: []string{"foo"}, s: "foo=123"}, want: "foo", wantOk: true}, + {args: args{prefixes: []string{"foo=1"}, s: "foo=123"}, want: "foo=1", wantOk: true}, + {args: args{prefixes: []string{"foo=1234"}, s: "foo=123"}, want: "", wantOk: false}, + {args: args{prefixes: []string{"bar"}, s: "foo=123"}, want: "", wantOk: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotOk := matchPrefix(tt.args.prefixes, tt.args.s) + if got != tt.want { + t.Errorf("matchPrefix() got = %v, want %v", got, tt.want) + } + if gotOk != tt.wantOk { + t.Errorf("matchPrefix() gotOk = %v, wantOk %v", gotOk, tt.wantOk) + } + }) + } +} + +func Test_env_watch(t *testing.T) { + prefixes := []string{"BAR", "FOO"} + source := NewSource(prefixes...) + w, err := source.Watch() + if err != nil { + t.Errorf("expect no err, got %v", err) + } + _ = w.Stop() +} diff --git a/config/env/watcher.go b/config/env/watcher.go new file mode 100644 index 0000000..2925919 --- /dev/null +++ b/config/env/watcher.go @@ -0,0 +1,30 @@ +package env + +import ( + "context" + + "github.com/go-kratos/kratos/v3/config" +) + +var _ config.Watcher = (*watcher)(nil) + +type watcher struct { + ctx context.Context + cancel context.CancelFunc +} + +func NewWatcher() (config.Watcher, error) { + ctx, cancel := context.WithCancel(context.Background()) + return &watcher{ctx: ctx, cancel: cancel}, nil +} + +// Next will be blocked until the Stop method is called +func (w *watcher) Next() ([]*config.KeyValue, error) { + <-w.ctx.Done() + return nil, w.ctx.Err() +} + +func (w *watcher) Stop() error { + w.cancel() + return nil +} diff --git a/config/env/watcher_test.go b/config/env/watcher_test.go new file mode 100644 index 0000000..4e0f71e --- /dev/null +++ b/config/env/watcher_test.go @@ -0,0 +1,32 @@ +package env + +import ( + "testing" +) + +func Test_watcher_next(t *testing.T) { + t.Run("next after stop should return err", func(t *testing.T) { + w, err := NewWatcher() + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + _ = w.Stop() + _, err = w.Next() + if err == nil { + t.Error("expect error, actual nil") + } + }) +} + +func Test_watcher_stop(t *testing.T) { + t.Run("stop multiple times should not panic", func(t *testing.T) { + w, err := NewWatcher() + if err != nil { + t.Errorf("expect no error, got %v", err) + } + + _ = w.Stop() + _ = w.Stop() + }) +} diff --git a/config/file/file.go b/config/file/file.go new file mode 100644 index 0000000..722d8c0 --- /dev/null +++ b/config/file/file.go @@ -0,0 +1,80 @@ +package file + +import ( + "io" + "os" + "path/filepath" + "strings" + + "github.com/go-kratos/kratos/v3/config" +) + +var _ config.Source = (*file)(nil) + +type file struct { + path string +} + +// NewSource new a file source. +func NewSource(path string) config.Source { + return &file{path: path} +} + +func (f *file) loadFile(path string) (*config.KeyValue, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + data, err := io.ReadAll(file) + if err != nil { + return nil, err + } + info, err := file.Stat() + if err != nil { + return nil, err + } + return &config.KeyValue{ + Key: info.Name(), + Format: format(info.Name()), + Value: data, + }, nil +} + +func (f *file) loadDir(path string) (kvs []*config.KeyValue, err error) { + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + for _, file := range files { + // ignore hidden files + if file.IsDir() || strings.HasPrefix(file.Name(), ".") { + continue + } + kv, err := f.loadFile(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + kvs = append(kvs, kv) + } + return +} + +func (f *file) Load() (kvs []*config.KeyValue, err error) { + fi, err := os.Stat(f.path) + if err != nil { + return nil, err + } + if fi.IsDir() { + return f.loadDir(f.path) + } + kv, err := f.loadFile(f.path) + if err != nil { + return nil, err + } + return []*config.KeyValue{kv}, nil +} + +func (f *file) Watch() (config.Watcher, error) { + return newWatcher(f) +} diff --git a/config/file/file_test.go b/config/file/file_test.go new file mode 100644 index 0000000..dff5d52 --- /dev/null +++ b/config/file/file_test.go @@ -0,0 +1,341 @@ +package file + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "sync" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/config" +) + +const ( + _testJSON = ` +{ + "test":{ + "settings":{ + "int_key":1000, + "float_key":1000.1, + "duration_key":10000, + "string_key":"string_value" + }, + "server":{ + "addr":"127.0.0.1", + "port":8000 + } + }, + "foo":[ + { + "name":"nihao", + "age":18 + }, + { + "name":"nihao", + "age":18 + } + ] +}` + + _testJSONUpdate = ` +{ + "test":{ + "settings":{ + "int_key":1000, + "float_key":1000.1, + "duration_key":10000, + "string_key":"string_value" + }, + "server":{ + "addr":"127.0.0.1", + "port":8000 + } + }, + "foo":[ + { + "name":"nihao", + "age":18 + }, + { + "name":"nihao", + "age":18 + } + ], + "bar":{ + "event":"update" + } +}` + + // _testYaml = ` + //Foo: + // bar : + // - {name: nihao,age: 1} + // - {name: nihao,age: 1} + // + // + //` +) + +//func TestScan(t *testing.T) { +// +//} + +func TestFile(t *testing.T) { + var ( + path = filepath.Join(t.TempDir(), "test_config") + file = filepath.Join(path, "test.json") + data = []byte(_testJSON) + ) + defer os.Remove(path) + if err := os.MkdirAll(path, 0o700); err != nil { + t.Error(err) + } + if err := os.WriteFile(file, data, 0o666); err != nil { + t.Error(err) + } + testSource(t, file, data) + testSource(t, path, data) + testWatchFile(t, file) + testWatchDir(t, path, file) +} + +func testWatchFile(t *testing.T, path string) { + t.Log(path) + + s := NewSource(path) + watch, err := s.Watch() + if err != nil { + t.Error(err) + } + + f, err := os.OpenFile(path, os.O_RDWR, 0) + if err != nil { + t.Error(err) + } + defer f.Close() + _, err = f.WriteString(_testJSONUpdate) + if err != nil { + t.Error(err) + } + kvs, err := watch.Next() + if err != nil { + t.Errorf("watch.Next() error(%v)", err) + } + if !reflect.DeepEqual(string(kvs[0].Value), _testJSONUpdate) { + t.Errorf("string(kvs[0].Value(%v) is not equal to _testJSONUpdate(%v)", kvs[0].Value, _testJSONUpdate) + } + + newFilepath := filepath.Join(filepath.Dir(path), "test1.json") + if err = os.Rename(path, newFilepath); err != nil { + t.Error(err) + } + kvs, err = watch.Next() + if err == nil { + t.Errorf("watch.Next() error(%v)", err) + } + if kvs != nil { + t.Errorf("watch.Next() error(%v)", err) + } + + err = watch.Stop() + if err != nil { + t.Errorf("watch.Stop() error(%v)", err) + } + + if err := os.Rename(newFilepath, path); err != nil { + t.Error(err) + } +} + +func testWatchDir(t *testing.T, path, file string) { + t.Log(path) + t.Log(file) + + s := NewSource(path) + watch, err := s.Watch() + if err != nil { + t.Error(err) + } + + f, err := os.OpenFile(file, os.O_RDWR, 0) + if err != nil { + t.Error(err) + } + defer f.Close() + _, err = f.WriteString(_testJSONUpdate) + if err != nil { + t.Error(err) + } + + kvs, err := watch.Next() + if err != nil { + t.Errorf("watch.Next() error(%v)", err) + } + if !reflect.DeepEqual(string(kvs[0].Value), _testJSONUpdate) { + t.Errorf("string(kvs[0].Value(%s) is not equal to _testJSONUpdate(%v)", kvs[0].Value, _testJSONUpdate) + } +} + +func testSource(t *testing.T, path string, data []byte) { + t.Log(path) + + s := NewSource(path) + kvs, err := s.Load() + if err != nil { + t.Error(err) + } + if string(kvs[0].Value) != string(data) { + t.Errorf("no expected: %s, but got: %s", kvs[0].Value, data) + } +} + +func TestConfig(t *testing.T) { + path := filepath.Join(t.TempDir(), "test_config.json") + defer os.Remove(path) + if err := os.WriteFile(path, []byte(_testJSON), 0o666); err != nil { + t.Error(err) + } + c := config.New(config.WithSource( + NewSource(path), + )) + testScan(t, c) + + testConfig(t, c) +} + +func testConfig(t *testing.T, c config.Config) { + expected := map[string]any{ + "test.settings.int_key": int64(1000), + "test.settings.float_key": 1000.1, + "test.settings.string_key": "string_value", + "test.settings.duration_key": time.Duration(10000), + "test.server.addr": "127.0.0.1", + "test.server.port": int64(8000), + } + if err := c.Load(); err != nil { + t.Error(err) + } + for key, value := range expected { + switch value.(type) { + case int64: + if v, err := c.Value(key).Int(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + case float64: + if v, err := c.Value(key).Float(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + case string: + if v, err := c.Value(key).String(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + case time.Duration: + if v, err := c.Value(key).Duration(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + } + } + // scan + var settings struct { + IntKey int64 `json:"int_key"` + FloatKey float64 `json:"float_key"` + StringKey string `json:"string_key"` + DurationKey time.Duration `json:"duration_key"` + } + if err := c.Value("test.settings").Scan(&settings); err != nil { + t.Error(err) + } + if v := expected["test.settings.int_key"]; settings.IntKey != v { + t.Errorf("no expect int_key value: %v, but got: %v", settings.IntKey, v) + } + if v := expected["test.settings.float_key"]; settings.FloatKey != v { + t.Errorf("no expect float_key value: %v, but got: %v", settings.FloatKey, v) + } + if v := expected["test.settings.string_key"]; settings.StringKey != v { + t.Errorf("no expect string_key value: %v, but got: %v", settings.StringKey, v) + } + if v := expected["test.settings.duration_key"]; settings.DurationKey != v { + t.Errorf("no expect duration_key value: %v, but got: %v", settings.DurationKey, v) + } + + // not found + if _, err := c.Value("not_found_key").Bool(); errors.Is(err, config.ErrNotFound) { + t.Logf("not_found_key not match: %v", err) + } +} + +func testScan(t *testing.T, c config.Config) { + type TestJSON struct { + Test struct { + Settings struct { + IntKey int `json:"int_key"` + FloatKey float64 `json:"float_key"` + DurationKey int `json:"duration_key"` + StringKey string `json:"string_key"` + } `json:"settings"` + Server struct { + Addr string `json:"addr"` + Port int `json:"port"` + } `json:"server"` + } `json:"test"` + Foo []struct { + Name string `json:"name"` + Age int `json:"age"` + } `json:"foo"` + } + var conf TestJSON + if err := c.Load(); err != nil { + t.Error(err) + } + if err := c.Scan(&conf); err != nil { + t.Error(err) + } + t.Log(conf) +} + +func TestMergeDataRace(t *testing.T) { + path := filepath.Join(t.TempDir(), "test_config.json") + defer os.Remove(path) + if err := os.WriteFile(path, []byte(_testJSON), 0o666); err != nil { + t.Error(err) + } + c := config.New(config.WithSource( + NewSource(path), + )) + const count = 80 + wg := &sync.WaitGroup{} + wg.Add(2) + startCh := make(chan struct{}) + go func() { + defer wg.Done() + <-startCh + for i := 0; i < count; i++ { + var conf struct{} + if err := c.Scan(&conf); err != nil { + t.Error(err) + } + } + }() + + go func() { + defer wg.Done() + <-startCh + for i := 0; i < count; i++ { + if err := c.Load(); err != nil { + t.Error(err) + } + } + }() + close(startCh) + wg.Wait() +} diff --git a/config/file/format.go b/config/file/format.go new file mode 100644 index 0000000..69c44c8 --- /dev/null +++ b/config/file/format.go @@ -0,0 +1,10 @@ +package file + +import "strings" + +func format(name string) string { + if idx := strings.LastIndexByte(name, '.'); idx >= 0 { + return name[idx+1:] + } + return "" +} diff --git a/config/file/format_test.go b/config/file/format_test.go new file mode 100644 index 0000000..a7aaae0 --- /dev/null +++ b/config/file/format_test.go @@ -0,0 +1,57 @@ +package file + +import ( + "testing" +) + +func TestFormat(t *testing.T) { + tests := []struct { + input string + expect string + }{ + { + input: "", + expect: "", + }, + { + input: " ", + expect: "", + }, + { + input: ".", + expect: "", + }, + { + input: "a", + expect: "", + }, + { + input: "a.", + expect: "", + }, + { + input: ".b", + expect: "b", + }, + { + input: "a.b", + expect: "b", + }, + { + input: "a.b.c", + expect: "c", + }, + } + for _, v := range tests { + content := format(v.input) + if got, want := content, v.expect; got != want { + t.Errorf("expect %v,got %v", want, got) + } + } +} + +func BenchmarkFormat(b *testing.B) { + for i := 0; i < b.N; i++ { + format("abc.txt") + } +} diff --git a/config/file/watcher.go b/config/file/watcher.go new file mode 100644 index 0000000..a63789f --- /dev/null +++ b/config/file/watcher.go @@ -0,0 +1,70 @@ +package file + +import ( + "context" + "os" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" + + "github.com/go-kratos/kratos/v3/config" +) + +var _ config.Watcher = (*watcher)(nil) + +type watcher struct { + f *file + fw *fsnotify.Watcher + + ctx context.Context + cancel context.CancelFunc +} + +func newWatcher(f *file) (config.Watcher, error) { + fw, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + if err := fw.Add(f.path); err != nil { + return nil, err + } + ctx, cancel := context.WithCancel(context.Background()) + return &watcher{f: f, fw: fw, ctx: ctx, cancel: cancel}, nil +} + +func (w *watcher) Next() ([]*config.KeyValue, error) { + select { + case <-w.ctx.Done(): + return nil, w.ctx.Err() + case event := <-w.fw.Events: + if event.Has(fsnotify.Rename) { + if _, err := os.Stat(event.Name); err == nil || os.IsExist(err) { + if err := w.fw.Add(event.Name); err != nil { + return nil, err + } + } + } + fi, err := os.Stat(w.f.path) + if err != nil { + return nil, err + } + path := w.f.path + if fi.IsDir() { + path = filepath.Join(w.f.path, filepath.Base(event.Name)) + } + time.Sleep(time.Millisecond) + kv, err := w.f.loadFile(path) + if err != nil { + return nil, err + } + return []*config.KeyValue{kv}, nil + case err := <-w.fw.Errors: + return nil, err + } +} + +func (w *watcher) Stop() error { + w.cancel() + return w.fw.Close() +} diff --git a/config/merge.go b/config/merge.go new file mode 100644 index 0000000..d043e5e --- /dev/null +++ b/config/merge.go @@ -0,0 +1,48 @@ +package config + +import "fmt" + +func defaultMerge(dst, src any) error { + dstMap, ok := dst.(*map[string]any) + if !ok { + return fmt.Errorf("config: merge dst must be *map[string]interface{}, got %T", dst) + } + srcMap, ok := convertMap(src).(map[string]any) + if !ok { + return fmt.Errorf("config: merge src must be map[string]interface{}, got %T", src) + } + if *dstMap == nil { + *dstMap = make(map[string]any, len(srcMap)) + } + mergeMap(*dstMap, srcMap) + return nil +} + +func mergeMap(dst, src map[string]any) { + for key, srcValue := range src { + if srcMap, ok := srcValue.(map[string]any); ok { + if dstMap, ok := dst[key].(map[string]any); ok { + mergeMap(dstMap, srcMap) + continue + } + } + dst[key] = cloneMergeValue(srcValue) + } +} + +func cloneMergeValue(v any) any { + switch val := v.(type) { + case map[string]any: + cloned := make(map[string]any, len(val)) + mergeMap(cloned, val) + return cloned + case []any: + cloned := make([]any, len(val)) + for i, item := range val { + cloned[i] = cloneMergeValue(item) + } + return cloned + default: + return val + } +} diff --git a/config/merge_test.go b/config/merge_test.go new file mode 100644 index 0000000..554d438 --- /dev/null +++ b/config/merge_test.go @@ -0,0 +1,126 @@ +package config + +import ( + "reflect" + "testing" +) + +func TestDefaultMerge(t *testing.T) { + tests := []struct { + name string + dst map[string]any + src any + want map[string]any + }{ + { + name: "merge nested maps and override leaves", + dst: map[string]any{ + "server": map[string]any{ + "http": map[string]any{ + "addr": "0.0.0.0", + "port": 80, + }, + "grpc": true, + }, + "endpoints": []any{"a.example.com"}, + }, + src: map[string]any{ + "server": map[string]any{ + "http": map[string]any{ + "port": 8080, + "tls": true, + }, + }, + "endpoints": []any{"b.example.com", "c.example.com"}, + }, + want: map[string]any{ + "server": map[string]any{ + "http": map[string]any{ + "addr": "0.0.0.0", + "port": 8080, + "tls": true, + }, + "grpc": true, + }, + "endpoints": []any{"b.example.com", "c.example.com"}, + }, + }, + { + name: "override type conflicts and nil values", + dst: map[string]any{ + "map_to_scalar": map[string]any{"value": "old"}, + "scalar_to_map": "old", + "nil_value": "old", + }, + src: map[string]any{ + "map_to_scalar": "new", + "scalar_to_map": map[string]any{"value": "new"}, + "nil_value": nil, + }, + want: map[string]any{ + "map_to_scalar": "new", + "scalar_to_map": map[string]any{"value": "new"}, + "nil_value": nil, + }, + }, + { + name: "convert map keys", + dst: map[string]any{}, + src: map[any]any{ + "service": map[any]any{ + "name": "kratos", + }, + }, + want: map[string]any{ + "service": map[string]any{ + "name": "kratos", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := defaultMerge(&tt.dst, tt.src); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(tt.dst, tt.want) { + t.Fatalf("defaultMerge() = %#v, want %#v", tt.dst, tt.want) + } + }) + } +} + +func TestDefaultMergeClonesSourceValues(t *testing.T) { + srcMap := map[string]any{"name": "kratos"} + srcSlice := []any{map[string]any{"port": 8000}} + dst := map[string]any{} + + if err := defaultMerge(&dst, map[string]any{ + "server": srcMap, + "listeners": srcSlice, + }); err != nil { + t.Fatal(err) + } + + srcMap["name"] = "changed" + srcSlice[0].(map[string]any)["port"] = 9000 + + want := map[string]any{ + "server": map[string]any{"name": "kratos"}, + "listeners": []any{map[string]any{"port": 8000}}, + } + if !reflect.DeepEqual(dst, want) { + t.Fatalf("defaultMerge() retained source aliases: got %#v, want %#v", dst, want) + } +} + +func TestDefaultMergeInvalidInput(t *testing.T) { + dst := map[string]any{} + if err := defaultMerge(dst, map[string]any{}); err == nil { + t.Fatal("defaultMerge() error is nil for non-pointer dst") + } + if err := defaultMerge(&dst, []any{}); err == nil { + t.Fatal("defaultMerge() error is nil for non-map src") + } +} diff --git a/config/options.go b/config/options.go new file mode 100644 index 0000000..7962bc7 --- /dev/null +++ b/config/options.go @@ -0,0 +1,203 @@ +package config + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Decoder is config decoder. +type Decoder func(*KeyValue, map[string]any) error + +// Resolver resolve placeholder in config. +type Resolver func(map[string]any) error + +// Merge is config merge func. +type Merge func(dst, src any) error + +// Option is config option. +type Option func(*options) + +type options struct { + sources []Source + decoder Decoder + resolver Resolver + merge Merge +} + +const ( + boolTrueValue = "true" + boolFalseValue = "false" +) + +// WithSource with config source. +func WithSource(s ...Source) Option { + return func(o *options) { + o.sources = s + } +} + +// WithDecoder with config decoder. +// DefaultDecoder behavior: +// If KeyValue.Format is non-empty, then KeyValue.Value will be deserialized into map[string]interface{} +// and stored in the config cache(map[string]interface{}) +// if KeyValue.Format is empty,{KeyValue.Key : KeyValue.Value} will be stored in config cache(map[string]interface{}) +func WithDecoder(d Decoder) Option { + return func(o *options) { + o.decoder = d + } +} + +// WithResolveActualTypes with config resolver. +// bool input will enable conversion of config to data types +func WithResolveActualTypes(enableConvertToType bool) Option { + return func(o *options) { + o.resolver = newActualTypesResolver(enableConvertToType) + } +} + +// WithResolver with config resolver. +func WithResolver(r Resolver) Option { + return func(o *options) { + o.resolver = r + } +} + +// WithMergeFunc with config merge func. +func WithMergeFunc(m Merge) Option { + return func(o *options) { + o.merge = m + } +} + +// defaultDecoder decode config from source KeyValue +// to target map[string]interface{} using src.Format codec. +func defaultDecoder(src *KeyValue, target map[string]any) error { + if src.Format == "" { + // expand key "aaa.bbb" into map[aaa]map[bbb]interface{} + keys := strings.Split(src.Key, ".") + for i, k := range keys { + if i == len(keys)-1 { + target[k] = src.Value + } else { + sub := make(map[string]any) + target[k] = sub + target = sub + } + } + return nil + } + if codec := encoding.GetCodec(src.Format); codec != nil { + return codec.Unmarshal(src.Value, &target) + } + return fmt.Errorf("unsupported key: %s format: %s", src.Key, src.Format) +} + +func newActualTypesResolver(enableConvertToType bool) func(map[string]any) error { + return func(input map[string]any) error { + mapper := mapper(input) + return resolver(input, mapper, enableConvertToType) + } +} + +// defaultResolver resolve placeholder in map value, +// placeholder format in ${key:default}. +func defaultResolver(input map[string]any) error { + mapper := mapper(input) + return resolver(input, mapper, false) +} + +func resolver(input map[string]any, mapper func(name string) string, toType bool) error { + var resolve func(map[string]any) error + resolve = func(sub map[string]any) error { + for k, v := range sub { + switch vt := v.(type) { + case string: + sub[k] = expand(vt, mapper, toType) + case map[string]any: + if err := resolve(vt); err != nil { + return err + } + case []any: + for i, iface := range vt { + switch it := iface.(type) { + case string: + vt[i] = expand(it, mapper, toType) + case map[string]any: + if err := resolve(it); err != nil { + return err + } + } + } + sub[k] = vt + } + } + return nil + } + return resolve(input) +} + +func mapper(input map[string]any) func(name string) string { + mapper := func(name string) string { + args := strings.SplitN(strings.TrimSpace(name), ":", 2) //nolint:mnd + if v, has := readValue(input, args[0]); has { + s, _ := v.String() + return s + } else if len(args) > 1 { // default value + return args[1] + } + return "" + } + return mapper +} + +func convertToType(input string) any { + // Check if the input is a string with quotes + if strings.HasPrefix(input, "\"") && strings.HasSuffix(input, "\"") { + // Trim the quotes and return the string value + return strings.Trim(input, "\"") + } + + // Try converting to bool + if input == boolTrueValue || input == boolFalseValue { + b, _ := strconv.ParseBool(input) + return b + } + + // Try converting to float64 + if strings.Contains(input, ".") { + if f, err := strconv.ParseFloat(input, 64); err == nil { + return f + } + } + + // Try converting to int64 + if i, err := strconv.ParseInt(input, 10, 64); err == nil { + return i + } + + // Default to string if no other conversion succeeds + return input +} + +// placeholderRegexp matches ${...} placeholders in config value +var placeholderRegexp = regexp.MustCompile(`\${(.*?)}`) + +func expand(s string, mapping func(string) string, toType bool) any { + re := placeholderRegexp.FindAllStringSubmatch(s, -1) + var ct any + for _, i := range re { + if len(i) == 2 { //nolint:mnd + m := mapping(i[1]) + if toType { + ct = convertToType(m) + return ct + } + s = strings.ReplaceAll(s, i[0], m) + } + } + return s +} diff --git a/config/options_test.go b/config/options_test.go new file mode 100644 index 0000000..c9af321 --- /dev/null +++ b/config/options_test.go @@ -0,0 +1,397 @@ +package config + +import ( + "reflect" + "strings" + "testing" +) + +func TestDefaultDecoder(t *testing.T) { + src := &KeyValue{ + Key: "service", + Value: []byte("config"), + Format: "", + } + target := make(map[string]any) + err := defaultDecoder(src, target) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(target, map[string]any{"service": []byte("config")}) { + t.Fatal(`target is not equal to map[string]interface{}{"service": "config"}`) + } + + src = &KeyValue{ + Key: "service.name.alias", + Value: []byte("2233"), + Format: "", + } + target = make(map[string]any) + err = defaultDecoder(src, target) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(map[string]any{ + "service": map[string]any{ + "name": map[string]any{ + "alias": []byte("2233"), + }, + }, + }, target) { + t.Fatal(`target is not equal to map[string]interface{}{"service": map[string]interface{}{"name": map[string]interface{}{"alias": []byte("2233")}}}`) + } +} + +func TestDefaultResolver(t *testing.T) { + var ( + portString = "8080" + countInt = 10 + rateFloat = 0.9 + ) + + data := map[string]any{ + "foo": map[string]any{ + "bar": map[string]any{ + "notexist": "${NOTEXIST:100}", + "port": "${PORT:8081}", + "count": "${COUNT:0}", + "enable": "${ENABLE:false}", + "rate": "${RATE}", + "empty": "${EMPTY:foobar}", + "url": "${URL:http://example.com}", + "array": []any{ + "${PORT}", + map[string]any{"foobar": "${NOTEXIST:8081}"}, + }, + "value1": "${test.value}", + "value2": "$PORT", + "value3": "abc${PORT}foo${COUNT}bar", + "value4": "${foo${bar}}", + }, + }, + "test": map[string]any{ + "value": "foobar", + }, + "PORT": "8080", + "COUNT": "10", + "ENABLE": "true", + "RATE": "0.9", + "EMPTY": "", + } + + tests := []struct { + name string + path string + expect any + }{ + { + name: "test not exist int env with default", + path: "foo.bar.notexist", + expect: 100, + }, + { + name: "test string with default", + path: "foo.bar.port", + expect: portString, + }, + { + name: "test int with default", + path: "foo.bar.count", + expect: countInt, + }, + { + name: "test bool with default", + path: "foo.bar.enable", + expect: true, + }, + { + name: "test float without default", + path: "foo.bar.rate", + expect: rateFloat, + }, + { + name: "test empty value with default", + path: "foo.bar.empty", + expect: "", + }, + { + name: "test url with default", + path: "foo.bar.url", + expect: "http://example.com", + }, + { + name: "test array", + path: "foo.bar.array", + expect: []any{portString, map[string]any{"foobar": "8081"}}, + }, + { + name: "test ${test.value}", + path: "foo.bar.value1", + expect: "foobar", + }, + { + name: "test $PORT", + path: "foo.bar.value2", + expect: "$PORT", + }, + { + name: "test abc${PORT}foo${COUNT}bar", + path: "foo.bar.value3", + expect: "abc8080foo10bar", + }, + { + name: "test ${foo${bar}}", + path: "foo.bar.value4", + expect: "}", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := defaultResolver(data) + if err != nil { + t.Fatal(err) + } + rd := reader{ + values: data, + } + if v, ok := rd.Value(test.path); ok { + var actual any + switch test.expect.(type) { + case int: + if actual, err = v.Int(); err == nil { + if !reflect.DeepEqual(test.expect.(int), int(actual.(int64))) { + t.Fatal("expect is not equal to actual") + } + } + case string: + if actual, err = v.String(); err == nil { + if !reflect.DeepEqual(test.expect, actual) { + t.Fatal("expect is not equal to actual") + } + } + case bool: + if actual, err = v.Bool(); err == nil { + if !reflect.DeepEqual(test.expect, actual) { + t.Fatal("expect is not equal to actual") + } + } + case float64: + if actual, err = v.Float(); err == nil { + if !reflect.DeepEqual(test.expect, actual) { + t.Fatal("expect is not equal to actual") + } + } + default: + actual = v.Load() + if !reflect.DeepEqual(test.expect, actual) { + t.Logf("expect: %#v, actual: %#v", test.expect, actual) + t.Fail() + } + } + if err != nil { + t.Error(err) + } + } else { + t.Error("value path not found") + } + }) + } +} + +func TestNewDefaultResolver(t *testing.T) { + var ( + portString = "8080" + countInt = 10 + rateFloat = 0.9 + ) + + data := map[string]any{ + "foo": map[string]any{ + "bar": map[string]any{ + "notexist": "${NOTEXIST:100}", + "port": "${PORT:\"8081\"}", + "count": "${COUNT:\"0\"}", + "enable": "${ENABLE:false}", + "rate": "${RATE}", + "empty": "${EMPTY:foobar}", + "url": "${URL:\"http://example.com\"}", + "array": []any{ + "${PORT}", + map[string]any{"foobar": "${NOTEXIST:\"8081\"}"}, + }, + "value1": "${test.value}", + "value2": "$PORT", + "value3": "abc${PORT}foo${COUNT}bar", + "value4": "${foo${bar}}", + }, + }, + "test": map[string]any{ + "value": "foobar", + }, + "PORT": "\"8080\"", + "COUNT": "\"10\"", + "ENABLE": "true", + "RATE": "0.9", + "EMPTY": "", + } + + tests := []struct { + name string + path string + expect any + }{ + { + name: "test not exist int env with default", + path: "foo.bar.notexist", + expect: 100, + }, + { + name: "test string with default", + path: "foo.bar.port", + expect: portString, + }, + { + name: "test int with default", + path: "foo.bar.count", + expect: countInt, + }, + { + name: "test bool with default", + path: "foo.bar.enable", + expect: true, + }, + { + name: "test float without default", + path: "foo.bar.rate", + expect: rateFloat, + }, + { + name: "test empty value with default", + path: "foo.bar.empty", + expect: "", + }, + { + name: "test url with default", + path: "foo.bar.url", + expect: "http://example.com", + }, + { + name: "test array", + path: "foo.bar.array", + expect: []any{portString, map[string]any{"foobar": "8081"}}, + }, + { + name: "test ${test.value}", + path: "foo.bar.value1", + expect: "foobar", + }, + { + name: "test $PORT", + path: "foo.bar.value2", + expect: "$PORT", + }, + //{ + // name: "test abc${PORT}foo${COUNT}bar", + // path: "foo.bar.value3", + // expect: "abc8080foo10bar", + //}, + { + name: "test ${foo${bar}}", + path: "foo.bar.value4", + expect: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fn := newActualTypesResolver(true) + err := fn(data) + if err != nil { + t.Fatal(err) + } + rd := reader{ + values: data, + } + if v, ok := rd.Value(test.path); ok { + var actual any + switch test.expect.(type) { + case int: + if actual, err = v.Int(); err == nil { + if !reflect.DeepEqual(test.expect.(int), int(actual.(int64))) { + t.Fatal("expect is not equal to actual") + } + } + case string: + if actual, err = v.String(); err == nil { + if !reflect.DeepEqual(test.expect, actual) { + t.Fatal("expect is not equal to actual") + } + } + case bool: + if actual, err = v.Bool(); err == nil { + if !reflect.DeepEqual(test.expect, actual) { + t.Fatal("expect is not equal to actual") + } + } + case float64: + if actual, err = v.Float(); err == nil { + if !reflect.DeepEqual(test.expect, actual) { + t.Fatal("expect is not equal to actual") + } + } + default: + actual = v.Load() + if !reflect.DeepEqual(test.expect, actual) { + t.Logf("expect: %#v, actual: %#v", test.expect, actual) + t.Fail() + } + } + if err != nil { + t.Error(err) + } + } else { + t.Error("value path not found") + } + }) + } +} + +func TestExpand(t *testing.T) { + tests := []struct { + input string + mapping func(string) string + want string + }{ + { + input: "${a}", + mapping: func(s string) string { + return strings.ToUpper(s) + }, + want: "A", + }, + { + input: "a", + mapping: func(s string) string { + return strings.ToUpper(s) + }, + want: "a", + }, + } + for _, tt := range tests { + if got := expand(tt.input, tt.mapping, false); got != tt.want { + t.Errorf("expand() want: %s, got: %s", tt.want, got) + } + } +} + +func TestWithMergeFunc(t *testing.T) { + c := &options{} + a := func(any, any) error { + return nil + } + WithMergeFunc(a)(c) + if c.merge == nil { + t.Fatal("c.merge is nil") + } +} diff --git a/config/reader.go b/config/reader.go new file mode 100644 index 0000000..7ca8f75 --- /dev/null +++ b/config/reader.go @@ -0,0 +1,172 @@ +package config + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "strings" + "sync" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v3/log" +) + +// Reader is config reader. +type Reader interface { + Merge(...*KeyValue) error + Value(string) (Value, bool) + Source() ([]byte, error) + Resolve() error +} + +type reader struct { + opts options + values map[string]any + lock sync.Mutex +} + +func newReader(opts options) Reader { + return &reader{ + opts: opts, + values: make(map[string]any), + lock: sync.Mutex{}, + } +} + +func (r *reader) Merge(kvs ...*KeyValue) error { + merged, err := r.cloneMap() + if err != nil { + return err + } + for _, kv := range kvs { + next := make(map[string]any) + if err := r.opts.decoder(kv, next); err != nil { + log.Error("failed to decode config", "error", err, "key", kv.Key, "value", string(kv.Value)) + return err + } + if err := r.opts.merge(&merged, convertMap(next)); err != nil { + log.Error("failed to merge config", "error", err, "key", kv.Key, "value", string(kv.Value)) + return err + } + } + r.lock.Lock() + r.values = merged + r.lock.Unlock() + return nil +} + +func (r *reader) Value(path string) (Value, bool) { + r.lock.Lock() + defer r.lock.Unlock() + return readValue(r.values, path) +} + +func (r *reader) Source() ([]byte, error) { + r.lock.Lock() + defer r.lock.Unlock() + return marshalJSON(convertMap(r.values)) +} + +func (r *reader) Resolve() error { + r.lock.Lock() + defer r.lock.Unlock() + return r.opts.resolver(r.values) +} + +func (r *reader) cloneMap() (map[string]any, error) { + r.lock.Lock() + defer r.lock.Unlock() + return cloneMap(r.values) +} + +func cloneMap(src map[string]any) (map[string]any, error) { + // https://gist.github.com/soroushjp/0ec92102641ddfc3ad5515ca76405f4d + var buf bytes.Buffer + gob.Register(map[string]any{}) + gob.Register([]any{}) + enc := gob.NewEncoder(&buf) + dec := gob.NewDecoder(&buf) + err := enc.Encode(src) + if err != nil { + return nil, err + } + var clone map[string]any + err = dec.Decode(&clone) + if err != nil { + return nil, err + } + return clone, nil +} + +func convertMap(src any) any { + switch m := src.(type) { + case map[string]any: + dst := make(map[string]any, len(m)) + for k, v := range m { + dst[k] = convertMap(v) + } + return dst + case map[any]any: + dst := make(map[string]any, len(m)) + for k, v := range m { + dst[fmt.Sprint(k)] = convertMap(v) + } + return dst + case []any: + dst := make([]any, len(m)) + for k, v := range m { + dst[k] = convertMap(v) + } + return dst + case []byte: + // there will be no binary data in the config data + return string(m) + default: + return src + } +} + +// readValue read Value in given map[string]interface{} +// by the given path, will return false if not found. +func readValue(values map[string]any, path string) (Value, bool) { + var ( + next = values + keys = strings.Split(path, ".") + last = len(keys) - 1 + ) + for idx, key := range keys { + value, ok := next[key] + if !ok { + return nil, false + } + if idx == last { + av := &atomicValue{} + av.Store(value) + return av, true + } + switch vm := value.(type) { + case map[string]any: + next = vm + default: + return nil, false + } + } + return nil, false +} + +func marshalJSON(v any) ([]byte, error) { + if m, ok := v.(proto.Message); ok { + return protojson.MarshalOptions{EmitUnpopulated: true}.Marshal(m) + } + return json.Marshal(v) +} + +func unmarshalJSON(data []byte, v any) error { + if m, ok := v.(proto.Message); ok { + return protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(data, m) + } + return json.Unmarshal(data, v) +} diff --git a/config/reader_test.go b/config/reader_test.go new file mode 100644 index 0000000..f8f1b90 --- /dev/null +++ b/config/reader_test.go @@ -0,0 +1,322 @@ +package config + +import ( + "fmt" + "reflect" + "testing" + + "github.com/go-kratos/kratos/v3/encoding" +) + +func TestReader_Merge(t *testing.T) { + var ( + err error + ok bool + ) + opts := options{ + decoder: func(kv *KeyValue, v map[string]any) error { + if codec := encoding.GetCodec(kv.Format); codec != nil { + return codec.Unmarshal(kv.Value, &v) + } + return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format) + }, + resolver: defaultResolver, + merge: defaultMerge, + } + r := newReader(opts) + err = r.Merge(&KeyValue{ + Key: "a", + Value: []byte("bad"), + Format: "json", + }) + if err == nil { + t.Fatal("err is nil") + } + + err = r.Merge(&KeyValue{ + Key: "b", + Value: []byte(`{"nice": "boat", "x": 1}`), + Format: "json", + }) + if err != nil { + t.Fatal(err) + } + vv, ok := r.Value("nice") + if !ok { + t.Fatal("ok is false") + } + vvv, err := vv.String() + if err != nil { + t.Fatal(err) + } + if vvv != "boat" { + t.Fatal(`vvv is not equal to "boat"`) + } + + err = r.Merge(&KeyValue{ + Key: "b", + Value: []byte(`{"x": 2}`), + Format: "json", + }) + if err != nil { + t.Fatal(err) + } + vv, ok = r.Value("x") + if !ok { + t.Fatal("ok is false") + } + vvx, err := vv.Int() + if err != nil { + t.Fatal(err) + } + if vvx != 2 { + t.Fatal("vvx is not equal to 2") + } +} + +func TestReader_Value(t *testing.T) { + opts := options{ + decoder: func(kv *KeyValue, v map[string]any) error { + if codec := encoding.GetCodec(kv.Format); codec != nil { + return codec.Unmarshal(kv.Value, &v) + } + return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format) + }, + resolver: defaultResolver, + merge: defaultMerge, + } + + ymlval := ` +a: + b: + X: 1 + Y: "lol" + z: true +` + tests := []struct { + name string + kv KeyValue + }{ + { + name: "json value", + kv: KeyValue{ + Key: "config", + Value: []byte(`{"a": {"b": {"X": 1, "Y": "lol", "z": true}}}`), + Format: "json", + }, + }, + { + name: "yaml value", + kv: KeyValue{ + Key: "config", + Value: []byte(ymlval), + Format: "yaml", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r := newReader(opts) + err := r.Merge(&test.kv) + if err != nil { + t.Fatal(err) + } + vv, ok := r.Value("a.b.X") + if !ok { + t.Fatal("ok is false") + } + vvv, err := vv.Int() + if err != nil { + t.Fatal(err) + } + if int64(1) != vvv { + t.Fatal("vvv is not equal to 1") + } + + vv, ok = r.Value("a.b.Y") + if !ok { + t.Fatal("ok is false") + } + vvy, err := vv.String() + if err != nil { + t.Fatal(err) + } + if vvy != "lol" { + t.Fatal(`vvy is not equal to "lol"`) + } + + vv, ok = r.Value("a.b.z") + if !ok { + t.Fatal("ok is false") + } + vvz, err := vv.Bool() + if err != nil { + t.Fatal(err) + } + if !vvz { + t.Fatal("vvz is not equal to true") + } + + _, ok = r.Value("aasasdg=234l.asdfk,") + if ok { + t.Fatal("ok is true") + } + + _, ok = r.Value("aas......asdg=234l.asdfk,") + if ok { + t.Fatal("ok is true") + } + + _, ok = r.Value("a.b.Y.") + if ok { + t.Fatal("ok is true") + } + }) + } +} + +func TestReader_Source(t *testing.T) { + var err error + opts := options{ + decoder: func(kv *KeyValue, v map[string]any) error { + if codec := encoding.GetCodec(kv.Format); codec != nil { + return codec.Unmarshal(kv.Value, &v) + } + return fmt.Errorf("unsupported key: %s format: %s", kv.Key, kv.Format) + }, + resolver: defaultResolver, + merge: defaultMerge, + } + r := newReader(opts) + err = r.Merge(&KeyValue{ + Key: "b", + Value: []byte(`{"a": {"b": {"X": 1}}}`), + Format: "json", + }) + if err != nil { + t.Fatal(err) + } + b, err := r.Source() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual([]byte(`{"a":{"b":{"X":1}}}`), b) { + t.Fatal("[]byte(`{\"a\":{\"b\":{\"X\":1}}}`) is not equal to b") + } +} + +func TestCloneMap(t *testing.T) { + tests := []struct { + input map[string]any + want map[string]any + }{ + { + input: map[string]any{ + "a": 1, + "b": "2", + "c": true, + }, + want: map[string]any{ + "a": 1, + "b": "2", + "c": true, + }, + }, + { + input: map[string]any{}, + want: map[string]any{}, + }, + { + input: nil, + want: map[string]any{}, + }, + } + for _, tt := range tests { + if got, err := cloneMap(tt.input); err != nil { + t.Errorf("expect no err, got %v", err) + } else if !reflect.DeepEqual(got, tt.want) { + t.Errorf("cloneMap(%v) = %v, want %v", tt.input, got, tt.want) + } + } +} + +func TestConvertMap(t *testing.T) { + tests := []struct { + input any + want any + }{ + { + input: map[string]any{ + "a": 1, + "b": "2", + "c": true, + "d": []byte{65, 66, 67}, + }, + want: map[string]any{ + "a": 1, + "b": "2", + "c": true, + "d": "ABC", + }, + }, + { + input: []any{1, 2.0, "3", true, nil, []any{1, 2.0, "3", true, nil}}, + want: []any{1, 2.0, "3", true, nil, []any{1, 2.0, "3", true, nil}}, + }, + { + input: []byte{65, 66, 67}, + want: "ABC", + }, + } + for _, tt := range tests { + if got := convertMap(tt.input); !reflect.DeepEqual(got, tt.want) { + t.Errorf("convertMap(%v) = %v, want %v", tt.input, got, tt.want) + } + } +} + +func TestReadValue(t *testing.T) { + m := map[string]any{ + "a": 1, + "b": map[string]any{ + "c": "3", + "d": map[string]any{ + "e": true, + }, + }, + } + va := atomicValue{} + va.Store(1) + + vbc := atomicValue{} + vbc.Store("3") + + vbde := atomicValue{} + vbde.Store(true) + + tests := []struct { + path string + want atomicValue + }{ + { + path: "a", + want: va, + }, + { + path: "b.c", + want: vbc, + }, + { + path: "b.d.e", + want: vbde, + }, + } + for _, tt := range tests { + if got, found := readValue(m, tt.path); !found { + t.Errorf("expect found %v in %v, but not.", tt.path, m) + } else if got.Load() != tt.want.Load() { + t.Errorf("readValue(%v, %v) = %v, want %v", m, tt.path, got, tt.want) + } + } +} diff --git a/config/source.go b/config/source.go new file mode 100644 index 0000000..bd86e4d --- /dev/null +++ b/config/source.go @@ -0,0 +1,20 @@ +package config + +// KeyValue is config key value. +type KeyValue struct { + Key string + Value []byte + Format string +} + +// Source is config source. +type Source interface { + Load() ([]*KeyValue, error) + Watch() (Watcher, error) +} + +// Watcher watches a source for changes. +type Watcher interface { + Next() ([]*KeyValue, error) + Stop() error +} diff --git a/config/value.go b/config/value.go new file mode 100644 index 0000000..972441b --- /dev/null +++ b/config/value.go @@ -0,0 +1,192 @@ +package config + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "sync/atomic" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +var ( + _ Value = (*atomicValue)(nil) + _ Value = (*errValue)(nil) +) + +// Value is config value interface. +type Value interface { + Bool() (bool, error) + Int() (int64, error) + Float() (float64, error) + String() (string, error) + Duration() (time.Duration, error) + Slice() ([]Value, error) + Map() (map[string]Value, error) + Scan(any) error + Load() any + Store(any) +} + +type atomicValue struct { + atomic.Value +} + +func (v *atomicValue) typeAssertError() error { + return fmt.Errorf("type assert to %v failed", reflect.TypeOf(v.Load())) +} + +func (v *atomicValue) Bool() (bool, error) { + switch val := v.Load().(type) { + case bool: + return val, nil + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + return strconv.ParseBool(fmt.Sprint(val)) + case string: + return strconv.ParseBool(val) + } + return false, v.typeAssertError() +} + +func (v *atomicValue) Int() (int64, error) { + switch val := v.Load().(type) { + case int: + return int64(val), nil + case int8: + return int64(val), nil + case int16: + return int64(val), nil + case int32: + return int64(val), nil + case int64: + return val, nil + case uint: + return int64(val), nil + case uint8: + return int64(val), nil + case uint16: + return int64(val), nil + case uint32: + return int64(val), nil + case uint64: + return int64(val), nil + case float32: + return int64(val), nil + case float64: + return int64(val), nil + case string: + return strconv.ParseInt(val, 10, 64) + } + return 0, v.typeAssertError() +} + +func (v *atomicValue) Slice() ([]Value, error) { + vals, ok := v.Load().([]any) + if !ok { + return nil, v.typeAssertError() + } + slices := make([]Value, 0, len(vals)) + for _, val := range vals { + a := new(atomicValue) + a.Store(val) + slices = append(slices, a) + } + return slices, nil +} + +func (v *atomicValue) Map() (map[string]Value, error) { + vals, ok := v.Load().(map[string]any) + if !ok { + return nil, v.typeAssertError() + } + m := make(map[string]Value, len(vals)) + for key, val := range vals { + a := new(atomicValue) + a.Store(val) + m[key] = a + } + return m, nil +} + +func (v *atomicValue) Float() (float64, error) { + switch val := v.Load().(type) { + case int: + return float64(val), nil + case int8: + return float64(val), nil + case int16: + return float64(val), nil + case int32: + return float64(val), nil + case int64: + return float64(val), nil + case uint: + return float64(val), nil + case uint8: + return float64(val), nil + case uint16: + return float64(val), nil + case uint32: + return float64(val), nil + case uint64: + return float64(val), nil + case float32: + return float64(val), nil + case float64: + return val, nil + case string: + return strconv.ParseFloat(val, 64) + } + return 0.0, v.typeAssertError() +} + +func (v *atomicValue) String() (string, error) { + switch val := v.Load().(type) { + case string: + return val, nil + case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + return fmt.Sprint(val), nil + case []byte: + return string(val), nil + case fmt.Stringer: + return val.String(), nil + } + return "", v.typeAssertError() +} + +func (v *atomicValue) Duration() (time.Duration, error) { + val, err := v.Int() + if err != nil { + return 0, err + } + return time.Duration(val), nil +} + +func (v *atomicValue) Scan(obj any) error { + data, err := json.Marshal(v.Load()) + if err != nil { + return err + } + if pb, ok := obj.(proto.Message); ok { + return protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(data, pb) + } + return json.Unmarshal(data, obj) +} + +type errValue struct { + err error +} + +func (v errValue) Bool() (bool, error) { return false, v.err } +func (v errValue) Int() (int64, error) { return 0, v.err } +func (v errValue) Float() (float64, error) { return 0.0, v.err } +func (v errValue) Duration() (time.Duration, error) { return 0, v.err } +func (v errValue) String() (string, error) { return "", v.err } +func (v errValue) Scan(any) error { return v.err } +func (v errValue) Load() any { return nil } +func (v errValue) Store(any) {} +func (v errValue) Slice() ([]Value, error) { return nil, v.err } +func (v errValue) Map() (map[string]Value, error) { return nil, v.err } diff --git a/config/value_test.go b/config/value_test.go new file mode 100644 index 0000000..cb1fcc7 --- /dev/null +++ b/config/value_test.go @@ -0,0 +1,224 @@ +package config + +import ( + "fmt" + "testing" + "time" +) + +func TestAtomicValue_Bool(t *testing.T) { + vlist := []any{"1", "t", "T", "true", "TRUE", "True", true, 1, int32(1)} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + b, err := v.Bool() + if err != nil { + t.Fatal(err) + } + if !b { + t.Fatal("b is not equal to true") + } + } + + vlist = []any{"0", "f", "F", "false", "FALSE", "False", false, 0, int32(0)} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + b, err := v.Bool() + if err != nil { + t.Fatal(err) + } + if b { + t.Fatal("b is not equal to false") + } + } + + vlist = []any{"bbb", "-1"} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + _, err := v.Bool() + if err == nil { + t.Fatal("err is nil") + } + } +} + +func TestAtomicValue_Int(t *testing.T) { + vlist := []any{"123123", float64(123123), int64(123123), int32(123123), 123123} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + b, err := v.Int() + if err != nil { + t.Fatal(err) + } + if b != 123123 { + t.Fatal("b is not equal to 123123") + } + } + + vlist = []any{"bbb", "-x1", true} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + _, err := v.Int() + if err == nil { + t.Fatal("err is nil") + } + } +} + +func TestAtomicValue_Float(t *testing.T) { + vlist := []any{"123123.1", 123123.1} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + b, err := v.Float() + if err != nil { + t.Fatal(err) + } + if b != 123123.1 { + t.Fatal("b is not equal to 123123.1") + } + } + + vlist = []any{"bbb", "-x1"} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + _, err := v.Float() + if err == nil { + t.Fatal("err is nil") + } + } +} + +type ts struct { + Name string + Age int +} + +func (t ts) String() string { + return fmt.Sprintf("%s%d", t.Name, t.Age) +} + +func TestAtomicValue_String(t *testing.T) { + vlist := []any{"1", float64(1), int64(1), 1, int64(1)} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + b, err := v.String() + if err != nil { + t.Fatal(err) + } + if b != "1" { + t.Fatal("b is not equal to 1") + } + } + + v := atomicValue{} + v.Store(true) + b, err := v.String() + if err != nil { + t.Fatal(err) + } + if b != "true" { + t.Fatal(`b is not equal to "true"`) + } + + v = atomicValue{} + v.Store(ts{ + Name: "test", + Age: 10, + }) + b, err = v.String() + if err != nil { + t.Fatal(err) + } + if b != "test10" { + t.Fatal(`b is not equal to "test10"`) + } +} + +func TestAtomicValue_Duration(t *testing.T) { + vlist := []any{int64(5)} + for _, x := range vlist { + v := atomicValue{} + v.Store(x) + b, err := v.Duration() + if err != nil { + t.Fatal(err) + } + if b != time.Duration(5) { + t.Fatal("b is not equal to time.Duration(5)") + } + } +} + +func TestAtomicValue_Slice(t *testing.T) { + vlist := []any{int64(5)} + v := atomicValue{} + v.Store(vlist) + slices, err := v.Slice() + if err != nil { + t.Fatal(err) + } + for _, v := range slices { + b, err := v.Duration() + if err != nil { + t.Fatal(err) + } + if b != time.Duration(5) { + t.Fatal("b is not equal to time.Duration(5)") + } + } +} + +func TestAtomicValue_Map(t *testing.T) { + vlist := make(map[string]any) + vlist["5"] = int64(5) + vlist["text"] = "text" + v := atomicValue{} + v.Store(vlist) + m, err := v.Map() + if err != nil { + t.Fatal(err) + } + for k, v := range m { + if k == "5" { + b, err := v.Duration() + if err != nil { + t.Fatal(err) + } + if b != time.Duration(5) { + t.Fatal("b is not equal to time.Duration(5)") + } + } else { + b, err := v.String() + if err != nil { + t.Fatal(err) + } + if b != "text" { + t.Fatal(`b is not equal to "text"`) + } + } + } +} + +func TestAtomicValue_Scan(t *testing.T) { + v := atomicValue{} + err := v.Scan(&struct { + A string `json:"a"` + }{"a"}) + if err != nil { + t.Fatal(err) + } + + err = v.Scan(&struct { + A string `json:"a"` + }{"a"}) + if err != nil { + t.Fatal(err) + } +} diff --git a/contrib/config/apollo/README.md b/contrib/config/apollo/README.md new file mode 100644 index 0000000..9007950 --- /dev/null +++ b/contrib/config/apollo/README.md @@ -0,0 +1,100 @@ +## Apollo config center + +This module implements the `config.Source` interface in kratos based apollo config management center. + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/go-kratos/kratos/contrib/config/apollo/v3) + +### Quick start + +```go +import ( + "fmt" + "log" + + "github.com/go-kratos/kratos/contrib/config/apollo/v3" + "github.com/go-kratos/kratos/v3/config" +) + +func main() { + c := config.New( + config.WithSource( + apollo.NewSource( + apollo.WithAppID("kratos"), + apollo.WithCluster("dev"), + apollo.WithEndpoint("http://localhost:8080"), + apollo.WithNamespace("application,event.yaml,demo.json"), + apollo.WithEnableBackup(), + apollo.WithSecret("ad75b33c77ae4b9c9626d969c44f41ee"), + ), + ), + ) + var bc bootstrap + if err := c.Load(); err != nil { + panic(err) + } + + // use value and watch operations,help yourself. +} +``` + +### Options list + +> You get what you see. + +```go +// specify the app id +func WithAppID(appID string) Option +// specify the cluster of application +func WithCluster(cluster string) Option + +// enable backup or not, and where to back up them. +func WithBackupPath(backupPath string) Option +func WithDisableBackup() Option +func WithEnableBackup() Option + +// specify apollo endpoint, such as http://localhost:8080 +func WithEndpoint(endpoint string) Option + +// namespaces to load, comma to separate. +func WithNamespace(name string) Option + +// secret is the apollo secret key to access application config. +func WithSecret(secret string) Option +``` + +### Notice + +apollo config center use `Namespace` to be part of the key. For example: + +***application.json*** + +```json +{ + "http": { + "address": ":8080", + "tls": { + "enable": false, + "cert_file": "", + "key_file": "" + } + } +} +``` + +you got them in kratos config instance maybe look like: + +```go +config := map[string]interface{}{ + // application be part of the key path. + "application": map[string]interface{}{ + "http": map[string]interface{}{ + "address": ":8080", + "tls": map[string]interface{}{ + "enable": false, + "cert_file": "", + "key_file": "", + }, + }, + }, +} +``` diff --git a/contrib/config/apollo/apollo.go b/contrib/config/apollo/apollo.go new file mode 100644 index 0000000..b8993eb --- /dev/null +++ b/contrib/config/apollo/apollo.go @@ -0,0 +1,286 @@ +package apollo + +import ( + "strings" + + "github.com/apolloconfig/agollo/v4" + "github.com/apolloconfig/agollo/v4/constant" + apolloconfig "github.com/apolloconfig/agollo/v4/env/config" + "github.com/apolloconfig/agollo/v4/extension" + + "github.com/go-kratos/kratos/v3/config" + "github.com/go-kratos/kratos/v3/encoding" + "github.com/go-kratos/kratos/v3/log" +) + +type apollo struct { + client agollo.Client + opt *options +} + +const ( + yaml = "yaml" + yml = "yml" + json = "json" + properties = "properties" +) + +var formats map[string]struct{} + +// Option is apollo option +type Option func(*options) + +type options struct { + appid string + secret string + cluster string + endpoint string + namespace string + isBackupConfig bool + backupPath string + originConfig bool +} + +// WithAppID with apollo config app id +func WithAppID(appID string) Option { + return func(o *options) { + o.appid = appID + } +} + +// WithCluster with apollo config cluster +func WithCluster(cluster string) Option { + return func(o *options) { + o.cluster = cluster + } +} + +// WithEndpoint with apollo config conf server ip +func WithEndpoint(endpoint string) Option { + return func(o *options) { + o.endpoint = endpoint + } +} + +// WithEnableBackup with apollo config enable backup config +func WithEnableBackup() Option { + return func(o *options) { + o.isBackupConfig = true + } +} + +// WithDisableBackup with apollo config enable backup config +func WithDisableBackup() Option { + return func(o *options) { + o.isBackupConfig = false + } +} + +// WithSecret with apollo config app secret +func WithSecret(secret string) Option { + return func(o *options) { + o.secret = secret + } +} + +// WithNamespace with apollo config namespace name +func WithNamespace(name string) Option { + return func(o *options) { + o.namespace = name + } +} + +// WithBackupPath with apollo config backupPath +func WithBackupPath(backupPath string) Option { + return func(o *options) { + o.backupPath = backupPath + } +} + +// WithOriginalConfig use the original configuration file without parse processing +func WithOriginalConfig() Option { + return func(o *options) { + extension.AddFormatParser(constant.JSON, &jsonExtParser{}) + extension.AddFormatParser(constant.YAML, &yamlExtParser{}) + extension.AddFormatParser(constant.YML, &yamlExtParser{}) + o.originConfig = true + } +} + +func NewSource(opts ...Option) config.Source { + op := options{} + for _, o := range opts { + o(&op) + } + client, err := agollo.StartWithConfig(func() (*apolloconfig.AppConfig, error) { + return &apolloconfig.AppConfig{ + AppID: op.appid, + Cluster: op.cluster, + NamespaceName: op.namespace, + IP: op.endpoint, + IsBackupConfig: op.isBackupConfig, + Secret: op.secret, + BackupConfigPath: op.backupPath, + }, nil + }) + if err != nil { + panic(err) + } + return &apollo{client: client, opt: &op} +} + +func format(ns string) string { + arr := strings.Split(ns, ".") + suffix := arr[len(arr)-1] + if len(arr) <= 1 || suffix == properties { + return json + } + if _, ok := formats[suffix]; !ok { + // fallback + return json + } + + return suffix +} + +func (e *apollo) load() []*config.KeyValue { + kvs := make([]*config.KeyValue, 0) + namespaces := strings.Split(e.opt.namespace, ",") + + for _, ns := range namespaces { + if !e.opt.originConfig { + kv, err := e.getConfig(ns) + if err != nil { + log.Error("apollo get config failed", "error", err) + continue + } + kvs = append(kvs, kv) + continue + } + if strings.Contains(ns, ".") && !strings.HasSuffix(ns, "."+properties) && + (format(ns) == yaml || format(ns) == yml || format(ns) == json) { + kv, err := e.getOriginConfig(ns) + if err != nil { + log.Error("apollo get config failed", "error", err) + continue + } + kvs = append(kvs, kv) + continue + } + kv, err := e.getConfig(ns) + if err != nil { + log.Error("apollo get config failed", "error", err) + continue + } + kvs = append(kvs, kv) + } + return kvs +} + +func (e *apollo) getConfig(ns string) (*config.KeyValue, error) { + next := map[string]any{} + e.client.GetConfigCache(ns).Range(func(key, value any) bool { + // all values are out properties format + resolve(genKey(ns, key.(string)), value, next) + return true + }) + f := format(ns) + codec := encoding.GetCodec(f) + val, err := codec.Marshal(next) + if err != nil { + return nil, err + } + return &config.KeyValue{ + Key: ns, + Value: val, + Format: f, + }, nil +} + +func (e apollo) getOriginConfig(ns string) (*config.KeyValue, error) { + value, err := e.client.GetConfigCache(ns).Get(contentKey) + if err != nil { + return nil, err + } + // serialize the namespace content KeyValue into bytes. + return &config.KeyValue{ + Key: ns, + Value: []byte(value.(string)), + Format: format(ns), + }, nil +} + +func (e *apollo) Load() (kv []*config.KeyValue, err error) { + return e.load(), nil +} + +func (e *apollo) Watch() (config.Watcher, error) { + w, err := newWatcher(e) + if err != nil { + return nil, err + } + return w, nil +} + +// resolve convert kv pair into one map[string]interface{} by split key into different +// map level. such as: app.name = "application" => map[app][name] = "application" +func resolve(key string, value any, target map[string]any) { + // expand key "aaa.bbb" into map[aaa]map[bbb]interface{} + keys := strings.Split(key, ".") + last := len(keys) - 1 + cursor := target + + for i, k := range keys { + if i == last { + cursor[k] = value + break + } + + // not the last key, be deeper + v, ok := cursor[k] + if !ok { + // create a new map + deeper := make(map[string]any) + cursor[k] = deeper + cursor = deeper + continue + } + + // current exists, then check existing value type, if it's not map + // that means duplicate keys, and at least one is not map instance. + if cursor, ok = v.(map[string]any); !ok { + log.Warn("duplicate key", "key", strings.Join(keys[:i+1], ".")) + break + } + } +} + +// genKey got the key of config.KeyValue pair. +// eg: namespace.ext with subKey got namespace.subKey +func genKey(ns, sub string) string { + arr := strings.Split(ns, ".") + if len(arr) == 1 { + if ns == "" { + return sub + } + + return ns + "." + sub + } + + suffix := arr[len(arr)-1] + _, ok := formats[suffix] + if ok { + return strings.Join(arr[:len(arr)-1], ".") + "." + sub + } + + return ns + "." + sub +} + +func init() { + formats = make(map[string]struct{}) + + formats[yaml] = struct{}{} + formats[yml] = struct{}{} + formats[json] = struct{}{} + formats[properties] = struct{}{} +} diff --git a/contrib/config/apollo/apollo_test.go b/contrib/config/apollo/apollo_test.go new file mode 100644 index 0000000..185ca4c --- /dev/null +++ b/contrib/config/apollo/apollo_test.go @@ -0,0 +1,90 @@ +package apollo + +import ( + "testing" +) + +func Test_genKey(t *testing.T) { + type args struct { + ns string + sub string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "blank namespace", + args: args{ + ns: "", + sub: "x.y", + }, + want: "x.y", + }, + { + name: "properties namespace", + args: args{ + ns: "application", + sub: "x.y", + }, + want: "application.x.y", + }, + { + name: "namespace with format", + args: args{ + ns: "app.yaml", + sub: "x.y", + }, + want: "app.x.y", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := genKey(tt.args.ns, tt.args.sub); got != tt.want { + t.Errorf("genKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_format(t *testing.T) { + tests := []struct { + name string + namespace string + want string + }{ + { + name: "properties namespace", + namespace: "application", + want: "json", + }, + { + name: "properties namespace #1", + namespace: "app.setting", + want: "json", + }, + { + name: "namespace with format[yaml]", + namespace: "app.yaml", + want: "yaml", + }, + { + name: "namespace with format[yml]", + namespace: "app.yml", + want: "yml", + }, + { + name: "namespace with format[json]", + namespace: "app.json", + want: "json", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := format(tt.namespace); got != tt.want { + t.Errorf("format() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/contrib/config/apollo/go.mod b/contrib/config/apollo/go.mod new file mode 100644 index 0000000..1a3826c --- /dev/null +++ b/contrib/config/apollo/go.mod @@ -0,0 +1,28 @@ +module github.com/go-kratos/kratos/contrib/config/apollo/v3 + +go 1.25.0 + +require ( + github.com/apolloconfig/agollo/v4 v4.4.0 + github.com/go-kratos/kratos/v3 v3.0.0 +) + +require ( + github.com/agiledragon/gomonkey/v2 v2.14.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/config/apollo/go.sum b/contrib/config/apollo/go.sum new file mode 100644 index 0000000..108ce08 --- /dev/null +++ b/contrib/config/apollo/go.sum @@ -0,0 +1,609 @@ +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/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agiledragon/gomonkey/v2 v2.11.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/agiledragon/gomonkey/v2 v2.14.0 h1:FASzes6sjtD0hRo5lu0g796qKL03bOHCgcIA/4am9QM= +github.com/agiledragon/gomonkey/v2 v2.14.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apolloconfig/agollo/v4 v4.4.0 h1:bIIRTEN4f7HgLx97/cNpduEvP9qQ7BkCyDOI2j800VM= +github.com/apolloconfig/agollo/v4 v4.4.0/go.mod h1:6WjI68IzqMk/Y6ghMtrj5AX6Uewo20ZnncvRhTceQqg= +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/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/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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/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/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/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/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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/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/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/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +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/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +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/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tevid/gohamcrest v1.1.1 h1:ou+xSqlIw1xfGTg1uq1nif/htZ2S3EzRqLm2BP+tYU0= +github.com/tevid/gohamcrest v1.1.1/go.mod h1:3UvtWlqm8j5JbwYZh80D/PVBt0mJ1eJiYgZMibh0H/k= +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= +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= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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/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/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/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/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-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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +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/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= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/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/contrib/config/apollo/parser.go b/contrib/config/apollo/parser.go new file mode 100644 index 0000000..a11944f --- /dev/null +++ b/contrib/config/apollo/parser.go @@ -0,0 +1,15 @@ +package apollo + +const contentKey = "content" + +type jsonExtParser struct{} + +func (parser jsonExtParser) Parse(configContent any) (map[string]any, error) { + return map[string]any{contentKey: configContent}, nil +} + +type yamlExtParser struct{} + +func (parser yamlExtParser) Parse(configContent any) (map[string]any, error) { + return map[string]any{contentKey: configContent}, nil +} diff --git a/contrib/config/apollo/watcher.go b/contrib/config/apollo/watcher.go new file mode 100644 index 0000000..d4b7864 --- /dev/null +++ b/contrib/config/apollo/watcher.go @@ -0,0 +1,108 @@ +package apollo + +import ( + "context" + "strings" + + "github.com/apolloconfig/agollo/v4/storage" + + "github.com/go-kratos/kratos/v3/config" + "github.com/go-kratos/kratos/v3/encoding" + "github.com/go-kratos/kratos/v3/log" +) + +type watcher struct { + out <-chan []*config.KeyValue + + ctx context.Context + cancelFn func() +} + +type customChangeListener struct { + in chan<- []*config.KeyValue + apollo *apollo +} + +func (c *customChangeListener) onChange(namespace string, changes map[string]*storage.ConfigChange) []*config.KeyValue { + kv := make([]*config.KeyValue, 0, 2) + if strings.Contains(namespace, ".") && !strings.HasSuffix(namespace, "."+properties) && + (format(namespace) == yaml || format(namespace) == yml || format(namespace) == json) { + if value, ok := changes[contentKey]; ok { + if s, ok := value.NewValue.(string); ok { + kv = append(kv, &config.KeyValue{ + Key: namespace, + Value: []byte(s), + Format: format(namespace), + }) + + return kv + } + } + } + + next := make(map[string]any) + + for key, change := range changes { + resolve(genKey(namespace, key), change.NewValue, next) + } + + f := format(namespace) + codec := encoding.GetCodec(f) + val, err := codec.Marshal(next) + if err != nil { + log.Warn("apollo could not handle namespace", "namespace", namespace, "error", err) + return nil + } + kv = append(kv, &config.KeyValue{ + Key: namespace, + Value: val, + Format: f, + }) + + return kv +} + +func (c *customChangeListener) OnChange(changeEvent *storage.ChangeEvent) { + change := c.onChange(changeEvent.Namespace, changeEvent.Changes) + if len(change) == 0 { + return + } + + c.in <- change +} + +func (c *customChangeListener) OnNewestChange(_ *storage.FullChangeEvent) {} + +func newWatcher(a *apollo) (config.Watcher, error) { + changeCh := make(chan []*config.KeyValue) + listener := &customChangeListener{in: changeCh, apollo: a} + a.client.AddChangeListener(listener) + + ctx, cancel := context.WithCancel(context.Background()) + return &watcher{ + out: changeCh, + + ctx: ctx, + cancelFn: func() { + a.client.RemoveChangeListener(listener) + cancel() + }, + }, nil +} + +// Next will be blocked until the Stop method is called +func (w *watcher) Next() ([]*config.KeyValue, error) { + select { + case kv := <-w.out: + return kv, nil + case <-w.ctx.Done(): + return nil, w.ctx.Err() + } +} + +func (w *watcher) Stop() error { + if w.cancelFn != nil { + w.cancelFn() + } + return nil +} diff --git a/contrib/config/apollo/watcher_test.go b/contrib/config/apollo/watcher_test.go new file mode 100644 index 0000000..a178760 --- /dev/null +++ b/contrib/config/apollo/watcher_test.go @@ -0,0 +1,146 @@ +package apollo + +import ( + "testing" + + "github.com/apolloconfig/agollo/v4/storage" + + "github.com/go-kratos/kratos/v3/config" + "github.com/go-kratos/kratos/v3/encoding" +) + +func Test_onChange(t *testing.T) { + s := map[string]struct { + Name string `yaml:"name"` + }{ + "app": { + Name: "new", + }, + } + codec := encoding.GetCodec(yaml) + val, _ := codec.Marshal(s) + c := customChangeListener{} + tests := []struct { + name string + namespace string + changes map[string]*storage.ConfigChange + kvs []*config.KeyValue + }{ + { + "test yaml onChange", + "app.yaml", + map[string]*storage.ConfigChange{ + "name": { + OldValue: "old", + NewValue: "new", + ChangeType: storage.MODIFIED, + }, + }, + []*config.KeyValue{ + { + Key: "app.yaml", + Value: val, + Format: yaml, + }, + }, + }, + { + "test json onChange", + "app.json", + map[string]*storage.ConfigChange{ + contentKey: { + OldValue: `{"name":"old"}`, + NewValue: `{"name":"new"}`, + ChangeType: storage.MODIFIED, + }, + }, + []*config.KeyValue{ + { + Key: "app.json", + Value: []byte(`{"name":"new"}`), + Format: json, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kvs := c.onChange(tt.namespace, tt.changes) + if len(kvs) != len(tt.kvs) { + t.Errorf("len(kvs) = %v, want %v", len(kvs), len(tt.kvs)) + } + for i := range kvs { + if kvs[i].Format != tt.kvs[i].Format || kvs[i].Key != tt.kvs[i].Key || string(kvs[i].Value) != string(tt.kvs[i].Value) { + t.Errorf("got %v, want %v", kvs[i], tt.kvs[i]) + } + } + }) + } +} + +func Test_onChange_deletedContent(t *testing.T) { + c := customChangeListener{} + + t.Run("json content deleted should not panic", func(t *testing.T) { + changes := map[string]*storage.ConfigChange{ + contentKey: { + OldValue: `{"name":"old"}`, + NewValue: nil, + ChangeType: storage.DELETED, + }, + } + kvs := c.onChange("app.json", changes) + // NewValue is nil, so the original config path is skipped; + // falls through to resolve path which also skips nil NewValue. + if len(kvs) != 1 { + t.Fatalf("expected 1 kv, got %d", len(kvs)) + } + }) + + t.Run("yaml content deleted should not panic", func(t *testing.T) { + changes := map[string]*storage.ConfigChange{ + contentKey: { + OldValue: "name: old", + NewValue: nil, + ChangeType: storage.DELETED, + }, + } + kvs := c.onChange("app.yaml", changes) + if len(kvs) != 1 { + t.Fatalf("expected 1 kv, got %d", len(kvs)) + } + }) + + t.Run("properties key deleted should not panic", func(t *testing.T) { + changes := map[string]*storage.ConfigChange{ + "name": { + OldValue: "old", + NewValue: nil, + ChangeType: storage.DELETED, + }, + } + kvs := c.onChange("app", changes) + if len(kvs) != 1 { + t.Fatalf("expected 1 kv, got %d", len(kvs)) + } + }) +} + +func Test_onChange_nonStringNewValue(t *testing.T) { + c := customChangeListener{} + + t.Run("json content with non-string NewValue should not panic", func(t *testing.T) { + changes := map[string]*storage.ConfigChange{ + contentKey: { + OldValue: `{"name":"old"}`, + NewValue: 12345, + ChangeType: storage.MODIFIED, + }, + } + // Should not panic; falls through to resolve path + kvs := c.onChange("app.json", changes) + if kvs == nil { + t.Fatal("expected non-nil kvs") + } + }) +} diff --git a/contrib/config/consul/README.md b/contrib/config/consul/README.md new file mode 100644 index 0000000..0820667 --- /dev/null +++ b/contrib/config/consul/README.md @@ -0,0 +1,25 @@ +# Consul Config + +```go +import ( + "github.com/hashicorp/consul/api" + + "github.com/go-kratos/kratos/contrib/config/consul/v3" +) + +func main() { + consulClient, err := api.NewClient(&api.Config{ + Address: "127.0.0.1:8500", + }) + if err != nil { + panic(err) + } + cs, err := consul.New(consulClient, consul.WithPath("app/cart/configs/")) + // consul中需要标注文件后缀,kratos读取配置需要适配文件后缀 + // The file suffix needs to be marked, and kratos needs to adapt the file suffix to read the configuration. + if err != nil { + panic(err) + } + c := config.New(config.WithSource(cs)) +} +``` diff --git a/contrib/config/consul/config.go b/contrib/config/consul/config.go new file mode 100644 index 0000000..e5556f8 --- /dev/null +++ b/contrib/config/consul/config.go @@ -0,0 +1,90 @@ +package consul + +import ( + "context" + "errors" + "path/filepath" + "strings" + + "github.com/hashicorp/consul/api" + + "github.com/go-kratos/kratos/v3/config" +) + +// Option is consul config option. +type Option func(o *options) + +type options struct { + ctx context.Context + path string +} + +// WithContext with registry context. +func WithContext(ctx context.Context) Option { + return func(o *options) { + o.ctx = ctx + } +} + +// WithPath is config path +func WithPath(p string) Option { + return func(o *options) { + o.path = p + } +} + +type source struct { + client *api.Client + options *options +} + +func New(client *api.Client, opts ...Option) (config.Source, error) { + options := &options{ + ctx: context.Background(), + path: "", + } + + for _, opt := range opts { + opt(options) + } + + if options.path == "" { + return nil, errors.New("path invalid") + } + + return &source{ + client: client, + options: options, + }, nil +} + +// Load return the config values +func (s *source) Load() ([]*config.KeyValue, error) { + kv, _, err := s.client.KV().List(s.options.path, nil) + if err != nil { + return nil, err + } + + pathPrefix := s.options.path + if !strings.HasSuffix(s.options.path, "/") { + pathPrefix = pathPrefix + "/" + } + kvs := make([]*config.KeyValue, 0) + for _, item := range kv { + k := strings.TrimPrefix(item.Key, pathPrefix) + if k == "" { + continue + } + kvs = append(kvs, &config.KeyValue{ + Key: k, + Value: item.Value, + Format: strings.TrimPrefix(filepath.Ext(k), "."), + }) + } + return kvs, nil +} + +// Watch return the watcher +func (s *source) Watch() (config.Watcher, error) { + return newWatcher(s) +} diff --git a/contrib/config/consul/config_test.go b/contrib/config/consul/config_test.go new file mode 100644 index 0000000..0ea9f2e --- /dev/null +++ b/contrib/config/consul/config_test.go @@ -0,0 +1,259 @@ +package consul + +import ( + "reflect" + "testing" + "time" + + "github.com/hashicorp/consul/api" + + "github.com/go-kratos/kratos/v3/config" +) + +const testPath = "kratos/test/config" + +const testKey = "kratos/test/config/key" + +func TestConfig(t *testing.T) { + client, err := api.NewClient(&api.Config{ + Address: "127.0.0.1:8500", + }) + if err != nil { + t.Fatal(err) + } + + if _, err = client.KV().Put(&api.KVPair{Key: testKey, Value: []byte("test config")}, nil); err != nil { + t.Fatal(err) + } + + source, err := New(client, WithPath(testPath)) + if err != nil { + t.Fatal(err) + } + + kvs, err := source.Load() + if err != nil { + t.Fatal(err) + } + + if len(kvs) != 1 || kvs[0].Key != "key" || string(kvs[0].Value) != "test config" { + t.Fatal("config error") + } + + w, err := source.Watch() + if err != nil { + t.Fatal(err) + } + defer func() { + _ = w.Stop() + }() + + if _, err = client.KV().Put(&api.KVPair{Key: testKey, Value: []byte("new config")}, nil); err != nil { + t.Error(err) + } + + if kvs, err = w.Next(); err != nil { + t.Fatal(err) + } + + if len(kvs) != 1 || kvs[0].Key != "key" || string(kvs[0].Value) != "new config" { + t.Fatal("config error") + } + + if _, err := client.KV().Delete(testKey, nil); err != nil { + t.Error(err) + } +} + +func TestExtToFormat(t *testing.T) { + client, err := api.NewClient(&api.Config{ + Address: "127.0.0.1:8500", + }) + if err != nil { + t.Fatal(err) + } + tp := "kratos/test/ext" + tn := "a.bird.json" + tk := tp + "/" + tn + tc := `{"a":1}` + if _, err = client.KV().Put(&api.KVPair{Key: tk, Value: []byte(tc)}, nil); err != nil { + t.Fatal(err) + } + + source, err := New(client, WithPath(tp)) + if err != nil { + t.Fatal(err) + } + + kvs, err := source.Load() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(len(kvs), 1) { + t.Errorf("len(kvs) is %d", len(kvs)) + } + if !reflect.DeepEqual(tn, kvs[0].Key) { + t.Errorf("kvs[0].Key is %s", kvs[0].Key) + } + if !reflect.DeepEqual(tc, string(kvs[0].Value)) { + t.Errorf("kvs[0].Value is %s", kvs[0].Value) + } + if !reflect.DeepEqual("json", kvs[0].Format) { + t.Errorf("kvs[0].Format is %s", kvs[0].Format) + } +} + +func Test_source_Watch(t *testing.T) { + client, err := api.NewClient(&api.Config{ + Address: "127.0.0.1:8500", + }) + if err != nil { + t.Fatal(err) + } + + source, err := New(client, WithPath(testPath)) + if err != nil { + t.Fatal(err) + } + + type fields struct { + source config.Source + } + + type args struct { + key string + value string + } + + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + deferFunc func(t *testing.T) + }{ + { + name: "normal", + fields: fields{source: source}, + args: args{ + key: testKey, + value: "test value", + }, + want: "test value", + wantErr: false, + deferFunc: func(t *testing.T) { + _, err := client.KV().Delete(testKey, nil) + if err != nil { + t.Error(err) + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.deferFunc != nil { + defer tt.deferFunc(t) + } + + got, err := tt.fields.source.Watch() + if (err != nil) != tt.wantErr { + t.Errorf("Watch() error = %v, wantErr %v", err, tt.wantErr) + return + } + + time.Sleep(100 * time.Millisecond) + _, err = client.KV().Put(&api.KVPair{Key: tt.args.key, Value: []byte(tt.args.value)}, nil) + if err != nil { + t.Error(err) + } + + next, err := got.Next() + if (err != nil) != tt.wantErr { + t.Errorf("Watch() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if len(next) != 1 { + t.Error("watch is error") + } + + if !reflect.DeepEqual(string(next[0].Value), tt.want) { + t.Errorf("Watch got = %v, want %v", string(next[0].Value), tt.want) + } + }) + } +} + +func Test_source_Load(t *testing.T) { + client, err := api.NewClient(&api.Config{ + Address: "127.0.0.1:8500", + }) + if err != nil { + t.Fatal(err) + } + + source, err := New(client, WithPath(testPath)) + if err != nil { + t.Fatal(err) + } + + type args struct { + key string + value string + } + type fields struct { + source config.Source + } + tests := []struct { + name string + args args + fields fields + want []*config.KeyValue + wantErr bool + deferFunc func(t *testing.T) + }{ + { + name: "normal", + args: args{ + key: testKey, + value: "test value", + }, + fields: fields{ + source: source, + }, + want: []*config.KeyValue{ + { + Key: "key", + Value: []byte("test value"), + }, + }, + deferFunc: func(t *testing.T) { + _, err1 := client.KV().Delete(testKey, nil) + if err1 != nil { + t.Error(err) + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.deferFunc != nil { + defer tt.deferFunc(t) + } + _, err = client.KV().Put(&api.KVPair{Key: tt.args.key, Value: []byte(tt.args.value)}, nil) + if err != nil { + t.Error(err) + } + got, err := tt.fields.source.Load() + if (err != nil) != tt.wantErr { + t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !reflect.DeepEqual(got[0], tt.want[0]) { + t.Errorf("Load() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/contrib/config/consul/go.mod b/contrib/config/consul/go.mod new file mode 100644 index 0000000..ede7fcb --- /dev/null +++ b/contrib/config/consul/go.mod @@ -0,0 +1,32 @@ +module github.com/go-kratos/kratos/contrib/config/consul/v3 + +go 1.26 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/hashicorp/consul/api v1.34.2 +) + +require ( + github.com/armon/go-metrics v0.4.1 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/serf v0.10.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/sys v0.44.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/config/consul/go.sum b/contrib/config/consul/go.sum new file mode 100644 index 0000000..d370aa6 --- /dev/null +++ b/contrib/config/consul/go.sum @@ -0,0 +1,240 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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.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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/consul/api v1.34.2 h1:B5jqSSKwWyY8U8WiGS5vmPEPkkF0bAvrECykdZkDR80= +github.com/hashicorp/consul/api v1.34.2/go.mod h1:+gAdHQa2zvgYX3ZfcgITtnYCSj6AgS/cgotvCKaE+b8= +github.com/hashicorp/consul/sdk v0.18.1 h1:RDTeBvAeOveI2xI86sV+8WkaN7OkP4zz+cG3fOobDCM= +github.com/hashicorp/consul/sdk v0.18.1/go.mod h1:XdP2tEJmAvlK4jgoKTTtohGkRJlS4mU44mv9/sjU21s= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0= +github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU= +github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI= +github.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4= +github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= +github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +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/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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-20190911185100-cd5d95a43a6e/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +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.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/config/consul/watcher.go b/contrib/config/consul/watcher.go new file mode 100644 index 0000000..9407cf5 --- /dev/null +++ b/contrib/config/consul/watcher.go @@ -0,0 +1,103 @@ +package consul + +import ( + "context" + "path/filepath" + "strings" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/api/watch" + + "github.com/go-kratos/kratos/v3/config" +) + +type watcher struct { + source *source + ch chan []*config.KeyValue + wp *watch.Plan + fileModifyIndex map[string]uint64 + ctx context.Context + cancel context.CancelFunc +} + +func (w *watcher) handle(_ uint64, data any) { + if data == nil { + return + } + + kv, ok := data.(api.KVPairs) + if !ok { + return + } + + pathPrefix := w.source.options.path + if !strings.HasSuffix(w.source.options.path, "/") { + pathPrefix = pathPrefix + "/" + } + kvs := make([]*config.KeyValue, 0, len(kv)) + for _, item := range kv { + if index, ok := w.fileModifyIndex[item.Key]; ok && item.ModifyIndex == index { + continue + } + k := strings.TrimPrefix(item.Key, pathPrefix) + if k == "" { + continue + } + kvs = append(kvs, &config.KeyValue{ + Key: k, + Value: item.Value, + Format: strings.TrimPrefix(filepath.Ext(k), "."), + }) + w.fileModifyIndex[item.Key] = item.ModifyIndex + } + + if len(kvs) == 0 { + return + } + + w.ch <- kvs +} + +func newWatcher(s *source) (*watcher, error) { + ctx, cancel := context.WithCancel(context.Background()) + w := &watcher{ + source: s, + ch: make(chan []*config.KeyValue), + fileModifyIndex: make(map[string]uint64), + ctx: ctx, + cancel: cancel, + } + + wp, err := watch.Parse(map[string]any{"type": "keyprefix", "prefix": s.options.path}) + if err != nil { + return nil, err + } + + wp.Handler = w.handle + w.wp = wp + + // wp.Run is a blocking call and will prevent newWatcher from returning + go func() { + err := wp.RunWithClientAndHclog(s.client, nil) + if err != nil { + panic(err) + } + }() + + return w, nil +} + +func (w *watcher) Next() ([]*config.KeyValue, error) { + select { + case kv := <-w.ch: + return kv, nil + case <-w.ctx.Done(): + return nil, w.ctx.Err() + } +} + +func (w *watcher) Stop() error { + w.wp.Stop() + w.cancel() + return nil +} diff --git a/contrib/config/etcd/README.md b/contrib/config/etcd/README.md new file mode 100644 index 0000000..feeb4e9 --- /dev/null +++ b/contrib/config/etcd/README.md @@ -0,0 +1,44 @@ +# Etcd Config + +```go +import ( + "log" + + clientv3 "go.etcd.io/etcd/client/v3" + + cfg "github.com/go-kratos/kratos/contrib/config/etcd/v3" + "github.com/go-kratos/kratos/v3/config" +) + +// create an etcd client +client, err := clientv3.New(clientv3.Config{ + Endpoints: []string{"127.0.0.1:2379"}, + DialTimeout: time.Second, +}) +if err != nil { + log.Fatal(err) +} + +// configure the source, "path" is required +source, err := cfg.New(client, cfg.WithPath("/app-config"), cfg.WithPrefix(true)) +if err != nil { + log.Fatalln(err) +} + +// create a config instance with source +c := config.New(config.WithSource(source)) +defer c.Close() + +// load sources before get +if err := c.Load(); err != nil { + log.Fatalln(err) +} + +// acquire config value +foo, err := c.Value("/app-config").String() +if err != nil { + log.Fatalln(err) +} + +log.Println(foo) +``` diff --git a/contrib/config/etcd/config.go b/contrib/config/etcd/config.go new file mode 100644 index 0000000..2e68ded --- /dev/null +++ b/contrib/config/etcd/config.go @@ -0,0 +1,96 @@ +package etcd + +import ( + "context" + "errors" + "path/filepath" + "strings" + + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/go-kratos/kratos/v3/config" +) + +// Option is etcd config option. +type Option func(o *options) + +type options struct { + ctx context.Context + path string + prefix bool +} + +// WithContext with registry context. +func WithContext(ctx context.Context) Option { + return func(o *options) { + o.ctx = ctx + } +} + +// WithPath is config path +func WithPath(p string) Option { + return func(o *options) { + o.path = p + } +} + +// WithPrefix is config prefix +func WithPrefix(prefix bool) Option { + return func(o *options) { + o.prefix = prefix + } +} + +type source struct { + client *clientv3.Client + options *options +} + +func New(client *clientv3.Client, opts ...Option) (config.Source, error) { + options := &options{ + ctx: context.Background(), + path: "", + prefix: false, + } + + for _, opt := range opts { + opt(options) + } + + if options.path == "" { + return nil, errors.New("path invalid") + } + + return &source{ + client: client, + options: options, + }, nil +} + +// Load return the config values +func (s *source) Load() ([]*config.KeyValue, error) { + var opts []clientv3.OpOption + if s.options.prefix { + opts = append(opts, clientv3.WithPrefix()) + } + + rsp, err := s.client.Get(s.options.ctx, s.options.path, opts...) + if err != nil { + return nil, err + } + kvs := make([]*config.KeyValue, 0, len(rsp.Kvs)) + for _, item := range rsp.Kvs { + k := string(item.Key) + kvs = append(kvs, &config.KeyValue{ + Key: k, + Value: item.Value, + Format: strings.TrimPrefix(filepath.Ext(k), "."), + }) + } + return kvs, nil +} + +// Watch return the watcher +func (s *source) Watch() (config.Watcher, error) { + return newWatcher(s), nil +} diff --git a/contrib/config/etcd/config_test.go b/contrib/config/etcd/config_test.go new file mode 100644 index 0000000..c70465e --- /dev/null +++ b/contrib/config/etcd/config_test.go @@ -0,0 +1,167 @@ +package etcd + +import ( + "context" + "reflect" + "testing" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +const testKey = "/kratos/test/config" + +func TestConfig(t *testing.T) { + client, err := clientv3.New(clientv3.Config{ + Endpoints: []string{"127.0.0.1:2379"}, + DialTimeout: time.Second, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = client.Close() + }() + if _, err = client.Put(context.Background(), testKey, "test config"); err != nil { + t.Fatal(err) + } + + source, err := New(client, WithPath(testKey)) + if err != nil { + t.Fatal(err) + } + + kvs, err := source.Load() + if err != nil { + t.Fatal(err) + } + + if len(kvs) != 1 || kvs[0].Key != testKey || string(kvs[0].Value) != "test config" { + t.Fatal("config error") + } + + w, err := source.Watch() + if err != nil { + t.Fatal(err) + } + defer func() { + _ = w.Stop() + }() + + if _, err = client.Put(context.Background(), testKey, "new config"); err != nil { + t.Error(err) + } + + if kvs, err = w.Next(); err != nil { + t.Fatal(err) + } + + if len(kvs) != 1 || kvs[0].Key != testKey || string(kvs[0].Value) != "new config" { + t.Fatal("config error") + } + + if _, err := client.Delete(context.Background(), testKey); err != nil { + t.Error(err) + } +} + +func TestExtToFormat(t *testing.T) { + client, err := clientv3.New(clientv3.Config{ + Endpoints: []string{"127.0.0.1:2379"}, + DialTimeout: time.Second, + }) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = client.Close() + }() + + tp := "/kratos/test/ext" + tn := "a.bird.json" + tk := tp + "/" + tn + tc := `{"a":1}` + if _, err = client.Put(context.Background(), tk, tc); err != nil { + t.Fatal(err) + } + + source, err := New(client, WithPath(tp), WithPrefix(true)) + if err != nil { + t.Fatal(err) + } + + kvs, err := source.Load() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(len(kvs), 1) { + t.Errorf("len(kvs) = %d", len(kvs)) + } + if !reflect.DeepEqual(tk, kvs[0].Key) { + t.Errorf("kvs[0].Key is %s", kvs[0].Key) + } + if !reflect.DeepEqual(tc, string(kvs[0].Value)) { + t.Errorf("kvs[0].Value is %s", kvs[0].Value) + } + if !reflect.DeepEqual("json", kvs[0].Format) { + t.Errorf("kvs[0].Format is %s", kvs[0].Format) + } +} + +func TestEtcdWithPath(t *testing.T) { + tests := []struct { + name string + fields string + want string + }{ + { + name: "default", + fields: testKey, + want: testKey, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := &options{ + ctx: context.Background(), + } + + got := WithPath(tt.fields) + got(options) + + if options.path != tt.want { + t.Errorf("WithPath(tt.fields) = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEtcdWithPrefix(t *testing.T) { + tests := []struct { + name string + fields bool + want bool + }{ + { + name: "default", + fields: false, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := &options{ + ctx: context.Background(), + } + + got := WithPrefix(tt.fields) + got(options) + + if options.prefix != tt.want { + t.Errorf("WithPrefix(tt.fields) = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/contrib/config/etcd/go.mod b/contrib/config/etcd/go.mod new file mode 100644 index 0000000..f8dac41 --- /dev/null +++ b/contrib/config/etcd/go.mod @@ -0,0 +1,30 @@ +module github.com/go-kratos/kratos/contrib/config/etcd/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + go.etcd.io/etcd/client/v3 v3.6.11 + google.golang.org/grpc v1.81.0 +) + +require ( + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + go.etcd.io/etcd/api/v3 v3.6.11 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.11 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/config/etcd/go.sum b/contrib/config/etcd/go.sum new file mode 100644 index 0000000..920290c --- /dev/null +++ b/contrib/config/etcd/go.sum @@ -0,0 +1,108 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +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/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.6.11 h1:XFGTgrJ8nak3kB4NgMG8t7NT+lEeuuvKQAqUHKVgkWQ= +go.etcd.io/etcd/api/v3 v3.6.11/go.mod h1:HYfTh0jyh+uFgp6gMbxJteIDYY97yMuYz85Rnw6Gy9o= +go.etcd.io/etcd/client/pkg/v3 v3.6.11 h1:e41mp315Yn3QMGPmEzCyLsMINgJXTY/dX8kM++1csxU= +go.etcd.io/etcd/client/pkg/v3 v3.6.11/go.mod h1:DysuMe/inqRyC/1tjRR6hReH/VV9Lufs27YKSKBWWJg= +go.etcd.io/etcd/client/v3 v3.6.11 h1:LAByD96VmmeuairkvdAcE0RZnrmGz/q3ceeWePo9bwc= +go.etcd.io/etcd/client/v3 v3.6.11/go.mod h1:vOTDMCo+fGPEClJqcFEFSqZ+8e7WKV7AyqJjX//HR2w= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/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= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/config/etcd/watcher.go b/contrib/config/etcd/watcher.go new file mode 100644 index 0000000..69565ce --- /dev/null +++ b/contrib/config/etcd/watcher.go @@ -0,0 +1,51 @@ +package etcd + +import ( + "context" + + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/go-kratos/kratos/v3/config" +) + +type watcher struct { + source *source + ch clientv3.WatchChan + + ctx context.Context + cancel context.CancelFunc +} + +func newWatcher(s *source) *watcher { + ctx, cancel := context.WithCancel(context.Background()) + w := &watcher{ + source: s, + ctx: ctx, + cancel: cancel, + } + + var opts []clientv3.OpOption + if s.options.prefix { + opts = append(opts, clientv3.WithPrefix()) + } + w.ch = s.client.Watch(s.options.ctx, s.options.path, opts...) + + return w +} + +func (w *watcher) Next() ([]*config.KeyValue, error) { + select { + case resp := <-w.ch: + if err := resp.Err(); err != nil { + return nil, err + } + return w.source.Load() + case <-w.ctx.Done(): + return nil, w.ctx.Err() + } +} + +func (w *watcher) Stop() error { + w.cancel() + return nil +} diff --git a/contrib/config/kubernetes/README.md b/contrib/config/kubernetes/README.md new file mode 100644 index 0000000..4adb669 --- /dev/null +++ b/contrib/config/kubernetes/README.md @@ -0,0 +1,34 @@ +# Kubernetes Config + +### Usage in the Kubernetes Cluster +It is required to +> serviceaccount should be set to the actual account of your environment, the default account will be `namespace::default` if the `spec.serviceAccount` is unset. +execute this command: +``` +kubectl create clusterrolebinding go-kratos:kube --clusterrole=view --serviceaccount=mesh:default +``` +or use `kubectl apply -f bind-role.yaml` +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: go-kratos:kube +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: view +subjects: +- kind: ServiceAccount + name: default + namespace: mesh +``` + +### Usage outside the Kubernetes Cluster +Set the path `~/.kube/config` to KubeConfig +```go +config.NewSource(SourceOption{ + Namespace: "mesh", + LabelSelector: "", + KubeConfig: filepath.Join(homedir.HomeDir(), ".kube", "config"), +}) +``` diff --git a/contrib/config/kubernetes/config.go b/contrib/config/kubernetes/config.go new file mode 100644 index 0000000..5da0cbd --- /dev/null +++ b/contrib/config/kubernetes/config.go @@ -0,0 +1,145 @@ +package kubernetes + +import ( + "context" + "errors" + "fmt" + "path/filepath" + "strings" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/go-kratos/kratos/v3/config" +) + +// Option is kubernetes option. +type Option func(*options) + +type options struct { + // kubernetes namespace + Namespace string + // kubernetes labelSelector example `app=test` + LabelSelector string + // kubernetes fieldSelector example `app=test` + FieldSelector string + // set KubeConfig out-of-cluster Use outside cluster + KubeConfig string + // set master url + Master string +} + +// Namespace with kubernetes namespace. +func Namespace(ns string) Option { + return func(o *options) { + o.Namespace = ns + } +} + +// LabelSelector with kubernetes label selector. +func LabelSelector(label string) Option { + return func(o *options) { + o.LabelSelector = label + } +} + +// FieldSelector with kubernetes field selector. +func FieldSelector(field string) Option { + return func(o *options) { + o.FieldSelector = field + } +} + +// KubeConfig with kubernetes config. +func KubeConfig(config string) Option { + return func(o *options) { + o.KubeConfig = config + } +} + +// Master with kubernetes master. +func Master(master string) Option { + return func(o *options) { + o.Master = master + } +} + +type kube struct { + opts options + client *kubernetes.Clientset +} + +// NewSource new a kubernetes config source. +func NewSource(opts ...Option) config.Source { + op := options{} + for _, o := range opts { + o(&op) + } + return &kube{ + opts: op, + } +} + +func (k *kube) init() (err error) { + var config *rest.Config + if k.opts.KubeConfig != "" { + if config, err = clientcmd.BuildConfigFromFlags(k.opts.Master, k.opts.KubeConfig); err != nil { + return err + } + } else { + if config, err = rest.InClusterConfig(); err != nil { + return err + } + } + if k.client, err = kubernetes.NewForConfig(config); err != nil { + return err + } + return nil +} + +func (k *kube) load() (kvs []*config.KeyValue, err error) { + cmList, err := k.client. + CoreV1(). + ConfigMaps(k.opts.Namespace). + List(context.Background(), metav1.ListOptions{ + LabelSelector: k.opts.LabelSelector, + FieldSelector: k.opts.FieldSelector, + }) + if err != nil { + return nil, err + } + for _, cm := range cmList.Items { + kvs = append(kvs, k.configMap(cm)...) + } + return kvs, nil +} + +func (k *kube) configMap(cm v1.ConfigMap) (kvs []*config.KeyValue) { + for name, val := range cm.Data { + k := fmt.Sprintf("%s/%s/%s", k.opts.Namespace, cm.Name, name) + + kvs = append(kvs, &config.KeyValue{ + Key: k, + Value: []byte(val), + Format: strings.TrimPrefix(filepath.Ext(k), "."), + }) + } + return kvs +} + +func (k *kube) Load() ([]*config.KeyValue, error) { + if k.opts.Namespace == "" { + return nil, errors.New("options namespace not full") + } + if err := k.init(); err != nil { + return nil, err + } + return k.load() +} + +func (k *kube) Watch() (config.Watcher, error) { + return newWatcher(k) +} diff --git a/contrib/config/kubernetes/config_test.go b/contrib/config/kubernetes/config_test.go new file mode 100644 index 0000000..760266d --- /dev/null +++ b/contrib/config/kubernetes/config_test.go @@ -0,0 +1,208 @@ +package kubernetes + +import ( + "context" + "log" + "path/filepath" + "reflect" + "strings" + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" + + "github.com/go-kratos/kratos/v3/config" +) + +const ( + testKey = "test_config.json" + namespace = "default" + name = "test" +) + +var ( + keyPath = strings.Join([]string{namespace, name, testKey}, "/") + objectMeta = metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + "app": "test", + }, + } +) + +func TestSource(t *testing.T) { + home := homedir.HomeDir() + s := NewSource( + Namespace("default"), + LabelSelector(""), + KubeConfig(filepath.Join(home, ".kube", "config")), + ) + kvs, err := s.Load() + if err != nil { + t.Error(err) + } + for _, v := range kvs { + t.Log(v) + } +} + +func ExampleNewSource() { + conf := config.New( + config.WithSource( + NewSource( + Namespace("mesh"), + LabelSelector("app=test"), + KubeConfig(filepath.Join(homedir.HomeDir(), ".kube", "config")), + ), + ), + ) + err := conf.Load() + if err != nil { + log.Panic(err) + } +} + +func TestConfig(t *testing.T) { + restConfig, err := rest.InClusterConfig() + home := homedir.HomeDir() + + options := []Option{ + Namespace(namespace), + LabelSelector("app=test"), + } + + if err != nil { + kubeconfig := filepath.Join(home, ".kube", "config") + restConfig, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + t.Fatal(err) + } + options = append(options, KubeConfig(kubeconfig)) + } + clientSet, err := kubernetes.NewForConfig(restConfig) + if err != nil { + t.Fatal(err) + } + + clientSetConfigMaps := clientSet.CoreV1().ConfigMaps(namespace) + + source := NewSource(options...) + if _, err = clientSetConfigMaps.Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: objectMeta, + Data: map[string]string{ + testKey: "test config", + }, + }, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + + defer func() { + if err = clientSetConfigMaps.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil { + t.Error(err) + } + }() + kvs, err := source.Load() + if err != nil { + t.Fatal(err) + } + if len(kvs) != 1 || kvs[0].Key != keyPath || string(kvs[0].Value) != "test config" { + t.Fatal("config error") + } + + w, err := source.Watch() + if err != nil { + t.Fatal(err) + } + defer func() { + _ = w.Stop() + }() + // create also produce an event, discard it + if _, err = w.Next(); err != nil { + t.Fatal(err) + } + + if _, err = clientSetConfigMaps.Update(context.Background(), &v1.ConfigMap{ + ObjectMeta: objectMeta, + Data: map[string]string{ + testKey: "new config", + }, + }, metav1.UpdateOptions{}); err != nil { + t.Error(err) + } + + if kvs, err = w.Next(); err != nil { + t.Fatal(err) + } + + if len(kvs) != 1 || kvs[0].Key != keyPath || string(kvs[0].Value) != "new config" { + t.Fatal("config error") + } +} + +func TestExtToFormat(t *testing.T) { + restConfig, err := rest.InClusterConfig() + home := homedir.HomeDir() + + options := []Option{ + Namespace(namespace), + LabelSelector("app=test"), + } + + if err != nil { + kubeconfig := filepath.Join(home, ".kube", "config") + restConfig, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + t.Fatal(err) + } + options = append(options, KubeConfig(kubeconfig)) + } + clientSet, err := kubernetes.NewForConfig(restConfig) + if err != nil { + t.Fatal(err) + } + + clientSetConfigMaps := clientSet.CoreV1().ConfigMaps(namespace) + + tc := `{"a":1}` + if _, err = clientSetConfigMaps.Create(context.Background(), &v1.ConfigMap{ + ObjectMeta: objectMeta, + Data: map[string]string{ + testKey: tc, + }, + }, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + + defer func() { + if err = clientSetConfigMaps.Delete(context.Background(), name, metav1.DeleteOptions{}); err != nil { + t.Error(err) + } + }() + + source := NewSource(options...) + if err != nil { + t.Fatal(err) + } + + kvs, err := source.Load() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(len(kvs), 1) { + t.Errorf("len(kvs) = %d", len(kvs)) + } + if !reflect.DeepEqual(keyPath, kvs[0].Key) { + t.Errorf("kvs[0].Key is %s", kvs[0].Key) + } + if !reflect.DeepEqual(tc, string(kvs[0].Value)) { + t.Errorf("kvs[0].Value is %s", kvs[0].Value) + } + if !reflect.DeepEqual("json", kvs[0].Format) { + t.Errorf("kvs[0].Format is %s", kvs[0].Format) + } +} diff --git a/contrib/config/kubernetes/go.mod b/contrib/config/kubernetes/go.mod new file mode 100644 index 0000000..6805a70 --- /dev/null +++ b/contrib/config/kubernetes/go.mod @@ -0,0 +1,64 @@ +module github.com/go-kratos/kratos/contrib/config/kubernetes/v3 + +go 1.26.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + k8s.io/api v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.23.1 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/swag v0.26.0 // indirect + github.com/go-openapi/swag/cmdutils v0.26.0 // indirect + github.com/go-openapi/swag/conv v0.26.0 // indirect + github.com/go-openapi/swag/fileutils v0.26.0 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect + github.com/go-openapi/swag/jsonutils v0.26.0 // indirect + github.com/go-openapi/swag/loading v0.26.0 // indirect + github.com/go-openapi/swag/mangling v0.26.0 // indirect + github.com/go-openapi/swag/netutils v0.26.0 // indirect + github.com/go-openapi/swag/stringutils v0.26.0 // indirect + github.com/go-openapi/swag/typeutils v0.26.0 // indirect + github.com/go-openapi/swag/yamlutils v0.26.0 // indirect + github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 // indirect + github.com/go-openapi/testify/v2 v2.5.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + 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/spf13/pflag v1.0.10 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260512234627-ef417d054102 // indirect + k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/config/kubernetes/go.sum b/contrib/config/kubernetes/go.sum new file mode 100644 index 0000000..a883a50 --- /dev/null +++ b/contrib/config/kubernetes/go.sum @@ -0,0 +1,128 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 h1:3hZD1fwydvCx/cc1R2uYNQirHqf2s6lqpKV3FcNTURA= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.0/go.mod h1:TvDZKBH7ZbMaF3EqH2AwTvNQCmzyZq8K1agRjf1B+Nk= +github.com/go-openapi/testify/v2 v2.5.0 h1:UOCr63aAsMIDydZbZGqo5Ev01D4eydItRbekDuZMJLw= +github.com/go-openapi/testify/v2 v2.5.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260512234627-ef417d054102 h1:xs2ux1MvyrOdfKwS3vuFWrGuLgDOHk6id975Twx2Jss= +k8s.io/kube-openapi v0.0.0-20260512234627-ef417d054102/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo= +sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/contrib/config/kubernetes/watcher.go b/contrib/config/kubernetes/watcher.go new file mode 100644 index 0000000..cff033f --- /dev/null +++ b/contrib/config/kubernetes/watcher.go @@ -0,0 +1,61 @@ +package kubernetes + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + + "github.com/go-kratos/kratos/v3/config" +) + +type watcher struct { + k *kube + watcher watch.Interface +} + +func newWatcher(k *kube) (config.Watcher, error) { + w, err := k.client.CoreV1().ConfigMaps(k.opts.Namespace).Watch(context.Background(), metav1.ListOptions{ + LabelSelector: k.opts.LabelSelector, + FieldSelector: k.opts.FieldSelector, + }) + if err != nil { + return nil, err + } + return &watcher{ + k: k, + watcher: w, + }, nil +} + +func (w *watcher) Next() ([]*config.KeyValue, error) { +ResultChan: + ch := <-w.watcher.ResultChan() + if ch.Object == nil { + // recreate the watcher + k8sWatcher, err := w.k.client.CoreV1().ConfigMaps(w.k.opts.Namespace).Watch(context.Background(), metav1.ListOptions{ + LabelSelector: w.k.opts.LabelSelector, + FieldSelector: w.k.opts.FieldSelector, + }) + if err != nil { + return nil, err + } + w.watcher = k8sWatcher + goto ResultChan + } + cm, ok := ch.Object.(*v1.ConfigMap) + if !ok { + return nil, fmt.Errorf("kubernetes Object not ConfigMap") + } + if ch.Type == "DELETED" { + return nil, fmt.Errorf("kubernetes configmap delete %s", cm.Name) + } + return w.k.configMap(*cm), nil +} + +func (w *watcher) Stop() error { + w.watcher.Stop() + return nil +} diff --git a/contrib/config/kubernetes/watcher_test.go b/contrib/config/kubernetes/watcher_test.go new file mode 100644 index 0000000..d0b375b --- /dev/null +++ b/contrib/config/kubernetes/watcher_test.go @@ -0,0 +1,42 @@ +package kubernetes + +import ( + "context" + "path/filepath" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" +) + +func TestKube(t *testing.T) { + home := homedir.HomeDir() + config, err := clientcmd.BuildConfigFromFlags("", filepath.Join(home, ".kube", "config")) + if err != nil { + t.Error(err) + } + client, err := kubernetes.NewForConfig(config) + if err != nil { + t.Error(err) + } + cmWatcher, err := client.CoreV1().ConfigMaps("mesh").Watch(context.Background(), metav1.ListOptions{ + LabelSelector: "app=test", + // FieldSelector: "", + }) + if err != nil { + t.Error(err) + } + go func() { + time.Sleep(5 * time.Second) + cmWatcher.Stop() + }() + for c := range cmWatcher.ResultChan() { + if c.Object == nil { + return + } + t.Log(c.Type, c.Object) + } +} diff --git a/contrib/config/nacos/README.md b/contrib/config/nacos/README.md new file mode 100644 index 0000000..e014708 --- /dev/null +++ b/contrib/config/nacos/README.md @@ -0,0 +1,34 @@ +# Nacos Config + +```go +import ( + "github.com/nacos-group/nacos-sdk-go/clients" + "github.com/nacos-group/nacos-sdk-go/common/constant" + + kconfig "github.com/go-kratos/kratos/v3/config" +) + +sc := []constant.ServerConfig{ + *constant.NewServerConfig("127.0.0.1", 8848), +} + +cc := &constant.ClientConfig{ + NamespaceId: "public", //namespace id + TimeoutMs: 5000, + NotLoadCacheAtStart: true, + LogDir: "/tmp/nacos/log", + CacheDir: "/tmp/nacos/cache", + LogLevel: "debug", +} + +// a more graceful way to create naming client +client, err := clients.NewConfigClient( + vo.NacosClientParam{ + ClientConfig: cc, + ServerConfigs: sc, + }, +) +if err != nil { + log.Panic(err) +} +``` diff --git a/contrib/config/nacos/config.go b/contrib/config/nacos/config.go new file mode 100644 index 0000000..1065294 --- /dev/null +++ b/contrib/config/nacos/config.go @@ -0,0 +1,81 @@ +package config + +import ( + "context" + "path/filepath" + "strings" + + "github.com/nacos-group/nacos-sdk-go/clients/config_client" + "github.com/nacos-group/nacos-sdk-go/vo" + + "github.com/go-kratos/kratos/v3/config" +) + +type Option func(*options) + +type options struct { + group string + dataID string +} + +// WithGroup With nacos config group. +func WithGroup(group string) Option { + return func(o *options) { + o.group = group + } +} + +// WithDataID With nacos config data id. +func WithDataID(dataID string) Option { + return func(o *options) { + o.dataID = dataID + } +} + +type Config struct { + opts options + client config_client.IConfigClient +} + +func NewConfigSource(client config_client.IConfigClient, opts ...Option) config.Source { + _options := options{} + for _, o := range opts { + o(&_options) + } + return &Config{client: client, opts: _options} +} + +func (c *Config) Load() ([]*config.KeyValue, error) { + content, err := c.client.GetConfig(vo.ConfigParam{ + DataId: c.opts.dataID, + Group: c.opts.group, + }) + if err != nil { + return nil, err + } + k := c.opts.dataID + return []*config.KeyValue{ + { + Key: k, + Value: []byte(content), + Format: strings.TrimPrefix(filepath.Ext(k), "."), + }, + }, nil +} + +func (c *Config) Watch() (config.Watcher, error) { + watcher := newWatcher(context.Background(), c.opts.dataID, c.opts.group, c.client.CancelListenConfig) + err := c.client.ListenConfig(vo.ConfigParam{ + DataId: c.opts.dataID, + Group: c.opts.group, + OnChange: func(_, group, dataId, data string) { + if dataId == watcher.dataID && group == watcher.group { + watcher.content <- data + } + }, + }) + if err != nil { + return nil, err + } + return watcher, nil +} diff --git a/contrib/config/nacos/config_test.go b/contrib/config/nacos/config_test.go new file mode 100644 index 0000000..887dd26 --- /dev/null +++ b/contrib/config/nacos/config_test.go @@ -0,0 +1,208 @@ +package config + +import ( + "reflect" + "testing" + "time" + + "github.com/nacos-group/nacos-sdk-go/clients" + "github.com/nacos-group/nacos-sdk-go/common/constant" + "github.com/nacos-group/nacos-sdk-go/vo" + + "github.com/go-kratos/kratos/v3/config" +) + +func TestConfig_Load(t *testing.T) { + sc := []constant.ServerConfig{ + *constant.NewServerConfig("127.0.0.1", 8848), + } + + cc := constant.ClientConfig{ + TimeoutMs: 5000, + NotLoadCacheAtStart: true, + LogDir: "/tmp/nacos/log", + CacheDir: "/tmp/nacos/cache", + LogLevel: "debug", + } + + client, err := clients.NewConfigClient( + vo.NacosClientParam{ + ClientConfig: &cc, + ServerConfigs: sc, + }, + ) + if err != nil { + t.Fatal(err) + } + source := NewConfigSource(client, WithGroup("test"), WithDataID("test.yaml")) + + type fields struct { + source config.Source + } + tests := []struct { + name string + fields fields + want []*config.KeyValue + wantErr bool + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + }{ + { + name: "normal", + fields: fields{ + source: source, + }, + wantErr: false, + preFunc: func(t *testing.T) { + _, err = client.PublishConfig(vo.ConfigParam{DataId: "test.yaml", Group: "test", Content: "test: test"}) + if err != nil { + t.Error(err) + } + time.Sleep(time.Second * 1) + }, + deferFunc: func(t *testing.T) { + _, dErr := client.DeleteConfig(vo.ConfigParam{DataId: "test.yaml", Group: "test"}) + if dErr != nil { + t.Error(dErr) + } + }, + want: []*config.KeyValue{{ + Key: "test.yaml", + Value: []byte("test: test"), + Format: "yaml", + }}, + }, + { + name: "error", + fields: fields{ + source: source, + }, + wantErr: false, + preFunc: func(t *testing.T) { + _, err = client.PublishConfig(vo.ConfigParam{DataId: "111.yaml", Group: "notExist", Content: "test: test"}) + if err != nil { + t.Error(err) + } + time.Sleep(time.Second * 1) + }, + deferFunc: func(t *testing.T) { + _, dErr := client.DeleteConfig(vo.ConfigParam{DataId: "111.yaml", Group: "notExist"}) + if dErr != nil { + t.Error(dErr) + } + }, + want: []*config.KeyValue{{ + Key: "test.yaml", + Value: []byte{}, + Format: "yaml", + }}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.preFunc != nil { + test.preFunc(t) + } + if test.deferFunc != nil { + defer test.deferFunc(t) + } + s := test.fields.source + configs, lErr := s.Load() + if (lErr != nil) != test.wantErr { + t.Errorf("Load error = %v, wantErr %v", lErr, test.wantErr) + t.Errorf("Load configs = %v", configs) + return + } + if !reflect.DeepEqual(configs, test.want) { + t.Errorf("Load configs = %v, want %v", configs, test.want) + } + }) + } +} + +func TestConfig_Watch(t *testing.T) { + sc := []constant.ServerConfig{ + *constant.NewServerConfig("127.0.0.1", 8848), + } + + cc := constant.ClientConfig{ + TimeoutMs: 5000, + NotLoadCacheAtStart: true, + LogDir: "/tmp/nacos/log", + CacheDir: "/tmp/nacos/cache", + LogLevel: "debug", + } + + client, err := clients.NewConfigClient( + vo.NacosClientParam{ + ClientConfig: &cc, + ServerConfigs: sc, + }, + ) + if err != nil { + t.Fatal(err) + } + + source := NewConfigSource(client, WithGroup("test"), WithDataID("test.yaml")) + + type fields struct { + source config.Source + } + tests := []struct { + name string + fields fields + want []*config.KeyValue + wantErr bool + processFunc func(t *testing.T, w config.Watcher) + deferFunc func(t *testing.T, w config.Watcher) + }{ + { + name: "normal", + fields: fields{ + source: source, + }, + wantErr: false, + processFunc: func(t *testing.T, _ config.Watcher) { + _, pErr := client.PublishConfig(vo.ConfigParam{DataId: "test.yaml", Group: "test", Content: "test: test"}) + if pErr != nil { + t.Error(pErr) + } + }, + deferFunc: func(t *testing.T, _ config.Watcher) { + _, dErr := client.DeleteConfig(vo.ConfigParam{DataId: "test.yaml", Group: "test"}) + if dErr != nil { + t.Error(dErr) + } + }, + want: []*config.KeyValue{{ + Key: "test.yaml", + Value: []byte("test: test"), + Format: "yaml", + }}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := test.fields.source + watch, wErr := s.Watch() + if wErr != nil { + t.Error(wErr) + return + } + if test.processFunc != nil { + test.processFunc(t, watch) + } + if test.deferFunc != nil { + defer test.deferFunc(t, watch) + } + want, nErr := watch.Next() + if (nErr != nil) != test.wantErr { + t.Errorf("Watch error = %v, wantErr %v", nErr, test.wantErr) + return + } + if !reflect.DeepEqual(want, test.want) { + t.Errorf("Watch watcher = %v, want %v", watch, test.want) + } + }) + } +} diff --git a/contrib/config/nacos/go.mod b/contrib/config/nacos/go.mod new file mode 100644 index 0000000..cb50bee --- /dev/null +++ b/contrib/config/nacos/go.mod @@ -0,0 +1,32 @@ +module github.com/go-kratos/kratos/contrib/config/nacos/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/nacos-group/nacos-sdk-go v1.1.6 +) + +require ( + github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect + github.com/buger/jsonparser v1.2.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/pkg/errors v0.9.1 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + golang.org/x/sync v0.20.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/ini.v1 v1.67.2 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/config/nacos/go.sum b/contrib/config/nacos/go.sum new file mode 100644 index 0000000..99a5a01 --- /dev/null +++ b/contrib/config/nacos/go.sum @@ -0,0 +1,182 @@ +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/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= +github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nacos-group/nacos-sdk-go v1.1.6 h1:zjn7CIoz0RxPHCalWc9kXOQx94oUFQl5J1rctbq2mYU= +github.com/nacos-group/nacos-sdk-go v1.1.6/go.mod h1:cBv9wy5iObs7khOqov1ERFQrCuTR4ILpgaiaVMxEmGI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/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-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +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-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/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-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/contrib/config/nacos/watcher.go b/contrib/config/nacos/watcher.go new file mode 100644 index 0000000..fee118c --- /dev/null +++ b/contrib/config/nacos/watcher.go @@ -0,0 +1,66 @@ +package config + +import ( + "context" + "path/filepath" + "strings" + + "github.com/nacos-group/nacos-sdk-go/vo" + + "github.com/go-kratos/kratos/v3/config" +) + +type Watcher struct { + dataID string + group string + content chan string + cancelListenConfig cancelListenConfigFunc + + ctx context.Context + cancel context.CancelFunc +} + +type cancelListenConfigFunc func(params vo.ConfigParam) (err error) + +func newWatcher(ctx context.Context, dataID string, group string, cancelListenConfig cancelListenConfigFunc) *Watcher { + ctx, cancel := context.WithCancel(ctx) + w := &Watcher{ + dataID: dataID, + group: group, + cancelListenConfig: cancelListenConfig, + content: make(chan string, 100), + + ctx: ctx, + cancel: cancel, + } + return w +} + +func (w *Watcher) Next() ([]*config.KeyValue, error) { + select { + case <-w.ctx.Done(): + return nil, w.ctx.Err() + case content := <-w.content: + k := w.dataID + return []*config.KeyValue{ + { + Key: k, + Value: []byte(content), + Format: strings.TrimPrefix(filepath.Ext(k), "."), + }, + }, nil + } +} + +func (w *Watcher) Close() error { + err := w.cancelListenConfig(vo.ConfigParam{ + DataId: w.dataID, + Group: w.group, + }) + w.cancel() + return err +} + +func (w *Watcher) Stop() error { + return w.Close() +} diff --git a/contrib/config/polaris/README.md b/contrib/config/polaris/README.md new file mode 100644 index 0000000..a714ee7 --- /dev/null +++ b/contrib/config/polaris/README.md @@ -0,0 +1,24 @@ +# Polaris Config + +```go +import ( + "log" + + "github.com/polarismesh/polaris-go" + + "github.com/go-kratos/kratos/contrib/config/polaris/v3" +) + +func main() { + configApi, err := polaris.NewConfigAPI() + if err != nil { + log.Fatalln(err) + } + + source, err := New(&configApi, WithNamespace("default"), WithFileGroup("default"), WithFileName("default.yaml")) + if err != nil { + log.Fatalln(err) + } + source.Load() +} +``` diff --git a/contrib/config/polaris/config.go b/contrib/config/polaris/config.go new file mode 100644 index 0000000..f8b634c --- /dev/null +++ b/contrib/config/polaris/config.go @@ -0,0 +1,108 @@ +package config + +import ( + "errors" + "fmt" + "path/filepath" + "strings" + + "github.com/polarismesh/polaris-go" + "github.com/polarismesh/polaris-go/pkg/model" + + "github.com/go-kratos/kratos/v3/config" +) + +// Option is polaris config option. +type Option func(o *options) + +type options struct { + namespace string + fileGroup string + fileName string + configFile polaris.ConfigFile +} + +// WithNamespace with polaris config namespace +func WithNamespace(namespace string) Option { + return func(o *options) { + o.namespace = namespace + } +} + +// WithFileGroup with polaris config fileGroup +func WithFileGroup(fileGroup string) Option { + return func(o *options) { + o.fileGroup = fileGroup + } +} + +// WithFileName with polaris config fileName +func WithFileName(fileName string) Option { + return func(o *options) { + o.fileName = fileName + } +} + +type source struct { + client polaris.ConfigAPI + options *options +} + +func New(client polaris.ConfigAPI, opts ...Option) (config.Source, error) { + options := &options{ + namespace: "default", + fileGroup: "", + fileName: "", + } + + for _, opt := range opts { + opt(options) + } + + if options.fileGroup == "" { + return nil, errors.New("fileGroup invalid") + } + + if options.fileName == "" { + return nil, errors.New("fileName invalid") + } + + return &source{ + client: client, + options: options, + }, nil +} + +// Load return the config values +func (s *source) Load() ([]*config.KeyValue, error) { + configFile, err := s.client.FetchConfigFile(&polaris.GetConfigFileRequest{ + GetConfigFileRequest: &model.GetConfigFileRequest{ + Namespace: s.options.namespace, + FileGroup: s.options.fileGroup, + FileName: s.options.fileName, + Subscribe: true, + }, + }) + if err != nil { + fmt.Println("fail to get config.", err) + return nil, err + } + + content := configFile.GetContent() + k := s.options.fileName + + s.options.configFile = configFile + + return []*config.KeyValue{ + { + Key: k, + Value: []byte(content), + Format: strings.TrimPrefix(filepath.Ext(k), "."), + }, + }, nil +} + +// Watch return the watcher +func (s *source) Watch() (config.Watcher, error) { + return newWatcher(s.options.configFile), nil +} diff --git a/contrib/config/polaris/go.mod b/contrib/config/polaris/go.mod new file mode 100644 index 0000000..bed50db --- /dev/null +++ b/contrib/config/polaris/go.mod @@ -0,0 +1,44 @@ +module github.com/go-kratos/kratos/contrib/config/polaris/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/polarismesh/polaris-go v1.7.0 +) + +require ( + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/natefinch/lumberjack v2.0.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/polarismesh/specification v1.8.0 // 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/procfs v0.20.1 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/config/polaris/go.sum b/contrib/config/polaris/go.sum new file mode 100644 index 0000000..4330fcb --- /dev/null +++ b/contrib/config/polaris/go.sum @@ -0,0 +1,1010 @@ +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 v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +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/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +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/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +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/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +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= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +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.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +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/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= +github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg= +github.com/gonum/integrate v0.0.0-20181209220457-a422b5c0fdf2/go.mod h1:pDgmNM6seYpwvPos3q+zxlXMsbve6mOIPucUnUOrI7Y= +github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks= +github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A= +github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw= +github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b/go.mod h1:Z4GIJBJO3Wa4gD4vbwQxXXZ+WHmW6E9ixmNrwvs0iZs= +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/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/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/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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/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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polarismesh/polaris-go v1.7.0 h1:iiaKcYWXrRWIOqoBBgAQVswhPxwOkIjI6P6nMVh+d14= +github.com/polarismesh/polaris-go v1.7.0/go.mod h1:up/F6sUGOXzd9pMDkkxtPFrMq3lmdz0U8BvEtvWLKCA= +github.com/polarismesh/specification v1.7.0/go.mod h1:rDvMMtl5qebPmqiBLNa5Ps0XtwkP31ZLirbH4kXA0YU= +github.com/polarismesh/specification v1.8.0 h1:k112R8eUZfdv1tVthWcf5AbE0SR/afyETz6Eii5OFw8= +github.com/polarismesh/specification v1.8.0/go.mod h1:Gb9OLXOELG4VwhxPh4DK6C0C3Z7FFYCmVkTW/d6+kvM= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/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.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-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-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/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-20181114220301-adae6a3d119a/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-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-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +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-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +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-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20200106162015-b016eb3dc98e/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/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-20210124154548-22da62e12c0c/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +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-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-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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/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/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= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +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.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +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-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +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.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-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/contrib/config/polaris/polaris.yaml b/contrib/config/polaris/polaris.yaml new file mode 100644 index 0000000..fdee615 --- /dev/null +++ b/contrib/config/polaris/polaris.yaml @@ -0,0 +1,8 @@ +global: + serverConnector: + addresses: + - 127.0.0.1:8091 +config: + configConnector: + addresses: + - 127.0.0.1:8093 diff --git a/contrib/config/polaris/watcher.go b/contrib/config/polaris/watcher.go new file mode 100644 index 0000000..853b80b --- /dev/null +++ b/contrib/config/polaris/watcher.go @@ -0,0 +1,80 @@ +package config + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/polarismesh/polaris-go" + "github.com/polarismesh/polaris-go/pkg/model" + + "github.com/go-kratos/kratos/v3/config" + "github.com/go-kratos/kratos/v3/log" +) + +type Watcher struct { + configFile polaris.ConfigFile + fullPath string +} + +type eventChan struct { + closed bool + event chan model.ConfigFileChangeEvent +} + +var eventChanMap = make(map[string]eventChan) + +func getFullPath(namespace string, fileGroup string, fileName string) string { + return fmt.Sprintf("%s/%s/%s", namespace, fileGroup, fileName) +} + +func receive(event model.ConfigFileChangeEvent) { + meta := event.ConfigFileMetadata + ec := eventChanMap[getFullPath(meta.GetNamespace(), meta.GetFileGroup(), meta.GetFileName())] + defer func() { + if err := recover(); err != nil { + log.Error("panic recovered", "err", err) + } + }() + if !ec.closed { + ec.event <- event + } +} + +func newWatcher(configFile polaris.ConfigFile) *Watcher { + configFile.AddChangeListener(receive) + + fullPath := getFullPath(configFile.GetNamespace(), configFile.GetFileGroup(), configFile.GetFileName()) + if _, ok := eventChanMap[fullPath]; !ok { + eventChanMap[fullPath] = eventChan{ + closed: false, + event: make(chan model.ConfigFileChangeEvent), + } + } + w := &Watcher{ + configFile: configFile, + fullPath: fullPath, + } + return w +} + +func (w *Watcher) Next() ([]*config.KeyValue, error) { + ec := eventChanMap[w.fullPath] + event := <-ec.event + return []*config.KeyValue{ + { + Key: w.configFile.GetFileName(), + Value: []byte(event.NewValue), + Format: strings.TrimPrefix(filepath.Ext(w.configFile.GetFileName()), "."), + }, + }, nil +} + +func (w *Watcher) Stop() error { + ec := eventChanMap[w.fullPath] + if !ec.closed { + ec.closed = true + close(ec.event) + } + return nil +} diff --git a/contrib/encoding/json/go.mod b/contrib/encoding/json/go.mod new file mode 100644 index 0000000..18173d4 --- /dev/null +++ b/contrib/encoding/json/go.mod @@ -0,0 +1,10 @@ +module github.com/go-kratos/kratos/contrib/encoding/json/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + google.golang.org/protobuf v1.36.11 +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/encoding/json/go.sum b/contrib/encoding/json/go.sum new file mode 100644 index 0000000..296be18 --- /dev/null +++ b/contrib/encoding/json/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/contrib/encoding/json/json.go b/contrib/encoding/json/json.go new file mode 100644 index 0000000..22aa363 --- /dev/null +++ b/contrib/encoding/json/json.go @@ -0,0 +1,71 @@ +package json + +import ( + "encoding/json" + "reflect" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Name is the name registered for the json codec. +const Name = "json" + +var ( + // MarshalOptions is a configurable JSON format marshaller. + MarshalOptions = protojson.MarshalOptions{ + EmitUnpopulated: true, + } + // UnmarshalOptions is a configurable JSON format parser. + UnmarshalOptions = protojson.UnmarshalOptions{ + DiscardUnknown: true, + } +) + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with json. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + switch m := v.(type) { + case json.Marshaler: + return m.MarshalJSON() + case proto.Message: + return MarshalOptions.Marshal(m) + default: + return json.Marshal(m) + } +} + +func (codec) Unmarshal(data []byte, v any) error { + if len(data) == 0 { + return nil + } + switch m := v.(type) { + case json.Unmarshaler: + return m.UnmarshalJSON(data) + case proto.Message: + return UnmarshalOptions.Unmarshal(data, m) + default: + rv := reflect.ValueOf(v) + for rv := rv; rv.Kind() == reflect.Pointer; { + if rv.IsNil() { + rv.Set(reflect.New(rv.Type().Elem())) + } + rv = rv.Elem() + } + if m, ok := reflect.Indirect(rv).Interface().(proto.Message); ok { + return UnmarshalOptions.Unmarshal(data, m) + } + return json.Unmarshal(data, m) + } +} + +func (codec) Name() string { + return Name +} diff --git a/contrib/encoding/json/json_test.go b/contrib/encoding/json/json_test.go new file mode 100644 index 0000000..6b5d529 --- /dev/null +++ b/contrib/encoding/json/json_test.go @@ -0,0 +1,168 @@ +package json + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + "google.golang.org/protobuf/types/known/wrapperspb" +) + +type testEmbed struct { + Level1a int `json:"a"` + Level1b int `json:"b"` + Level1c int `json:"c"` +} + +type testMessage struct { + Field1 string `json:"a"` + Field2 string `json:"b"` + Field3 string `json:"c"` + Embed *testEmbed `json:"embed,omitempty"` +} + +type mock struct { + value int +} + +const ( + Unknown = iota + Gopher + Zebra +) + +func (a *mock) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + switch strings.ToLower(s) { + default: + a.value = Unknown + case "gopher": + a.value = Gopher + case "zebra": + a.value = Zebra + } + + return nil +} + +func (a *mock) MarshalJSON() ([]byte, error) { + var s string + switch a.value { + default: + s = "unknown" + case Gopher: + s = "gopher" + case Zebra: + s = "zebra" + } + + return json.Marshal(s) +} + +func TestJSON_Marshal(t *testing.T) { + tests := []struct { + input any + expect string + }{ + { + input: &testMessage{}, + expect: `{"a":"","b":"","c":""}`, + }, + { + input: &testMessage{Field1: "a", Field2: "b", Field3: "c"}, + expect: `{"a":"a","b":"b","c":"c"}`, + }, + { + input: &mock{value: Gopher}, + expect: `"gopher"`, + }, + { + input: wrapperspb.Int64(1), + expect: `"1"`, + }, + } + for _, v := range tests { + data, err := (codec{}).Marshal(v.input) + if err != nil { + t.Errorf("marshal(%#v): %s", v.input, err) + } + if got, want := string(data), v.expect; strings.ReplaceAll(got, " ", "") != want { + if strings.Contains(want, "\n") { + t.Errorf("marshal(%#v):\nHAVE:\n%s\nWANT:\n%s", v.input, got, want) + } else { + t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) + } + } + } +} + +func TestJSON_Unmarshal(t *testing.T) { + p := testMessage{} + p2 := &mock{} + p3 := wrapperspb.Int64(0) + tests := []struct { + input string + expect any + }{ + { + input: `{"a":"","b":"","c":""}`, + expect: &testMessage{}, + }, + { + input: `{"a":"a","b":"b","c":"c"}`, + expect: &p, + }, + { + input: `"zebra"`, + expect: p2, + }, + { + input: `"2"`, + expect: p3, + }, + } + for _, v := range tests { + want := []byte(v.input) + err := (codec{}).Unmarshal(want, v.expect) + if err != nil { + t.Errorf("unmarshal(%#v): %s", v.input, err) + } + got, err := codec{}.Marshal(v.expect) + if err != nil { + t.Errorf("marshal(%#v): %s", v.input, err) + } + if !reflect.DeepEqual(strings.ReplaceAll(string(got), " ", ""), strings.ReplaceAll(string(want), " ", "")) { + t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) + } + } +} + +func TestJSON_UnmarshalPointerToProtoMessage(t *testing.T) { + var msg *wrapperspb.StringValue + if err := (codec{}).Unmarshal([]byte(`"kratos"`), &msg); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + if msg == nil { + t.Fatal("Unmarshal() message = nil, want allocated message") + } + if got, want := msg.Value, "kratos"; got != want { + t.Fatalf("Unmarshal() value = %s, want %s", got, want) + } +} + +func TestJSON_EmptyData(t *testing.T) { + var msg wrapperspb.StringValue + if err := (codec{}).Unmarshal(nil, &msg); err != nil { + t.Fatalf("Unmarshal(nil) error = %v", err) + } +} + +func TestJSON_Name(t *testing.T) { + if got, want := (codec{}).Name(), Name; got != want { + t.Fatalf("Name() = %s, want %s", got, want) + } +} diff --git a/contrib/encoding/msgpack/go.mod b/contrib/encoding/msgpack/go.mod new file mode 100644 index 0000000..14f14ae --- /dev/null +++ b/contrib/encoding/msgpack/go.mod @@ -0,0 +1,15 @@ +module github.com/go-kratos/kratos/contrib/encoding/msgpack/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/vmihailenco/msgpack/v5 v5.4.1 +) + +require ( + github.com/stretchr/testify v1.11.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/encoding/msgpack/go.sum b/contrib/encoding/msgpack/go.sum new file mode 100644 index 0000000..4999b51 --- /dev/null +++ b/contrib/encoding/msgpack/go.sum @@ -0,0 +1,12 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/encoding/msgpack/msgpack.go b/contrib/encoding/msgpack/msgpack.go new file mode 100644 index 0000000..26cb27f --- /dev/null +++ b/contrib/encoding/msgpack/msgpack.go @@ -0,0 +1,29 @@ +package msgpack + +import ( + "github.com/vmihailenco/msgpack/v5" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Name is the name registered for the msgpack compressor. +const Name = "msgpack" + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with msgpack. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + return msgpack.Marshal(v) +} + +func (codec) Unmarshal(data []byte, v any) error { + return msgpack.Unmarshal(data, v) +} + +func (codec) Name() string { + return Name +} diff --git a/contrib/encoding/msgpack/msgpack_test.go b/contrib/encoding/msgpack/msgpack_test.go new file mode 100644 index 0000000..529a1b9 --- /dev/null +++ b/contrib/encoding/msgpack/msgpack_test.go @@ -0,0 +1,63 @@ +package msgpack + +import ( + "reflect" + "testing" +) + +type loginRequest struct { + UserName string + Password string +} + +type testModel struct { + ID int32 + Name string +} + +func TestName(t *testing.T) { + c := new(codec) + if !reflect.DeepEqual("msgpack", c.Name()) { + t.Errorf("Name() should be msgpack, but got %s", c.Name()) + } +} + +func TestCodec(t *testing.T) { + c := new(codec) + t2 := testModel{ID: 1, Name: "name"} + m, err := c.Marshal(&t2) + if err != nil { + t.Errorf("Marshal() should be nil, but got %s", err) + } + var t3 testModel + err = c.Unmarshal(m, &t3) + if err != nil { + t.Errorf("Unmarshal() should be nil, but got %s", err) + } + if !reflect.DeepEqual(t2.ID, t3.ID) { + t.Errorf("ID should be %d, but got %d", t2.ID, t3.ID) + } + if !reflect.DeepEqual(t3.Name, t2.Name) { + t.Errorf("Name should be %s, but got %s", t2.Name, t3.Name) + } + + request := loginRequest{ + UserName: "username", + Password: "password", + } + m, err = c.Marshal(&request) + if err != nil { + t.Errorf("Marshal() should be nil, but got %s", err) + } + var req loginRequest + err = c.Unmarshal(m, &req) + if err != nil { + t.Errorf("Unmarshal() should be nil, but got %s", err) + } + if !reflect.DeepEqual(req.Password, request.Password) { + t.Errorf("ID should be %s, but got %s", req.Password, request.Password) + } + if !reflect.DeepEqual(req.UserName, request.UserName) { + t.Errorf("Name should be %s, but got %s", req.UserName, request.UserName) + } +} diff --git a/contrib/errortracker/sentry/README.md b/contrib/errortracker/sentry/README.md new file mode 100644 index 0000000..0af1b35 --- /dev/null +++ b/contrib/errortracker/sentry/README.md @@ -0,0 +1,64 @@ +# Sentry middleware for Kratos +This middleware helps you to catch panics and report them to [sentry](https://sentry.io/) + +## Quick Start +You could check the full demo in example folder. +```go +// Step 1: +// init sentry in the entry of your application +import "github.com/getsentry/sentry-go" + +sentry.Init(sentry.ClientOptions{ + Dsn: "", + AttachStacktrace: true, // recommended +}) + +// Step 2: +// set middleware +import ( + "context" + + ksentry "github.com/go-kratos/kratos/contrib/errortracker/sentry/v3" + "github.com/go-kratos/kratos/contrib/otel/v3/tracing" +) + +// for HTTP server, new HTTP server with sentry middleware options +var opts = []http.ServerOption{ + http.Middleware( + recovery.Recovery(), + tracing.Server(), + ksentry.Server( + ksentry.WithTags(map[string]string{ + "tag": "some-custom-constant-tag", + }), + ksentry.WithContextTags(func(ctx context.Context) map[string]string { + return map[string]string{"trace_id": tracing.TraceID(ctx)} + }), + ), // must after Recovery middleware, because of the exiting order will be reversed + logging.Server(logger), + ), +} + +// for gRPC server, new gRPC server with sentry middleware options +var opts = []grpc.ServerOption{ + grpc.Middleware( + recovery.Recovery(), + tracing.Server(), + ksentry.Server( + ksentry.WithTags(map[string]string{ + "tag": "some-custom-constant-tag", + }), + ksentry.WithContextTags(func(ctx context.Context) map[string]string { + return map[string]string{"trace_id": tracing.TraceID(ctx)} + }), + ), // must after Recovery middleware, because of the exiting order will be reversed + logging.Server(logger), + ), +} + +// Then, the framework will report events to Sentry when your trigger panics. +// Or your can push events to Sentry manually +``` + +## Reference +* [https://docs.sentry.io/platforms/go/](https://docs.sentry.io/platforms/go/) diff --git a/contrib/errortracker/sentry/go.mod b/contrib/errortracker/sentry/go.mod new file mode 100644 index 0000000..cb89939 --- /dev/null +++ b/contrib/errortracker/sentry/go.mod @@ -0,0 +1,28 @@ +module github.com/go-kratos/kratos/contrib/errortracker/sentry/v3 + +go 1.25.0 + +require ( + github.com/getsentry/sentry-go v0.46.2 + github.com/go-kratos/kratos/v3 v3.0.0 +) + +require ( + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-playground/form/v4 v4.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/errortracker/sentry/go.sum b/contrib/errortracker/sentry/go.sum new file mode 100644 index 0000000..a849f12 --- /dev/null +++ b/contrib/errortracker/sentry/go.sum @@ -0,0 +1,87 @@ +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns= +github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/errortracker/sentry/sentry.go b/contrib/errortracker/sentry/sentry.go new file mode 100644 index 0000000..1191f5e --- /dev/null +++ b/contrib/errortracker/sentry/sentry.go @@ -0,0 +1,151 @@ +package sentry + +import ( + "context" + "net" + "os" + "strings" + "time" + + "github.com/getsentry/sentry-go" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/grpc" + "github.com/go-kratos/kratos/v3/transport/http" +) + +type ctxKey struct{} + +type Option func(*options) + +type options struct { + repanic bool + waitForDelivery bool + timeout time.Duration + tags map[string]string + contextTags func(context.Context) map[string]string +} + +// WithRepanic repanic configures whether Sentry should repanic after recovery, in most cases it should be set to true. +func WithRepanic(repanic bool) Option { + return func(opts *options) { + opts.repanic = repanic + } +} + +// WithWaitForDelivery waitForDelivery configures whether you want to block the request before moving forward with the response. +func WithWaitForDelivery(waitForDelivery bool) Option { + return func(opts *options) { + opts.waitForDelivery = waitForDelivery + } +} + +// WithTimeout timeout for the event delivery requests. +func WithTimeout(timeout time.Duration) Option { + return func(opts *options) { + opts.timeout = timeout + } +} + +// WithTags configures global tags. +func WithTags(kvs map[string]string) Option { + return func(opts *options) { + opts.tags = kvs + } +} + +// WithContextTags configures tags resolved from each request context. +func WithContextTags(fn func(context.Context) map[string]string) Option { + return func(opts *options) { + opts.contextTags = fn + } +} + +// Server returns a new server middleware for Sentry. +func Server(opts ...Option) middleware.Middleware { + conf := options{repanic: true} + for _, o := range opts { + o(&conf) + } + if conf.timeout == 0 { + conf.timeout = 2 * time.Second + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + hub := GetHubFromContext(ctx) + scope := hub.Scope() + + for k, v := range conf.tags { + scope.SetTag(k, v) + } + if conf.contextTags != nil { + for k, v := range conf.contextTags(ctx) { + scope.SetTag(k, v) + } + } + + if tr, ok := transport.FromServerContext(ctx); ok { + switch tr.Kind() { + case transport.KindGRPC: + gtr := tr.(*grpc.Transport) + scope.SetContext("gRPC", map[string]any{ + "endpoint": gtr.Endpoint(), + "operation": gtr.Operation(), + }) + headers := make(map[string]any) + for _, k := range gtr.RequestHeader().Keys() { + headers[k] = gtr.RequestHeader().Get(k) + } + scope.SetContext("Headers", headers) + case transport.KindHTTP: + htr := tr.(*http.Transport) + r := htr.Request() + scope.SetRequest(r) + } + } + + ctx = context.WithValue(ctx, ctxKey{}, hub) + defer recoverWithSentry(ctx, conf, hub, req) + return handler(ctx, req) + } + } +} + +func recoverWithSentry(ctx context.Context, opts options, hub *sentry.Hub, req any) { + if err := recover(); err != nil { + if !isBrokenPipeError(err) { + eventID := hub.RecoverWithContext( + context.WithValue(ctx, sentry.RequestContextKey, req), + err, + ) + if eventID != nil && opts.waitForDelivery { + hub.Flush(opts.timeout) + } + } + if opts.repanic { + panic(err) + } + } +} + +func isBrokenPipeError(err any) bool { + if netErr, ok := err.(*net.OpError); ok { + if sysErr, ok := netErr.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(sysErr.Error()), "broken pipe") || + strings.Contains(strings.ToLower(sysErr.Error()), "connection reset by peer") { + return true + } + } + } + return false +} + +// GetHubFromContext retrieves attached *sentry.Hub instance from context or sentry. +// You can use this hub for extra information reporting +func GetHubFromContext(ctx context.Context) *sentry.Hub { + if hub, ok := ctx.Value(ctxKey{}).(*sentry.Hub); ok { + return hub + } + return sentry.CurrentHub().Clone() +} diff --git a/contrib/errortracker/sentry/sentry_test.go b/contrib/errortracker/sentry/sentry_test.go new file mode 100644 index 0000000..8fcd064 --- /dev/null +++ b/contrib/errortracker/sentry/sentry_test.go @@ -0,0 +1,65 @@ +package sentry + +import ( + "context" + "testing" + "time" +) + +func TestWithTags(t *testing.T) { + opts := new(options) + strval := "bar" + kvs := map[string]string{ + "foo": strval, + } + funcTags := WithTags(kvs) + funcTags(opts) + if opts.tags["foo"] != strval { + t.Errorf("TestWithTags() = %v, want %v", opts.tags["foo"], strval) + } +} + +func TestWithContextTags(t *testing.T) { + opts := new(options) + fn := func(context.Context) map[string]string { + return map[string]string{"foo": "bar"} + } + funcTags := WithContextTags(fn) + funcTags(opts) + if opts.contextTags == nil { + t.Fatal("contextTags is nil") + } + if got := opts.contextTags(context.Background())["foo"]; got != "bar" { + t.Errorf("TestWithContextTags() = %v, want %v", got, "bar") + } +} + +func TestWithRepanic(t *testing.T) { + opts := new(options) + val := true + f := WithRepanic(val) + f(opts) + if opts.repanic != val { + t.Errorf("TestWithRepanic() = %v, want %v", opts.repanic, val) + } +} + +func TestWithWaitForDelivery(t *testing.T) { + opts := new(options) + val := true + f := WithWaitForDelivery(val) + f(opts) + if opts.waitForDelivery != val { + t.Errorf("TestWithWaitForDelivery() = %v, want %v", opts.waitForDelivery, val) + } +} + +func TestWithTimeout(t *testing.T) { + opts := new(options) + val := time.Second * 10 + f := WithTimeout(val) + f(opts) + if opts.timeout != val { + t.Errorf("TestWithTimeout() = %v, want %v", opts.timeout, val) + } +} diff --git a/contrib/middleware/jwt/go.mod b/contrib/middleware/jwt/go.mod new file mode 100644 index 0000000..6c6b1d3 --- /dev/null +++ b/contrib/middleware/jwt/go.mod @@ -0,0 +1,20 @@ +module github.com/go-kratos/kratos/contrib/middleware/jwt/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/golang-jwt/jwt/v5 v5.3.1 +) + +require ( + github.com/go-playground/form/v4 v4.3.0 // indirect + github.com/kr/text v0.2.0 // indirect + golang.org/x/sys v0.44.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../.. diff --git a/contrib/middleware/jwt/go.sum b/contrib/middleware/jwt/go.sum new file mode 100644 index 0000000..c09a89f --- /dev/null +++ b/contrib/middleware/jwt/go.sum @@ -0,0 +1,34 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/middleware/jwt/jwt.go b/contrib/middleware/jwt/jwt.go new file mode 100644 index 0000000..aaa32be --- /dev/null +++ b/contrib/middleware/jwt/jwt.go @@ -0,0 +1,187 @@ +package jwt + +import ( + "context" + "fmt" + "strings" + + "github.com/golang-jwt/jwt/v5" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +type authKey struct{} + +const ( + + // bearerWord the bearer key word for authorization + bearerWord string = "Bearer" + + // bearerFormat authorization token format + bearerFormat string = "Bearer %s" + + // authorizationKey holds the key used to store the JWT Token in the request tokenHeader. + authorizationKey string = "Authorization" + + // reason holds the error reason. + reason string = "UNAUTHORIZED" +) + +var ( + ErrMissingJwtToken = errors.Unauthorized(reason, "JWT token is missing") + ErrMissingKeyFunc = errors.Unauthorized(reason, "keyFunc is missing") + ErrTokenInvalid = errors.Unauthorized(reason, "Token is invalid") + ErrTokenExpired = errors.Unauthorized(reason, "JWT token has expired") + ErrTokenParseFail = errors.Unauthorized(reason, "Fail to parse JWT token ") + ErrUnSupportSigningMethod = errors.Unauthorized(reason, "Wrong signing method") + ErrWrongContext = errors.Unauthorized(reason, "Wrong context for middleware") + ErrNeedTokenProvider = errors.Unauthorized(reason, "Token provider is missing") + ErrSignToken = errors.Unauthorized(reason, "Can not sign token.Is the key correct?") + ErrGetKey = errors.Unauthorized(reason, "Can not get key while signing token") +) + +// Option is jwt option. +type Option func(*options) + +// Parser is a jwt parser +type options struct { + signingMethod jwt.SigningMethod + claims func() jwt.Claims + tokenHeader map[string]any + parserOptions []jwt.ParserOption +} + +// WithSigningMethod with signing method option. +func WithSigningMethod(method jwt.SigningMethod) Option { + return func(o *options) { + o.signingMethod = method + } +} + +// WithClaims with customer claim +// If you use it in Server, f needs to return a new jwt.Claims object each time to avoid concurrent write problems +// If you use it in Client, f only needs to return a single object to provide performance +func WithClaims(f func() jwt.Claims) Option { + return func(o *options) { + o.claims = f + } +} + +// WithTokenHeader withe customer tokenHeader for client side +func WithTokenHeader(header map[string]any) Option { + return func(o *options) { + o.tokenHeader = header + } +} + +// WithParserOptions with custom parser options for the jwt parser. +// It allows customization of the jwt.Parser used during token validation, +// for example, to enforce mandatory claim validations such as issuer, subject, or expiration. +func WithParserOptions(opts ...jwt.ParserOption) Option { + return func(o *options) { + o.parserOptions = opts + } +} + +// Server is a server auth middleware. Check the token and extract the info from token. +func Server(keyFunc jwt.Keyfunc, opts ...Option) middleware.Middleware { + o := &options{ + signingMethod: jwt.SigningMethodHS256, + } + for _, opt := range opts { + opt(o) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + if header, ok := transport.FromServerContext(ctx); ok { + if keyFunc == nil { + return nil, ErrMissingKeyFunc + } + auths := strings.SplitN(header.RequestHeader().Get(authorizationKey), " ", 2) + if len(auths) != 2 || !strings.EqualFold(auths[0], bearerWord) { + return nil, ErrMissingJwtToken + } + jwtToken := auths[1] + var ( + tokenInfo *jwt.Token + err error + ) + if o.claims != nil { + tokenInfo, err = jwt.ParseWithClaims(jwtToken, o.claims(), keyFunc, o.parserOptions...) + } else { + tokenInfo, err = jwt.Parse(jwtToken, keyFunc, o.parserOptions...) + } + if err != nil { + if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) { + return nil, ErrTokenInvalid + } + if errors.Is(err, jwt.ErrTokenNotValidYet) || errors.Is(err, jwt.ErrTokenExpired) { + return nil, ErrTokenExpired + } + return nil, ErrTokenParseFail + } + + if !tokenInfo.Valid { + return nil, ErrTokenInvalid + } + if tokenInfo.Method != o.signingMethod { + return nil, ErrUnSupportSigningMethod + } + ctx = NewContext(ctx, tokenInfo.Claims) + return handler(ctx, req) + } + return nil, ErrWrongContext + } + } +} + +// Client is a client jwt middleware. +func Client(keyProvider jwt.Keyfunc, opts ...Option) middleware.Middleware { + claims := jwt.RegisteredClaims{} + o := &options{ + signingMethod: jwt.SigningMethodHS256, + claims: func() jwt.Claims { return claims }, + } + for _, opt := range opts { + opt(o) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + if keyProvider == nil { + return nil, ErrNeedTokenProvider + } + token := jwt.NewWithClaims(o.signingMethod, o.claims()) + if o.tokenHeader != nil { + for k, v := range o.tokenHeader { + token.Header[k] = v + } + } + key, err := keyProvider(token) + if err != nil { + return nil, ErrGetKey + } + tokenStr, err := token.SignedString(key) + if err != nil { + return nil, ErrSignToken + } + if clientContext, ok := transport.FromClientContext(ctx); ok { + clientContext.RequestHeader().Set(authorizationKey, fmt.Sprintf(bearerFormat, tokenStr)) + return handler(ctx, req) + } + return nil, ErrWrongContext + } + } +} + +// NewContext put auth info into context +func NewContext(ctx context.Context, info jwt.Claims) context.Context { + return context.WithValue(ctx, authKey{}, info) +} + +// FromContext extract auth info from context +func FromContext(ctx context.Context) (token jwt.Claims, ok bool) { + token, ok = ctx.Value(authKey{}).(jwt.Claims) + return +} diff --git a/contrib/middleware/jwt/jwt_test.go b/contrib/middleware/jwt/jwt_test.go new file mode 100644 index 0000000..a6d1d60 --- /dev/null +++ b/contrib/middleware/jwt/jwt_test.go @@ -0,0 +1,759 @@ +package jwt + +import ( + "context" + "errors" + "fmt" + "math/rand/v2" + "net/http" + "reflect" + "strconv" + "sync" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +type headerCarrier http.Header + +func (hc headerCarrier) Get(key string) string { return http.Header(hc).Get(key) } + +func (hc headerCarrier) Set(key string, value string) { http.Header(hc).Set(key, value) } + +func (hc headerCarrier) Add(key string, value string) { http.Header(hc).Add(key, value) } + +// Keys lists the keys stored in this carrier. +func (hc headerCarrier) Keys() []string { + keys := make([]string, 0, len(hc)) + for k := range http.Header(hc) { + keys = append(keys, k) + } + return keys +} + +// Values returns a slice value associated with the passed key. +func (hc headerCarrier) Values(key string) []string { + return http.Header(hc).Values(key) +} + +func newTokenHeader(headerKey string, token string) *headerCarrier { + header := &headerCarrier{} + header.Set(headerKey, token) + return header +} + +type Transport struct { + kind transport.Kind + endpoint string + operation string + reqHeader transport.Header +} + +func (tr *Transport) Kind() transport.Kind { + return tr.kind +} + +func (tr *Transport) Endpoint() string { + return tr.endpoint +} + +func (tr *Transport) Operation() string { + return tr.operation +} + +func (tr *Transport) RequestHeader() transport.Header { + return tr.reqHeader +} + +func (tr *Transport) ReplyHeader() transport.Header { + return nil +} + +type CustomerClaims struct { + Name string `json:"name"` + jwt.RegisteredClaims +} + +func TestJWTServerParse(t *testing.T) { + var ( + errConcurrentWrite = errors.New("concurrent write claims") + errParseClaims = errors.New("bad result, token claims is not CustomerClaims") + ) + + testKey := "testKey" + tests := []struct { + name string + token func() string + claims func() jwt.Claims + exceptErr error + key string + goroutineNum int + }{ + { + name: "normal", + token: func() string { + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &CustomerClaims{}).SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + return fmt.Sprintf(bearerFormat, token) + }, + claims: func() jwt.Claims { + return &CustomerClaims{} + }, + exceptErr: nil, + key: testKey, + goroutineNum: 1, + }, + { + name: "concurrent request", + token: func() string { + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &CustomerClaims{ + Name: strconv.Itoa(rand.Int()), + }).SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + return fmt.Sprintf(bearerFormat, token) + }, + claims: func() jwt.Claims { + return &CustomerClaims{} + }, + exceptErr: nil, + key: testKey, + goroutineNum: 10000, + }, + } + + next := func(ctx context.Context, _ any) (any, error) { + testToken, _ := FromContext(ctx) + var name string + if customerClaims, ok := testToken.(*CustomerClaims); ok { + name = customerClaims.Name + } else { + return nil, errParseClaims + } + + // mock biz + time.Sleep(100 * time.Millisecond) + + if customerClaims, ok := testToken.(*CustomerClaims); ok { + if name != customerClaims.Name { + return nil, errConcurrentWrite + } + } else { + return nil, errParseClaims + } + return "reply", nil + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server := Server( + func(*jwt.Token) (any, error) { return []byte(testKey), nil }, + WithClaims(test.claims), + )(next) + wg := sync.WaitGroup{} + for i := 0; i < test.goroutineNum; i++ { + wg.Add(1) + go func() { + defer wg.Done() + ctx := transport.NewServerContext(context.Background(), &Transport{reqHeader: newTokenHeader(authorizationKey, test.token())}) + _, err2 := server(ctx, test.name) + if !errors.Is(test.exceptErr, err2) { + t.Errorf("except error %v, but got %v", test.exceptErr, err2) + } + }() + } + wg.Wait() + }) + } +} + +func TestServer(t *testing.T) { + testKey := "testKey" + mapClaims := jwt.MapClaims{} + mapClaims["name"] = "xiaoli" + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims) + token, err := claims.SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + token = fmt.Sprintf(bearerFormat, token) + tests := []struct { + name string + ctx context.Context + signingMethod jwt.SigningMethod + exceptErr error + key string + }{ + { + name: "normal", + ctx: transport.NewServerContext(context.Background(), &Transport{reqHeader: newTokenHeader(authorizationKey, token)}), + signingMethod: jwt.SigningMethodHS256, + exceptErr: nil, + key: testKey, + }, + { + name: "miss token", + ctx: transport.NewServerContext(context.Background(), &Transport{reqHeader: headerCarrier{}}), + signingMethod: jwt.SigningMethodHS256, + exceptErr: ErrMissingJwtToken, + key: testKey, + }, + { + name: "token invalid", + ctx: transport.NewServerContext(context.Background(), &Transport{ + reqHeader: newTokenHeader(authorizationKey, fmt.Sprintf(bearerFormat, "12313123")), + }), + signingMethod: jwt.SigningMethodHS256, + exceptErr: ErrTokenInvalid, + key: testKey, + }, + { + name: "method invalid", + ctx: transport.NewServerContext(context.Background(), &Transport{reqHeader: newTokenHeader(authorizationKey, token)}), + signingMethod: jwt.SigningMethodES384, + exceptErr: ErrUnSupportSigningMethod, + key: testKey, + }, + { + name: "miss signing method", + ctx: transport.NewServerContext(context.Background(), &Transport{reqHeader: newTokenHeader(authorizationKey, token)}), + signingMethod: nil, + exceptErr: nil, + key: testKey, + }, + { + name: "miss signing method", + ctx: transport.NewServerContext(context.Background(), &Transport{reqHeader: newTokenHeader(authorizationKey, token)}), + signingMethod: nil, + exceptErr: nil, + key: testKey, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var testToken jwt.Claims + next := func(ctx context.Context, req any) (any, error) { + t.Log(req) + testToken, _ = FromContext(ctx) + return "reply", nil + } + var server middleware.Handler + if test.signingMethod != nil { + server = Server(func(*jwt.Token) (any, error) { + return []byte(test.key), nil + }, WithSigningMethod(test.signingMethod))(next) + } else { + server = Server(func(*jwt.Token) (any, error) { + return []byte(test.key), nil + })(next) + } + _, err2 := server(test.ctx, test.name) + if !errors.Is(test.exceptErr, err2) { + t.Errorf("except error %v, but got %v", test.exceptErr, err2) + } + if test.exceptErr == nil { + if testToken == nil { + t.Fatal("except testToken not nil, but got nil") + } + _, ok := testToken.(jwt.MapClaims) + if !ok { + t.Errorf("except testToken is jwt.MapClaims, but got %T", testToken) + } + } + }) + } +} + +func TestClient(t *testing.T) { + testKey := "testKey" + + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{}) + token, err := claims.SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + tProvider := func(*jwt.Token) (any, error) { + return []byte(testKey), nil + } + tests := []struct { + name string + expectError error + tokenProvider jwt.Keyfunc + }{ + { + name: "normal", + expectError: nil, + tokenProvider: tProvider, + }, + { + name: "miss token provider", + expectError: ErrNeedTokenProvider, + tokenProvider: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + next := func(context.Context, any) (any, error) { + return "reply", nil + } + handler := Client(test.tokenProvider)(next) + header := &headerCarrier{} + _, err2 := handler(transport.NewClientContext(context.Background(), &Transport{reqHeader: header}), "ok") + if !errors.Is(test.expectError, err2) { + t.Errorf("except error %v, but got %v", test.expectError, err2) + } + if err2 == nil { + if !reflect.DeepEqual(header.Get(authorizationKey), fmt.Sprintf(bearerFormat, token)) { + t.Errorf("except header %s, but got %s", fmt.Sprintf(bearerFormat, token), header.Get(authorizationKey)) + } + } + }) + } +} + +func TestTokenExpire(t *testing.T) { + testKey := "testKey" + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Millisecond)), + }) + token, err := claims.SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + token = fmt.Sprintf(bearerFormat, token) + time.Sleep(time.Second) + next := func(_ context.Context, req any) (any, error) { + t.Log(req) + return "reply", nil + } + ctx := transport.NewServerContext(context.Background(), &Transport{reqHeader: newTokenHeader(authorizationKey, token)}) + server := Server(func(*jwt.Token) (any, error) { + return []byte(testKey), nil + }, WithSigningMethod(jwt.SigningMethodHS256))(next) + _, err2 := server(ctx, "test expire token") + if !errors.Is(ErrTokenExpired, err2) { + t.Errorf("except error %v, but got %v", ErrTokenExpired, err2) + } +} + +func TestMissingKeyFunc(t *testing.T) { + testKey := "testKey" + mapClaims := jwt.MapClaims{} + mapClaims["name"] = "xiaoli" + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims) + token, err := claims.SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + token = fmt.Sprintf(bearerFormat, token) + test := struct { + name string + ctx context.Context + signingMethod jwt.SigningMethod + exceptErr error + key string + }{ + name: "miss key", + ctx: transport.NewServerContext(context.Background(), &Transport{reqHeader: newTokenHeader(authorizationKey, token)}), + signingMethod: jwt.SigningMethodHS256, + exceptErr: ErrMissingKeyFunc, + key: "", + } + + var testToken jwt.Claims + next := func(ctx context.Context, req any) (any, error) { + t.Log(req) + testToken, _ = FromContext(ctx) + return "reply", nil + } + server := Server(nil)(next) + _, err2 := server(test.ctx, test.name) + if !errors.Is(test.exceptErr, err2) { + t.Errorf("except error %v, but got %v", test.exceptErr, err2) + } + if test.exceptErr == nil { + if testToken == nil { + t.Errorf("except testToken not nil, but got nil") + } + } +} + +func TestClientWithClaims(t *testing.T) { + testKey := "testKey" + mapClaims := jwt.MapClaims{} + mapClaims["name"] = "xiaoli" + mapClaimsFunc := func() jwt.Claims { return mapClaims } + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims) + token, err := claims.SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + tProvider := func(*jwt.Token) (any, error) { + return []byte(testKey), nil + } + test := struct { + name string + expectError error + tokenProvider jwt.Keyfunc + }{ + name: "normal", + expectError: nil, + tokenProvider: tProvider, + } + + t.Run(test.name, func(t *testing.T) { + next := func(context.Context, any) (any, error) { + return "reply", nil + } + handler := Client(test.tokenProvider, WithClaims(mapClaimsFunc))(next) + header := &headerCarrier{} + _, err2 := handler(transport.NewClientContext(context.Background(), &Transport{reqHeader: header}), "ok") + if !errors.Is(test.expectError, err2) { + t.Errorf("except error %v, but got %v", test.expectError, err2) + } + if err2 == nil { + if !reflect.DeepEqual(header.Get(authorizationKey), fmt.Sprintf(bearerFormat, token)) { + t.Errorf("except header %s, but got %s", fmt.Sprintf(bearerFormat, token), header.Get(authorizationKey)) + } + } + }) +} + +func TestClientWithHeader(t *testing.T) { + testKey := "testKey" + mapClaims := jwt.MapClaims{} + mapClaims["name"] = "xiaoli" + mapClaimsFunc := func() jwt.Claims { return mapClaims } + tokenHeader := map[string]any{ + "test": "test", + } + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims) + for k, v := range tokenHeader { + claims.Header[k] = v + } + token, err := claims.SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + tProvider := func(*jwt.Token) (any, error) { + return []byte(testKey), nil + } + next := func(context.Context, any) (any, error) { + return "reply", nil + } + handler := Client(tProvider, WithClaims(mapClaimsFunc), WithTokenHeader(tokenHeader))(next) + header := &headerCarrier{} + _, err2 := handler(transport.NewClientContext(context.Background(), &Transport{reqHeader: header}), "ok") + if err2 != nil { + t.Errorf("except error nil, but got %v", err2) + } + if !reflect.DeepEqual(header.Get(authorizationKey), fmt.Sprintf(bearerFormat, token)) { + t.Errorf("except header %s, but got %s", fmt.Sprintf(bearerFormat, token), header.Get(authorizationKey)) + } +} + +func TestClientMissKey(t *testing.T) { + testKey := "testKey" + mapClaims := jwt.MapClaims{} + mapClaims["name"] = "xiaoli" + mapClaimsFunc := func() jwt.Claims { return mapClaims } + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, mapClaims) + token, err := claims.SignedString([]byte(testKey)) + if err != nil { + panic(err) + } + tProvider := func(*jwt.Token) (any, error) { + return nil, errors.New("some error") + } + test := struct { + name string + expectError error + tokenProvider jwt.Keyfunc + }{ + name: "normal", + expectError: ErrGetKey, + tokenProvider: tProvider, + } + + t.Run(test.name, func(t *testing.T) { + next := func(context.Context, any) (any, error) { + return "reply", nil + } + handler := Client(test.tokenProvider, WithClaims(mapClaimsFunc))(next) + header := &headerCarrier{} + _, err2 := handler(transport.NewClientContext(context.Background(), &Transport{reqHeader: header}), "ok") + if !errors.Is(test.expectError, err2) { + t.Errorf("except error %v, but got %v", test.expectError, err2) + } + if err2 == nil { + if !reflect.DeepEqual(header.Get(authorizationKey), fmt.Sprintf(bearerFormat, token)) { + t.Errorf("except header %s, but got %s", fmt.Sprintf(bearerFormat, token), header.Get(authorizationKey)) + } + } + }) +} + +func TestNewContextAndFromContext(t *testing.T) { + tests := []struct { + name string + claims jwt.MapClaims + }{ + {"val not nil", jwt.MapClaims{"name": "kratos"}}, + {"val nil", nil}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := NewContext(context.Background(), test.claims) + + claims, ok := FromContext(ctx) + if !ok { + t.Fatal("ctx not found authKey{}") + } + if !reflect.DeepEqual(test.claims, claims) { + t.Errorf(`want: %s, got: %v`, test.claims, claims) + } + }) + } +} + +func TestWithParserOptions(t *testing.T) { + testKey := "testKey" + issuer := "https://example.com" + subject := "user" + + next := func(ctx context.Context, _ any) (any, error) { + testToken, _ := FromContext(ctx) + if testToken == nil { + t.Error("expected testToken not nil, but got nil") + } + return "reply", nil + } + + tests := []struct { + name string + claims jwt.RegisteredClaims + parserOptions []jwt.ParserOption + customClaims func() jwt.Claims + exceptErr error + }{ + { + name: "valid token with matching issuer", + claims: jwt.RegisteredClaims{ + Issuer: issuer, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: []jwt.ParserOption{ + jwt.WithIssuer(issuer), + }, + exceptErr: nil, + }, + { + name: "invalid token with wrong issuer", + claims: jwt.RegisteredClaims{ + Issuer: "https://wrong-issuer.com", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: []jwt.ParserOption{ + jwt.WithIssuer(issuer), + }, + exceptErr: ErrTokenParseFail, + }, + { + name: "valid token with matching subject", + claims: jwt.RegisteredClaims{ + Subject: subject, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: []jwt.ParserOption{ + jwt.WithSubject(subject), + }, + exceptErr: nil, + }, + { + name: "invalid token with wrong subject", + claims: jwt.RegisteredClaims{ + Subject: "admin", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: []jwt.ParserOption{ + jwt.WithSubject(subject), + }, + exceptErr: ErrTokenParseFail, + }, + { + name: "valid token with multiple parser options", + claims: jwt.RegisteredClaims{ + Issuer: issuer, + Subject: subject, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: []jwt.ParserOption{ + jwt.WithIssuer(issuer), + jwt.WithSubject(subject), + jwt.WithExpirationRequired(), + }, + exceptErr: nil, + }, + { + name: "invalid token missing required expiration", + claims: jwt.RegisteredClaims{ + Issuer: issuer, + Subject: subject, + }, + parserOptions: []jwt.ParserOption{ + jwt.WithExpirationRequired(), + }, + exceptErr: ErrTokenParseFail, + }, + { + name: "valid token with no parser options (backward compatibility)", + claims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: nil, + exceptErr: nil, + }, + { + name: "valid token with custom claims and matching issuer", + claims: jwt.RegisteredClaims{ + Issuer: issuer, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: []jwt.ParserOption{ + jwt.WithIssuer(issuer), + }, + customClaims: func() jwt.Claims { + return &CustomerClaims{} + }, + exceptErr: nil, + }, + { + name: "invalid token with custom claims and wrong issuer", + claims: jwt.RegisteredClaims{ + Issuer: "https://wrong-issuer.com", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + parserOptions: []jwt.ParserOption{ + jwt.WithIssuer(issuer), + }, + customClaims: func() jwt.Claims { + return &CustomerClaims{} + }, + exceptErr: ErrTokenParseFail, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, test.claims).SignedString([]byte(testKey)) + if err != nil { + t.Fatal(err) + } + ctx := transport.NewServerContext(context.Background(), &Transport{ + reqHeader: newTokenHeader(authorizationKey, fmt.Sprintf(bearerFormat, token)), + }) + + opts := []Option{WithSigningMethod(jwt.SigningMethodHS256)} + if test.parserOptions != nil { + opts = append(opts, WithParserOptions(test.parserOptions...)) + } + if test.customClaims != nil { + opts = append(opts, WithClaims(test.customClaims)) + } + + server := Server( + func(*jwt.Token) (any, error) { return []byte(testKey), nil }, + opts..., + )(next) + + _, err2 := server(ctx, test.name) + if !errors.Is(test.exceptErr, err2) { + t.Errorf("expected error %v, but got %v", test.exceptErr, err2) + } + }) + } +} + +func TestWithParserOptionsConcurrent(t *testing.T) { + testKey := "testKey" + issuer := "https://example.com" + + next := func(ctx context.Context, _ any) (any, error) { + testToken, _ := FromContext(ctx) + if testToken == nil { + return nil, errors.New("expected testToken not nil, but got nil") + } + return "reply", nil + } + + server := Server( + func(*jwt.Token) (any, error) { return []byte(testKey), nil }, + WithClaims(func() jwt.Claims { return &CustomerClaims{} }), + WithParserOptions(jwt.WithIssuer(issuer), jwt.WithExpirationRequired()), + )(next) + + wg := sync.WaitGroup{} + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &CustomerClaims{ + Name: strconv.Itoa(rand.Int()), + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: issuer, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + }).SignedString([]byte(testKey)) + if err != nil { + t.Error(err) + return + } + ctx := transport.NewServerContext(context.Background(), &Transport{ + reqHeader: newTokenHeader(authorizationKey, fmt.Sprintf(bearerFormat, token)), + }) + _, err2 := server(ctx, "concurrent") + if err2 != nil { + t.Errorf("expected nil error, but got %v", err2) + } + }() + } + wg.Wait() +} + +func TestWithParserOptionsEmpty(t *testing.T) { + testKey := "testKey" + + next := func(_ context.Context, _ any) (any, error) { + return "reply", nil + } + + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "name": "kratos", + }).SignedString([]byte(testKey)) + if err != nil { + t.Fatal(err) + } + + ctx := transport.NewServerContext(context.Background(), &Transport{ + reqHeader: newTokenHeader(authorizationKey, fmt.Sprintf(bearerFormat, token)), + }) + + // WithParserOptions called with no arguments should behave identically to not calling it. + server := Server( + func(*jwt.Token) (any, error) { return []byte(testKey), nil }, + WithParserOptions(), + )(next) + + _, err2 := server(ctx, "empty parser options") + if err2 != nil { + t.Errorf("expected nil error, but got %v", err2) + } +} diff --git a/contrib/middleware/validate/README.md b/contrib/middleware/validate/README.md new file mode 100644 index 0000000..db61413 --- /dev/null +++ b/contrib/middleware/validate/README.md @@ -0,0 +1,15 @@ +# Validator Middleware for Kratos Project + +This module provides a middleware for Kratos to validate request parameters, using schema defined in `.proto` files. + +There used to be a middleware named `Validator` in Kratos, which calls the generated validation functions +from [PGV](https://github.com/bufbuild/protoc-gen-validate) at runtime. Since PGV has been +in [maintenance](https://github.com/bufbuild/protoc-gen-validate/commit/4a8ffc4942463929c4289407cd4b8c8328ff5422), and +recommend using [protovalidate](https://github.com/bufbuild/protovalidate) as an alternative. + +That's why we provide a new middleware that uses the schema definitions and validation functions provided by +protovalidate. + +protovalidate no longer requires code generation at build time, but for compatibility with existing Kratos +projects, we enable the legacy mode of protovalidate. For most users, no changes are needed to existing code. **But for +users who have manually implemented the Validator interface, you need to migrate the relevant implementation yourself**. diff --git a/contrib/middleware/validate/go.mod b/contrib/middleware/validate/go.mod new file mode 100644 index 0000000..75d8e2c --- /dev/null +++ b/contrib/middleware/validate/go.mod @@ -0,0 +1,27 @@ +module github.com/go-kratos/kratos/contrib/middleware/validate/v3 + +go 1.25.0 + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 + github.com/envoyproxy/protoc-gen-validate v1.3.3 + github.com/go-kratos/kratos/v3 v3.0.0 + google.golang.org/protobuf v1.36.11 +) + +require go.yaml.in/yaml/v3 v3.0.4 // indirect + +require ( + buf.build/go/protovalidate v1.2.0 + cel.dev/expr v0.25.2 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/google/cel-go v0.28.1 // indirect + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/middleware/validate/go.sum b/contrib/middleware/validate/go.sum new file mode 100644 index 0000000..7e9fba1 --- /dev/null +++ b/contrib/middleware/validate/go.sum @@ -0,0 +1,55 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0= +buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +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/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= +github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/middleware/validate/internal/testdata/generate.go b/contrib/middleware/validate/internal/testdata/generate.go new file mode 100644 index 0000000..73edd68 --- /dev/null +++ b/contrib/middleware/validate/internal/testdata/generate.go @@ -0,0 +1,3 @@ +package testdata + +//go:generate protoc -I . -I ../../../../../third_party --go_out=paths=source_relative:. --validate_out=paths=source_relative,lang=go:. ./test.proto diff --git a/contrib/middleware/validate/internal/testdata/test.pb.go b/contrib/middleware/validate/internal/testdata/test.pb.go new file mode 100644 index 0000000..95edd59 --- /dev/null +++ b/contrib/middleware/validate/internal/testdata/test.pb.go @@ -0,0 +1,258 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.4 +// protoc v5.29.3 +// source: test.proto + +package testdata + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + _ "github.com/envoyproxy/protoc-gen-validate/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Legacy struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Legacy) Reset() { + *x = Legacy{} + mi := &file_test_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Legacy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Legacy) ProtoMessage() {} + +func (x *Legacy) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Legacy.ProtoReflect.Descriptor instead. +func (*Legacy) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{0} +} + +func (x *Legacy) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Legacy) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +type Mixed struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Mixed) Reset() { + *x = Mixed{} + mi := &file_test_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Mixed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Mixed) ProtoMessage() {} + +func (x *Mixed) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Mixed.ProtoReflect.Descriptor instead. +func (*Mixed) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{1} +} + +func (x *Mixed) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Mixed) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +type Modern struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Modern) Reset() { + *x = Modern{} + mi := &file_test_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Modern) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Modern) ProtoMessage() {} + +func (x *Modern) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Modern.ProtoReflect.Descriptor instead. +func (*Modern) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{2} +} + +func (x *Modern) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Modern) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +var File_test_proto protoreflect.FileDescriptor + +var file_test_proto_rawDesc = string([]byte{ + 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x74, 0x65, + 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x40, 0x0a, 0x06, + 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x05, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x42, 0x07, 0xfa, 0x42, 0x04, 0x1a, 0x02, 0x20, 0x12, 0x52, 0x03, 0x61, 0x67, 0x65, 0x22, 0x3f, + 0x0a, 0x05, 0x4d, 0x69, 0x78, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x05, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x1a, 0x02, 0x20, 0x12, 0x52, 0x03, 0x61, 0x67, 0x65, 0x22, + 0x40, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x65, 0x72, 0x6e, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x05, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x42, 0x07, 0xba, 0x48, 0x04, 0x1a, 0x02, 0x20, 0x12, 0x52, 0x03, 0x61, 0x67, + 0x65, 0x42, 0x4b, 0x5a, 0x49, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, + 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x69, 0x62, 0x2f, 0x6d, 0x69, 0x64, 0x64, 0x6c, 0x65, 0x77, + 0x61, 0x72, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_test_proto_rawDescOnce sync.Once + file_test_proto_rawDescData []byte +) + +func file_test_proto_rawDescGZIP() []byte { + file_test_proto_rawDescOnce.Do(func() { + file_test_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_test_proto_rawDesc), len(file_test_proto_rawDesc))) + }) + return file_test_proto_rawDescData +} + +var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_test_proto_goTypes = []any{ + (*Legacy)(nil), // 0: testdata.Legacy + (*Mixed)(nil), // 1: testdata.Mixed + (*Modern)(nil), // 2: testdata.Modern +} +var file_test_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_test_proto_init() } +func file_test_proto_init() { + if File_test_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_test_proto_rawDesc), len(file_test_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_test_proto_goTypes, + DependencyIndexes: file_test_proto_depIdxs, + MessageInfos: file_test_proto_msgTypes, + }.Build() + File_test_proto = out.File + file_test_proto_goTypes = nil + file_test_proto_depIdxs = nil +} diff --git a/contrib/middleware/validate/internal/testdata/test.pb.validate.go b/contrib/middleware/validate/internal/testdata/test.pb.validate.go new file mode 100644 index 0000000..6001d39 --- /dev/null +++ b/contrib/middleware/validate/internal/testdata/test.pb.validate.go @@ -0,0 +1,369 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: test.proto + +package testdata + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on Legacy with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *Legacy) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Legacy with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in LegacyMultiError, or nil if none found. +func (m *Legacy) ValidateAll() error { + return m.validate(true) +} + +func (m *Legacy) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if utf8.RuneCountInString(m.GetName()) < 5 { + err := LegacyValidationError{ + field: "Name", + reason: "value length must be at least 5 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if m.GetAge() <= 18 { + err := LegacyValidationError{ + field: "Age", + reason: "value must be greater than 18", + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return LegacyMultiError(errors) + } + + return nil +} + +// LegacyMultiError is an error wrapping multiple validation errors returned by +// Legacy.ValidateAll() if the designated constraints aren't met. +type LegacyMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m LegacyMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m LegacyMultiError) AllErrors() []error { return m } + +// LegacyValidationError is the validation error returned by Legacy.Validate if +// the designated constraints aren't met. +type LegacyValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e LegacyValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e LegacyValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e LegacyValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e LegacyValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e LegacyValidationError) ErrorName() string { return "LegacyValidationError" } + +// Error satisfies the builtin error interface +func (e LegacyValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sLegacy.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = LegacyValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = LegacyValidationError{} + +// Validate checks the field values on Mixed with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *Mixed) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Mixed with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in MixedMultiError, or nil if none found. +func (m *Mixed) ValidateAll() error { + return m.validate(true) +} + +func (m *Mixed) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + if m.GetAge() <= 18 { + err := MixedValidationError{ + field: "Age", + reason: "value must be greater than 18", + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return MixedMultiError(errors) + } + + return nil +} + +// MixedMultiError is an error wrapping multiple validation errors returned by +// Mixed.ValidateAll() if the designated constraints aren't met. +type MixedMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m MixedMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m MixedMultiError) AllErrors() []error { return m } + +// MixedValidationError is the validation error returned by Mixed.Validate if +// the designated constraints aren't met. +type MixedValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e MixedValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e MixedValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e MixedValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e MixedValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e MixedValidationError) ErrorName() string { return "MixedValidationError" } + +// Error satisfies the builtin error interface +func (e MixedValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sMixed.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = MixedValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = MixedValidationError{} + +// Validate checks the field values on Modern with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *Modern) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Modern with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in ModernMultiError, or nil if none found. +func (m *Modern) ValidateAll() error { + return m.validate(true) +} + +func (m *Modern) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + // no validation rules for Age + + if len(errors) > 0 { + return ModernMultiError(errors) + } + + return nil +} + +// ModernMultiError is an error wrapping multiple validation errors returned by +// Modern.ValidateAll() if the designated constraints aren't met. +type ModernMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ModernMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ModernMultiError) AllErrors() []error { return m } + +// ModernValidationError is the validation error returned by Modern.Validate if +// the designated constraints aren't met. +type ModernValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ModernValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ModernValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ModernValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ModernValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ModernValidationError) ErrorName() string { return "ModernValidationError" } + +// Error satisfies the builtin error interface +func (e ModernValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sModern.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ModernValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ModernValidationError{} diff --git a/contrib/middleware/validate/internal/testdata/test.proto b/contrib/middleware/validate/internal/testdata/test.proto new file mode 100644 index 0000000..8e4b82b --- /dev/null +++ b/contrib/middleware/validate/internal/testdata/test.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package testdata; + +import "buf/validate/validate.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/go-kratos/kratos/contrib/middleware/validate/internal/testdata"; + +message Legacy { + string name = 1 [(validate.rules).string.min_len = 5]; + int32 age = 2 [(validate.rules).int32.gt = 18]; +} + +message Mixed { + string name = 1 [(buf.validate.field).string.min_len = 5]; + int32 age = 2 [(validate.rules).int32.gt = 18]; +} + +message Modern { + string name = 1 [(buf.validate.field).string.min_len = 5]; + int32 age = 2 [(buf.validate.field).int32.gt = 18]; +} diff --git a/contrib/middleware/validate/validate.go b/contrib/middleware/validate/validate.go new file mode 100644 index 0000000..7a1d43f --- /dev/null +++ b/contrib/middleware/validate/validate.go @@ -0,0 +1,37 @@ +package validate + +import ( + "context" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" + + "buf.build/go/protovalidate" + "google.golang.org/protobuf/proto" +) + +type validator interface { + Validate() error +} + +// ProtoValidate is a middleware that validates the request message with [protovalidate](https://github.com/bufbuild/protovalidate) +func ProtoValidate() middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if msg, ok := req.(proto.Message); ok { + if err := protovalidate.Validate(msg); err != nil { + return nil, errors.BadRequest("VALIDATOR", err.Error()).WithCause(err) + } + } + + // to compatible with the [old validator](https://github.com/envoyproxy/protoc-gen-validate) + if v, ok := req.(validator); ok { + if err := v.Validate(); err != nil { + return nil, errors.BadRequest("VALIDATOR", err.Error()).WithCause(err) + } + } + + return handler(ctx, req) + } + } +} diff --git a/contrib/middleware/validate/validate_test.go b/contrib/middleware/validate/validate_test.go new file mode 100644 index 0000000..53f289d --- /dev/null +++ b/contrib/middleware/validate/validate_test.go @@ -0,0 +1,82 @@ +package validate + +import ( + "context" + "testing" + + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/contrib/middleware/validate/v3/internal/testdata" + kerrors "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" +) + +type testcase struct { + name string + req proto.Message + err bool +} + +func TestTable(t *testing.T) { + var mock middleware.Handler = func(context.Context, any) (any, error) { return nil, nil } + + tests := []testcase{ + { + name: "valid_legacy", + req: &testdata.Legacy{Name: "testcase", Age: 19}, + err: false, + }, + { + name: "invalid_legacy1", + req: &testdata.Legacy{Name: "testcase", Age: 10}, + err: true, + }, + { + name: "invalid_legacy2", + req: &testdata.Legacy{Name: "test", Age: 100}, + err: true, + }, + { + name: "valid_mixed", + req: &testdata.Mixed{Name: "testcase", Age: 19}, + err: false, + }, + { + name: "invalid_mixed1", + req: &testdata.Mixed{Name: "testcase", Age: 10}, + err: true, + }, + { + name: "invalid_mixed2", + req: &testdata.Mixed{Name: "test", Age: 100}, + err: true, + }, + { + name: "valid_modern", + req: &testdata.Modern{Name: "testcase", Age: 19}, + err: false, + }, + { + name: "invalid_modern1", + req: &testdata.Modern{Name: "testcase", Age: 10}, + err: true, + }, + { + name: "invalid_modern2", + req: &testdata.Modern{Name: "test", Age: 100}, + err: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + handle := ProtoValidate()(mock) + _, err := handle(context.Background(), test.req) + expect := test.err + actual := kerrors.IsBadRequest(err) + if expect != actual { + t.Errorf("case %s expect %v, actual %v, err %v", test.name, expect, actual, err) + } + }) + } +} diff --git a/contrib/opensergo/README.md b/contrib/opensergo/README.md new file mode 100644 index 0000000..f356e5a --- /dev/null +++ b/contrib/opensergo/README.md @@ -0,0 +1,32 @@ +# OpenSergo + +## Usage + +```go +osServer, err := opensergo.New(opensergo.WithEndpoint("localhost:9090")) +if err != nil { + panic("init opensergo error") +} + +s := &server{} +grpcSrv := grpc.NewServer( + grpc.Address(":9000"), + grpc.Middleware( + recovery.Recovery(), + ), +) +helloworld.RegisterGreeterServer(grpcSrv, s) + +app := kratos.New( + kratos.Name(Name), + kratos.Server( + grpcSrv, + ), +) + +osServer.ReportMetadata(context.Background(), app) + +if err := app.Run(); err != nil { + log.Fatal(err) +} +``` diff --git a/contrib/opensergo/go.mod b/contrib/opensergo/go.mod new file mode 100644 index 0000000..a8ca965 --- /dev/null +++ b/contrib/opensergo/go.mod @@ -0,0 +1,25 @@ +module github.com/go-kratos/kratos/contrib/opensergo/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/opensergo/opensergo-go v0.0.0-20220331070310-e5b01fee4d1c + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/go-playground/form/v4 v4.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kr/text v0.2.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../ diff --git a/contrib/opensergo/go.sum b/contrib/opensergo/go.sum new file mode 100644 index 0000000..fab7337 --- /dev/null +++ b/contrib/opensergo/go.sum @@ -0,0 +1,171 @@ +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= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/opensergo/opensergo-go v0.0.0-20220331070310-e5b01fee4d1c h1:Ivg+PYq7sOf2IwBGbo9Tt6jk+LHSUapWVPBEr9ygh8Q= +github.com/opensergo/opensergo-go v0.0.0-20220331070310-e5b01fee4d1c/go.mod h1:VxL391S2BWXU1m14087xt8+2YTgsnfa+xsSrbuoFKl4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/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-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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/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= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/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.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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/contrib/opensergo/opensergo.go b/contrib/opensergo/opensergo.go new file mode 100644 index 0000000..1f02945 --- /dev/null +++ b/contrib/opensergo/opensergo.go @@ -0,0 +1,201 @@ +package opensergo + +import ( + "context" + "encoding/json" + "net" + "net/http" + "net/url" + "os" + "strconv" + + v1 "github.com/opensergo/opensergo-go/proto/service_contract/v1" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + + "github.com/go-kratos/kratos/v3" +) + +type Option func(*options) + +func WithEndpoint(endpoint string) Option { + return func(o *options) { + o.Endpoint = endpoint + } +} + +type options struct { + Endpoint string `json:"endpoint"` +} + +func (o *options) ParseJSON(data []byte) error { + return json.Unmarshal(data, o) +} + +type OpenSergo struct { + mdClient v1.MetadataServiceClient +} + +func New(opts ...Option) (*OpenSergo, error) { + opt := options{ + Endpoint: os.Getenv("OPENSERGO_ENDPOINT"), + } + // https://github.com/opensergo/opensergo-specification/blob/main/specification/en/README.md + if v := os.Getenv("OPENSERGO_BOOTSTRAP"); v != "" { + if err := opt.ParseJSON([]byte(v)); err != nil { + return nil, err + } + } + if v := os.Getenv("OPENSERGO_BOOTSTRAP_CONFIG"); v != "" { + b, err := os.ReadFile(v) + if err != nil { + return nil, err + } + if err := opt.ParseJSON(b); err != nil { + return nil, err + } + } + for _, o := range opts { + o(&opt) + } + conn, err := grpc.NewClient(opt.Endpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + conn.Connect() + return &OpenSergo{ + mdClient: v1.NewMetadataServiceClient(conn), + }, nil +} + +func (s *OpenSergo) ReportMetadata(ctx context.Context, app kratos.AppInfo) error { + services, types, err := listDescriptors() + if err != nil { + return err + } + + serviceMetadata := &v1.ServiceMetadata{ + ServiceContract: &v1.ServiceContract{ + Services: services, + Types: types, + }, + } + + for _, endpoint := range app.Endpoint() { + u, err := url.Parse(endpoint) // nolint + if err != nil { + return err + } + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return err + } + portUint64, err := strconv.ParseUint(port, 10, 32) + if err != nil { + return err + } + serviceMetadata.Protocols = append(serviceMetadata.Protocols, u.Scheme) + serviceMetadata.ListeningAddresses = append(serviceMetadata.ListeningAddresses, &v1.SocketAddress{ + Address: host, + PortValue: uint32(portUint64), + }) + } + _, err = s.mdClient.ReportMetadata(ctx, &v1.ReportMetadataRequest{ + AppName: app.Name(), + ServiceMetadata: []*v1.ServiceMetadata{serviceMetadata}, + // TODO: Node: *v1.Node, + }) + return err +} + +func listDescriptors() (services []*v1.ServiceDescriptor, types []*v1.TypeDescriptor, err error) { + protoregistry.GlobalFiles.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + for i := 0; i < fd.Services().Len(); i++ { + var ( + methods []*v1.MethodDescriptor + sd = fd.Services().Get(i) + ) + for j := 0; j < sd.Methods().Len(); j++ { + md := sd.Methods().Get(j) + mName := string(md.Name()) + inputType := string(md.Input().FullName()) + outputType := string(md.Output().FullName()) + isClientStreaming := md.IsStreamingClient() + isServerStreaming := md.IsStreamingServer() + pattern := proto.GetExtension(md.Options(), annotations.E_Http).(*annotations.HttpRule).GetPattern() + var httpPath, httpMethod string + if pattern != nil { + httpMethod, httpPath = HTTPPatternInfo(pattern) + } + methodDesc := v1.MethodDescriptor{ + Name: mName, + InputTypes: []string{inputType}, + OutputTypes: []string{outputType}, + ClientStreaming: &isClientStreaming, + ServerStreaming: &isServerStreaming, + HttpPaths: []string{httpPath}, + HttpMethods: []string{httpMethod}, + // TODO: Description: *string, + } + methods = append(methods, &methodDesc) + } + services = append(services, &v1.ServiceDescriptor{ + Name: string(sd.Name()), + Methods: methods, + // TODO: Description: *string, + }) + } + + for i := 0; i < fd.Messages().Len(); i++ { + var ( + fields []*v1.FieldDescriptor + md = fd.Messages().Get(i) + ) + + for j := 0; j < md.Fields().Len(); j++ { + fd := md.Fields().Get(j) + kind := fd.Kind() + typeName := kind.String() + + fields = append(fields, &v1.FieldDescriptor{ + Name: string(fd.Name()), + Number: int32(fd.Number()), + Type: v1.FieldDescriptor_Type(kind), + TypeName: &typeName, + // TODO: Description: *string, + }) + } + + types = append(types, &v1.TypeDescriptor{ + Name: string(md.Name()), + Fields: fields, + }) + } + + return true + }) + return +} + +func HTTPPatternInfo(pattern any) (method string, path string) { + switch p := pattern.(type) { + case *annotations.HttpRule_Get: + return http.MethodGet, p.Get + case *annotations.HttpRule_Post: + return http.MethodPost, p.Post + case *annotations.HttpRule_Delete: + return http.MethodDelete, p.Delete + case *annotations.HttpRule_Patch: + return http.MethodPatch, p.Patch + case *annotations.HttpRule_Put: + return http.MethodPut, p.Put + case *annotations.HttpRule_Custom: + return p.Custom.Kind, p.Custom.Path + default: + return "", "" + } +} diff --git a/contrib/opensergo/opensergo_test.go b/contrib/opensergo/opensergo_test.go new file mode 100644 index 0000000..f4162b0 --- /dev/null +++ b/contrib/opensergo/opensergo_test.go @@ -0,0 +1,385 @@ +package opensergo + +import ( + "context" + "net" + "net/http" + "os" + "path/filepath" + "reflect" + "testing" + + srvContractPb "github.com/opensergo/opensergo-go/proto/service_contract/v1" + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + pref "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/descriptorpb" +) + +type testMetadataServiceServer struct { + srvContractPb.UnimplementedMetadataServiceServer +} + +func (m *testMetadataServiceServer) ReportMetadata(_ context.Context, _ *srvContractPb.ReportMetadataRequest) (*srvContractPb.ReportMetadataReply, error) { + return &srvContractPb.ReportMetadataReply{}, nil +} + +type testAppInfo struct { + id string + name string + version string + metaData map[string]string + endpoint []string +} + +func (t testAppInfo) ID() string { + return t.id +} + +func (t testAppInfo) Name() string { + return t.name +} + +func (t testAppInfo) Version() string { + return t.version +} + +func (t testAppInfo) Metadata() map[string]string { + return t.metaData +} + +func (t testAppInfo) Endpoint() []string { + return t.endpoint +} + +func TestWithEndpoint(t *testing.T) { + o := &options{} + v := "127.0.0.1:9090" + WithEndpoint(v)(o) + if !reflect.DeepEqual(v, o.Endpoint) { + t.Fatalf("o.Endpoint:%s is not equal to v:%s", o.Endpoint, v) + } +} + +func TestOptionsParseJSON(t *testing.T) { + want := &options{ + Endpoint: "127.0.0.1:9090", + } + o := &options{} + if err := o.ParseJSON([]byte(`{"endpoint":"127.0.0.1:9090"}`)); err != nil { + t.Fatalf("o.ParseJSON(v) error:%s", err) + } + if !reflect.DeepEqual(o, want) { + t.Fatalf("o:%v is not equal to want:%v", o, want) + } +} + +func TestListDescriptors(t *testing.T) { + testPb := &descriptorpb.FileDescriptorProto{ + Syntax: proto.String("proto3"), + Name: proto.String("test.proto"), + Package: proto.String("test"), + MessageType: []*descriptorpb.DescriptorProto{ + { + Name: proto.String("TestMessage"), + Field: []*descriptorpb.FieldDescriptorProto{ + { + Name: proto.String("id"), + JsonName: proto.String("id"), + Number: proto.Int32(1), + Type: descriptorpb.FieldDescriptorProto_Type(pref.Int32Kind).Enum(), + }, + { + Name: proto.String("name"), + JsonName: proto.String("name"), + Number: proto.Int32(2), + Type: descriptorpb.FieldDescriptorProto_Type(pref.StringKind).Enum(), + }, + }, + }, + }, + Service: []*descriptorpb.ServiceDescriptorProto{ + { + Name: proto.String("TestService"), + Method: []*descriptorpb.MethodDescriptorProto{ + { + Name: proto.String("Create"), + InputType: proto.String("TestMessage"), + OutputType: proto.String("TestMessage"), + }, + }, + }, + }, + } + + fd, err := protodesc.NewFile(testPb, nil) + if err != nil { + t.Fatalf("protodesc.NewFile(pb, nil) error:%s", err) + } + + protoregistry.GlobalFiles = new(protoregistry.Files) + err = protoregistry.GlobalFiles.RegisterFile(fd) + if err != nil { + t.Fatalf("protoregistry.GlobalFiles.RegisterFile(fd) error:%s", err) + } + + want := struct { + services []*srvContractPb.ServiceDescriptor + types []*srvContractPb.TypeDescriptor + }{ + services: []*srvContractPb.ServiceDescriptor{ + { + Name: "TestService", + Methods: []*srvContractPb.MethodDescriptor{ + { + Name: "Create", + InputTypes: []string{"test.TestMessage"}, + OutputTypes: []string{"test.TestMessage"}, + ClientStreaming: proto.Bool(false), + ServerStreaming: proto.Bool(false), + Description: nil, + HttpPaths: []string{""}, + HttpMethods: []string{""}, + }, + }, + }, + }, + types: []*srvContractPb.TypeDescriptor{ + { + Name: "TestMessage", + Fields: []*srvContractPb.FieldDescriptor{ + { + Name: "id", + Number: int32(1), + Type: srvContractPb.FieldDescriptor_TYPE_INT32, + TypeName: proto.String("int32"), + }, + { + Name: "name", + Number: int32(2), + Type: srvContractPb.FieldDescriptor_TYPE_STRING, + TypeName: proto.String("string"), + }, + }, + }, + }, + } + + services, types, err := listDescriptors() + if err != nil { + t.Fatalf("listDescriptors error:%s", err) + } + + if !reflect.DeepEqual(services, want.services) { + t.Fatalf("services:%v is not equal to want.services:%v", services, want.services) + } + if !reflect.DeepEqual(types, want.types) { + t.Fatalf("types:%v is not equal to want.types:%v", types, want.types) + } +} + +func TestHTTPPatternInfo(t *testing.T) { + type args struct { + pattern any + } + tests := []struct { + name string + args args + wantMethod string + wantPath string + }{ + { + name: "get", + args: args{ + pattern: &annotations.HttpRule_Get{Get: "/foo"}, + }, + wantMethod: http.MethodGet, + wantPath: "/foo", + }, + { + name: "post", + args: args{ + pattern: &annotations.HttpRule_Post{Post: "/foo"}, + }, + wantMethod: http.MethodPost, + wantPath: "/foo", + }, + { + name: "put", + args: args{ + pattern: &annotations.HttpRule_Put{Put: "/foo"}, + }, + wantMethod: http.MethodPut, + wantPath: "/foo", + }, + { + name: "delete", + args: args{ + pattern: &annotations.HttpRule_Delete{Delete: "/foo"}, + }, + wantMethod: http.MethodDelete, + wantPath: "/foo", + }, + { + name: "patch", + args: args{ + pattern: &annotations.HttpRule_Patch{Patch: "/foo"}, + }, + wantMethod: http.MethodPatch, + wantPath: "/foo", + }, + { + name: "custom", + args: args{ + pattern: &annotations.HttpRule_Custom{ + Custom: &annotations.CustomHttpPattern{ + Kind: "CUSTOM", + Path: "/foo", + }, + }, + }, + wantMethod: "CUSTOM", + wantPath: "/foo", + }, + { + name: "other", + args: args{ + pattern: nil, + }, + wantMethod: "", + wantPath: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotMethod, gotPath := HTTPPatternInfo(tt.args.pattern) + if gotMethod != tt.wantMethod { + t.Errorf("HTTPPatternInfo() gotMethod = %v, want %v", gotMethod, tt.wantMethod) + } + if gotPath != tt.wantPath { + t.Errorf("HTTPPatternInfo() gotPath = %v, want %v", gotPath, tt.wantPath) + } + }) + } +} + +func TestOpenSergo(t *testing.T) { + srv := grpc.NewServer() + srvContractPb.RegisterMetadataServiceServer(srv, new(testMetadataServiceServer)) + lis, err := net.Listen("tcp", "127.0.0.1:9090") + if err != nil { + t.Fatalf("net.Listen error:%s", err) + } + go func() { + err := srv.Serve(lis) + if err != nil { + panic(err) + } + }() + + app := &testAppInfo{ + name: "testApp", + endpoint: []string{"//example.com:9090", "//foo.com:9090"}, + } + + type args struct { + opts []Option + } + tests := []struct { + name string + args args + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + wantErr bool + }{ + { + name: "test_with_opts", + args: args{ + opts: []Option{ + WithEndpoint("127.0.0.1:9090"), + }, + }, + wantErr: false, + }, + { + name: "test_with_env_endpoint", + args: args{ + opts: []Option{}, + }, + preFunc: func(_ *testing.T) { + err := os.Setenv("OPENSERGO_ENDPOINT", "127.0.0.1:9090") + if err != nil { + panic(err) + } + }, + wantErr: false, + }, + { + name: "test_with_env_config_file", + args: args{ + opts: []Option{}, + }, + preFunc: func(_ *testing.T) { + err := os.Setenv("OPENSERGO_BOOTSTRAP", `{"endpoint": "127.0.0.1:9090"}`) + if err != nil { + panic(err) + } + }, + wantErr: false, + }, + { + name: "test_with_env_bootstrap", + args: args{ + opts: []Option{}, + }, + preFunc: func(t *testing.T) { + fileContent := `{"endpoint": "127.0.0.1:9090"}` + err := os.WriteFile("test.json", []byte(fileContent), 0o644) + if err != nil { + t.Fatalf("os.WriteFile error:%s", err) + } + confPath, err := filepath.Abs("./test.json") + if err != nil { + t.Fatalf("filepath.Abs error:%s", err) + } + err = os.Setenv("OPENSERGO_BOOTSTRAP_CONFIG", confPath) + if err != nil { + panic(err) + } + }, + deferFunc: func(t *testing.T) { + path := os.Getenv("OPENSERGO_BOOTSTRAP_CONFIG") + if path != "" { + err := os.Remove(path) + if err != nil { + t.Fatalf("os.Remove error:%s", err) + } + } + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc(t) + } + if tt.deferFunc != nil { + defer tt.deferFunc(t) + } + osServer, err := New(tt.args.opts...) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + err = osServer.ReportMetadata(context.Background(), app) + if (err != nil) != tt.wantErr { + t.Errorf("ReportMetadata() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/contrib/otel/README.md b/contrib/otel/README.md new file mode 100644 index 0000000..82a74a5 --- /dev/null +++ b/contrib/otel/README.md @@ -0,0 +1,40 @@ +# OpenTelemetry contrib + +This module keeps OpenTelemetry integrations out of the core Kratos module. + +## Packages + +- `github.com/go-kratos/kratos/contrib/otel/v3/log`: slog handler bridge, usually imported as `otel` for `otel.NewHandler`. +- `github.com/go-kratos/kratos/contrib/otel/v3/tracing`: tracing middleware and trace slog attributes. +- `github.com/go-kratos/kratos/contrib/otel/v3/metrics`: metrics middleware and OTel metric helpers. + +## Logger + +```go +import ( + otel "github.com/go-kratos/kratos/contrib/otel/v3/log" + "github.com/go-kratos/kratos/v3/log" +) + +logger := log.NewLogger(otel.NewHandler("helloworld")) +``` + +Use the core Kratos log builder when the logger also needs fixed attrs or +filtering: + +```go +import ( + "log/slog" + + otel "github.com/go-kratos/kratos/contrib/otel/v3/log" + "github.com/go-kratos/kratos/v3/log" +) + +logger := log.NewLogger( + otel.NewHandler("helloworld"), + log.WithFilter(log.FilterKey("password")), +).With(slog.String("service.name", "helloworld")) +``` + +Log, tracing, and metrics stay as shallow subpackages because they expose common +names such as `NewHandler`, `Server`, `Client`, and `Option`. diff --git a/contrib/otel/go.mod b/contrib/otel/go.mod new file mode 100644 index 0000000..60f1b70 --- /dev/null +++ b/contrib/otel/go.mod @@ -0,0 +1,38 @@ +module github.com/go-kratos/kratos/contrib/otel/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/log v0.19.0 + go.opentelemetry.io/otel/log/logtest v0.19.0 + go.opentelemetry.io/otel/metric v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/form/v4 v4.3.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../ diff --git a/contrib/otel/go.sum b/contrib/otel/go.sum new file mode 100644 index 0000000..1a3b578 --- /dev/null +++ b/contrib/otel/go.sum @@ -0,0 +1,76 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 h1:hhPGP3zvvy1xWT9RTy970wlniSxFttBIsAK1gvMguJM= +go.opentelemetry.io/contrib/bridges/otelslog v0.18.0/go.mod h1:twJF7inoMza6kxMcF8JOdL3mPmtOZu7GEr34CUNE6Dg= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/log/logtest v0.19.0 h1:HdSsl4ndTK15LtJGLWBfMsSlLrCgSeE3VMzwOrLYiYs= +go.opentelemetry.io/otel/log/logtest v0.19.0/go.mod h1:c1sH1nOHTwfMCWhhQTdWGqxgDjZhtkbkzAqGGyj0Ijs= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/otel/log/log.go b/contrib/otel/log/log.go new file mode 100644 index 0000000..16d930c --- /dev/null +++ b/contrib/otel/log/log.go @@ -0,0 +1,103 @@ +package log + +import ( + "context" + "log/slog" + + "go.opentelemetry.io/contrib/bridges/otelslog" + otellog "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/trace" +) + +// Option configures the OpenTelemetry slog bridge. +type Option func(*options) + +type options struct { + otel []otelslog.Option +} + +// WithLoggerProvider configures the OpenTelemetry LoggerProvider. +func WithLoggerProvider(provider otellog.LoggerProvider) Option { + return func(c *options) { + c.otel = append(c.otel, otelslog.WithLoggerProvider(provider)) + } +} + +// WithSchemaURL configures the semantic convention schema URL. +func WithSchemaURL(schemaURL string) Option { + return func(c *options) { + c.otel = append(c.otel, otelslog.WithSchemaURL(schemaURL)) + } +} + +// WithSource configures whether source locations are emitted. +func WithSource(source bool) Option { + return func(c *options) { + c.otel = append(c.otel, otelslog.WithSource(source)) + } +} + +// WithVersion configures the instrumentation version. +func WithVersion(version string) Option { + return func(c *options) { + c.otel = append(c.otel, otelslog.WithVersion(version)) + } +} + +// NewHandler returns a slog handler that sends records to OpenTelemetry Logs +// and adds trace correlation attrs from the log context. +func NewHandler(name string, opts ...Option) slog.Handler { + cfg := newOptions(opts) + return newHandler(name, cfg) +} + +func newOptions(opts []Option) options { + var cfg options + for _, opt := range opts { + if opt != nil { + opt(&cfg) + } + } + return cfg +} + +func newHandler(name string, cfg options) slog.Handler { + return &traceHandler{next: otelslog.NewHandler(name, cfg.otel...)} +} + +type traceHandler struct { + next slog.Handler +} + +func (h *traceHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.next.Enabled(ctx, level) +} + +func (h *traceHandler) Handle(ctx context.Context, record slog.Record) error { + attrs := traceAttrs(ctx) + if len(attrs) > 0 { + record = record.Clone() + record.AddAttrs(attrs...) + } + return h.next.Handle(ctx, record) +} + +func (h *traceHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &traceHandler{next: h.next.WithAttrs(attrs)} +} + +func (h *traceHandler) WithGroup(name string) slog.Handler { + return &traceHandler{next: h.next.WithGroup(name)} +} + +func traceAttrs(ctx context.Context) []slog.Attr { + span := trace.SpanContextFromContext(ctx) + if !span.IsValid() { + return nil + } + return []slog.Attr{ + slog.String("trace_id", span.TraceID().String()), + slog.String("span_id", span.SpanID().String()), + slog.String("trace_flags", span.TraceFlags().String()), + } +} diff --git a/contrib/otel/log/log_test.go b/contrib/otel/log/log_test.go new file mode 100644 index 0000000..cad7a82 --- /dev/null +++ b/contrib/otel/log/log_test.go @@ -0,0 +1,165 @@ +package log + +import ( + "context" + "log/slog" + "testing" + + otellog "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/logtest" + "go.opentelemetry.io/otel/trace" + + klog "github.com/go-kratos/kratos/v3/log" +) + +func TestTraceAttrs(t *testing.T) { + traceID := trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + spanID := trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8} + spanContext := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + ctx := trace.ContextWithSpanContext(context.Background(), spanContext) + + attrs := map[string]string{} + for _, attr := range traceAttrs(ctx) { + if attr.Value.Kind() == slog.KindString { + attrs[attr.Key] = attr.Value.String() + } + } + if attrs["trace_id"] != traceID.String() { + t.Fatalf("trace_id = %q, want %q", attrs["trace_id"], traceID.String()) + } + if attrs["span_id"] != spanID.String() { + t.Fatalf("span_id = %q, want %q", attrs["span_id"], spanID.String()) + } + if attrs["trace_flags"] != trace.FlagsSampled.String() { + t.Fatalf("trace_flags = %q, want %q", attrs["trace_flags"], trace.FlagsSampled.String()) + } +} + +func TestNewHandlerAppliesOTelOptionsAndTraceAttrs(t *testing.T) { + recorder := logtest.NewRecorder() + traceID := trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + spanID := trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8} + spanContext := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + ctx := trace.ContextWithSpanContext(context.Background(), spanContext) + + logger := slog.New(NewHandler("helloworld", + WithLoggerProvider(recorder), + WithVersion("v1.2.3"), + WithSchemaURL("https://example.test/schema"), + )) + logger.InfoContext(ctx, "user created", "user_id", "42") + + scope, records := onlyRecording(t, recorder.Result()) + if scope.Name != "helloworld" { + t.Fatalf("scope name = %q, want %q", scope.Name, "helloworld") + } + if scope.Version != "v1.2.3" { + t.Fatalf("scope version = %q, want %q", scope.Version, "v1.2.3") + } + if scope.SchemaURL != "https://example.test/schema" { + t.Fatalf("scope schema URL = %q, want %q", scope.SchemaURL, "https://example.test/schema") + } + if len(records) != 1 { + t.Fatalf("len(records) = %d, want 1", len(records)) + } + + attrs := recordAttrs(records[0]) + tests := map[string]string{ + "trace_id": traceID.String(), + "span_id": spanID.String(), + "trace_flags": trace.FlagsSampled.String(), + "user_id": "42", + } + for key, want := range tests { + if attrs[key] != want { + t.Fatalf("%s = %q, want %q", key, attrs[key], want) + } + } +} + +func TestNewHandlerComposesWithCoreLoggerOptions(t *testing.T) { + recorder := logtest.NewRecorder() + traceID := trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + spanID := trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8} + spanContext := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + }) + ctx := klog.ContextWithAttrs(context.Background(), slog.String("request_id", "req-1")) + ctx = trace.ContextWithSpanContext(ctx, spanContext) + + logger := klog.NewLogger( + NewHandler("helloworld", WithLoggerProvider(recorder)), + klog.WithFilter(klog.FilterKey("password")), + ).With(slog.String("service.name", "helloworld")) + logger.InfoContext(ctx, "user created", "user_id", "42", "password", "secret") + + _, records := onlyRecording(t, recorder.Result()) + if len(records) != 1 { + t.Fatalf("len(records) = %d, want 1", len(records)) + } + + record := records[0] + attrs := recordAttrs(record) + tests := map[string]string{ + "service.name": "helloworld", + "request_id": "req-1", + "trace_id": traceID.String(), + "span_id": spanID.String(), + "trace_flags": trace.FlagsSampled.String(), + "user_id": "42", + "password": "***", + } + for key, want := range tests { + if attrs[key] != want { + t.Fatalf("%s = %q, want %q", key, attrs[key], want) + } + } + if count := recordAttrCount(record, "request_id"); count != 1 { + t.Fatalf("request_id count = %d, want 1", count) + } + if count := recordAttrCount(record, "trace_id"); count != 1 { + t.Fatalf("trace_id count = %d, want 1", count) + } +} + +func onlyRecording(t *testing.T, results logtest.Recording) (logtest.Scope, []logtest.Record) { + t.Helper() + if len(results) != 1 { + t.Fatalf("len(results) = %d, want 1", len(results)) + } + for scope, records := range results { + return scope, records + } + t.Fatal("missing recording") + return logtest.Scope{}, nil +} + +func recordAttrs(record logtest.Record) map[string]string { + attrs := map[string]string{} + for _, kv := range record.Attributes { + if kv.Value.Kind() == otellog.KindString { + attrs[kv.Key] = kv.Value.AsString() + } + } + return attrs +} + +func recordAttrCount(record logtest.Record, key string) int { + var count int + for _, kv := range record.Attributes { + if kv.Key == key { + count++ + } + } + return count +} diff --git a/contrib/otel/metrics/metrics.go b/contrib/otel/metrics/metrics.go new file mode 100644 index 0000000..f955811 --- /dev/null +++ b/contrib/otel/metrics/metrics.go @@ -0,0 +1,210 @@ +package metrics + +import ( + "context" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + metricsdk "go.opentelemetry.io/otel/sdk/metric" + "google.golang.org/grpc/codes" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/http/status" +) + +const ( + metricLabelKind = "kind" + metricLabelOperation = "operation" + metricLabelCode = "code" + metricLabelReason = "reason" +) + +const ( + DefaultServerSecondsHistogramName = "server_requests_seconds" + DefaultServerRequestsCounterName = "server_requests_code_total" + DefaultClientSecondsHistogramName = "client_requests_seconds" + DefaultClientRequestsCounterName = "client_requests_code_total" +) + +// Option is metrics option. +type Option func(*options) + +// WithRequests with requests counter. +func WithRequests(c metric.Int64Counter) Option { + return func(o *options) { + o.requests = c + } +} + +// WithSeconds with seconds histogram. +// notice: the record unit in current middleware is s(Seconds) +func WithSeconds(histogram metric.Float64Histogram) Option { + return func(o *options) { + o.seconds = histogram + } +} + +// DefaultRequestsCounter +// return metric.Int64Counter for WithRequests +// suggest histogramName = _requests_code_total +func DefaultRequestsCounter(meter metric.Meter, histogramName string) (metric.Int64Counter, error) { + return meter.Int64Counter(histogramName, metric.WithUnit("{call}")) +} + +// DefaultSecondsHistogram +// return metric.Float64Histogram for WithSeconds +// suggest histogramName = _requests_seconds +func DefaultSecondsHistogram(meter metric.Meter, histogramName string) (metric.Float64Histogram, error) { + return meter.Float64Histogram( + histogramName, + metric.WithUnit("s"), + metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.1, 0.250, 0.5, 1), + ) +} + +// DefaultSecondsHistogramView +// need register in sdkmetric.MeterProvider +// eg: +// view := SecondsHistogramView() +// mp := sdkmetric.NewMeterProvider(sdkmetric.WithView(view)) +// otel.SetMeterProvider(mp) +func DefaultSecondsHistogramView(histogramName string) metricsdk.View { + return func(instrument metricsdk.Instrument) (metricsdk.Stream, bool) { + if instrument.Name == histogramName { + return metricsdk.Stream{ + Name: instrument.Name, + Description: instrument.Description, + Unit: instrument.Unit, + Aggregation: metricsdk.AggregationExplicitBucketHistogram{ + Boundaries: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.250, 0.5, 1}, + NoMinMax: true, + }, + AttributeFilter: func(attribute.KeyValue) bool { + return true + }, + }, true + } + return metricsdk.Stream{}, false + } +} + +type options struct { + // counter: _requests_code_total{kind, operation, code, reason} + requests metric.Int64Counter + // histogram: _requests_seconds{kind, operation} + seconds metric.Float64Histogram +} + +// Server is middleware server-side metrics. +func Server(opts ...Option) middleware.Middleware { + op := options{} + for _, o := range opts { + o(&op) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + // if requests and seconds are nil, return directly + if op.requests == nil && op.seconds == nil { + return handler(ctx, req) + } + + var ( + code int + reason string + kind string + operation string + ) + + // default code + code = status.FromGRPCCode(codes.OK) + + startTime := time.Now() + if info, ok := transport.FromServerContext(ctx); ok { + kind = info.Kind().String() + operation = info.Operation() + } + reply, err := handler(ctx, req) + if se := errors.FromError(err); se != nil { + code = int(se.Code) + reason = se.Reason + } + if op.requests != nil { + op.requests.Add( + ctx, 1, + metric.WithAttributes( + attribute.String(metricLabelKind, kind), + attribute.String(metricLabelOperation, operation), + attribute.Int(metricLabelCode, code), + attribute.String(metricLabelReason, reason), + ), + ) + } + if op.seconds != nil { + op.seconds.Record( + ctx, time.Since(startTime).Seconds(), + metric.WithAttributes( + attribute.String(metricLabelKind, kind), + attribute.String(metricLabelOperation, operation), + ), + ) + } + return reply, err + } + } +} + +// Client is middleware client-side metrics. +func Client(opts ...Option) middleware.Middleware { + op := options{} + for _, o := range opts { + o(&op) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + var ( + code int + reason string + kind string + operation string + ) + + // default code + code = status.FromGRPCCode(codes.OK) + + startTime := time.Now() + if info, ok := transport.FromClientContext(ctx); ok { + kind = info.Kind().String() + operation = info.Operation() + } + reply, err := handler(ctx, req) + if se := errors.FromError(err); se != nil { + code = int(se.Code) + reason = se.Reason + } + if op.requests != nil { + op.requests.Add( + ctx, 1, + metric.WithAttributes( + attribute.String(metricLabelKind, kind), + attribute.String(metricLabelOperation, operation), + attribute.Int(metricLabelCode, code), + attribute.String(metricLabelReason, reason), + ), + ) + } + if op.seconds != nil { + op.seconds.Record( + ctx, time.Since(startTime).Seconds(), + metric.WithAttributes( + attribute.String(metricLabelKind, kind), + attribute.String(metricLabelOperation, operation), + ), + ) + } + return reply, err + } + } +} diff --git a/contrib/otel/metrics/metrics_test.go b/contrib/otel/metrics/metrics_test.go new file mode 100644 index 0000000..4aa8aea --- /dev/null +++ b/contrib/otel/metrics/metrics_test.go @@ -0,0 +1,256 @@ +package metrics + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "math/rand/v2" + "strings" + "sync" + "testing" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/http" +) + +type dummyExporter struct { + mu sync.Mutex + writeBuf bytes.Buffer +} + +func (x *dummyExporter) Temporality(kind sdkmetric.InstrumentKind) metricdata.Temporality { + return sdkmetric.DefaultTemporalitySelector(kind) +} + +func (x *dummyExporter) Aggregation(kind sdkmetric.InstrumentKind) sdkmetric.Aggregation { + return sdkmetric.DefaultAggregationSelector(kind) +} + +func (x *dummyExporter) Export(ctx context.Context, resourceMetrics *metricdata.ResourceMetrics) error { + select { + case <-ctx.Done(): + // Don't do anything if the context has already timed out. + return ctx.Err() + default: + // Context is still valid, continue. + } + x.mu.Lock() + defer x.mu.Unlock() + return json.NewEncoder(&x.writeBuf).Encode(resourceMetrics) +} + +func (x *dummyExporter) ForceFlush(ctx context.Context) error { + return ctx.Err() +} + +func (x *dummyExporter) Shutdown(ctx context.Context) error { + return ctx.Err() +} + +func (x *dummyExporter) String() string { + x.mu.Lock() + defer x.mu.Unlock() + return x.writeBuf.String() +} + +var exporter = &dummyExporter{} + +func init() { + err := EnableOTELExemplar() + if err != nil { + panic(err) + } + + mp := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter, sdkmetric.WithInterval(time.Microsecond*400))), + sdkmetric.WithView(DefaultSecondsHistogramView(DefaultServerSecondsHistogramName), func(instrument sdkmetric.Instrument) (sdkmetric.Stream, bool) { + return sdkmetric.Stream{ + Name: instrument.Name, + Description: instrument.Description, + Unit: instrument.Unit, + AttributeFilter: func(attribute.KeyValue) bool { + return true + }, + }, true + }), + ) + otel.SetMeterProvider(mp) + + tr := sdktrace.NewTracerProvider() + otel.SetTracerProvider(tr) +} + +func TestWithRequests(t *testing.T) { + o := options{} + + if o.requests != nil { + t.Errorf(`The type of the option requests property must be of "nil"`) + return + } + + meter := otel.Meter("test_meter") + requests, err := meter.Int64Counter(DefaultServerRequestsCounterName) + if err != nil { + t.Errorf("[Int64Counter] something went wrong: %v", err) + return + } + + WithRequests(requests)(&o) + + if o.requests == nil { + t.Errorf(`The type of the option requests property must be of "mockCounter", %T given.`, o.requests) + } +} + +func TestWithSeconds(t *testing.T) { + o := options{} + + if o.seconds != nil { + t.Errorf(`The type of the option seconds property must be of "nil"`) + return + } + + meter := otel.Meter("test_meter") + seconds, err := meter.Float64Histogram(DefaultServerSecondsHistogramName) + if err != nil { + t.Errorf("[Float64Histogram] something went wrong: %v", err) + return + } + + WithSeconds(seconds)(&o) + + if o.seconds == nil { + t.Errorf(`The type of the option seconds property must be of "mockObserver", %T given.`, o.requests) + } +} + +func TestServer(t *testing.T) { + tracer := otel.Tracer("test_trace") + ctx, span := tracer.Start(context.Background(), "TestServer") + defer span.End() + + e := errors.New("got an error") + nextError := func(context.Context, any) (any, error) { + return nil, e + } + nextValid := func(context.Context, any) (any, error) { + time.Sleep(time.Millisecond * time.Duration(rand.Int32N(100))) + return "Hello valid", nil + } + + // init server handler + meter := otel.Meter("test_meter") + requests, err := DefaultRequestsCounter(meter, DefaultServerRequestsCounterName) + if err != nil { + t.Errorf("[DefaultRequestsCounter] something went wrong: %v", err) + return + } + seconds, err := DefaultSecondsHistogram(meter, DefaultServerSecondsHistogramName) + if err != nil { + t.Errorf("[DefaultSecondsHistogram] something went wrong: %v", err) + return + } + serverHandler := Server( + WithRequests(requests), + WithSeconds(seconds), + ) + + _, err = serverHandler(nextError)(ctx, "test:") + if err != e { + t.Error("The given error mismatch the expected.") + return + } + + for i := 0; i < 20; i++ { + res, err := serverHandler(nextValid)(transport.NewServerContext(ctx, &http.Transport{}), "test:") + if err != nil { + t.Error("The server must not throw an error.") + } + if res != "Hello valid" { + t.Error(`The server must return a "Hello valid" response.`) + } + } + bufStr := exporter.String() + + for _, label := range []string{ + metricLabelKind, + metricLabelOperation, + metricLabelCode, + metricLabelReason, + "SpanID", + "TraceID", + } { + if !strings.Contains(bufStr, fmt.Sprintf("\"%s\"", label)) { + t.Errorf("The expected metric label %s is not found in the output: %s", label, bufStr) + } + } +} + +func TestClient(t *testing.T) { + tracer := otel.Tracer("test_trace") + ctx, span := tracer.Start(context.Background(), "TestClient") + defer span.End() + + e := errors.New("got an error") + nextError := func(context.Context, any) (any, error) { + return nil, e + } + nextValid := func(context.Context, any) (any, error) { + return "Hello valid", nil + } + + // init client handler + meter := otel.Meter("test_meter") + requests, err := DefaultRequestsCounter(meter, DefaultServerRequestsCounterName) + if err != nil { + t.Errorf("[DefaultRequestsCounter] something went wrong: %v", err) + return + } + seconds, err := DefaultSecondsHistogram(meter, DefaultServerSecondsHistogramName) + if err != nil { + t.Errorf("[DefaultSecondsHistogram] something went wrong: %v", err) + return + } + clientHandler := Client( + WithRequests(requests), + WithSeconds(seconds), + ) + + _, err = clientHandler(nextError)(ctx, "test:") + if err != e { + t.Error("The given error mismatch the expected.") + } + + for i := 0; i < 20; i++ { + res, err := clientHandler(nextValid)(transport.NewClientContext(ctx, &http.Transport{}), "test:") + if err != nil { + t.Error("The server must not throw an error.") + } + if res != "Hello valid" { + t.Error(`The server must return a "Hello valid" response.`) + } + } + bufStr := exporter.String() + + for _, label := range []string{ + metricLabelKind, + metricLabelOperation, + metricLabelCode, + metricLabelReason, + "SpanID", + "TraceID", + } { + if !strings.Contains(bufStr, fmt.Sprintf("\"%s\"", label)) { + t.Errorf("The expected metric label %s is not found in the output: %s", label, bufStr) + } + } +} diff --git a/contrib/otel/metrics/otel.go b/contrib/otel/metrics/otel.go new file mode 100644 index 0000000..c90a8f2 --- /dev/null +++ b/contrib/otel/metrics/otel.go @@ -0,0 +1,7 @@ +package metrics + +import "os" + +func EnableOTELExemplar() error { + return os.Setenv("OTEL_GO_X_EXEMPLAR", "true") +} diff --git a/contrib/otel/tracing/metadata.go b/contrib/otel/tracing/metadata.go new file mode 100644 index 0000000..96d07c3 --- /dev/null +++ b/contrib/otel/tracing/metadata.go @@ -0,0 +1,46 @@ +package tracing + +import ( + "context" + + "go.opentelemetry.io/otel/propagation" + + "github.com/go-kratos/kratos/v3" + "github.com/go-kratos/kratos/v3/metadata" +) + +const serviceHeader = "x-md-service-name" + +// Metadata is tracing metadata propagator +type Metadata struct{} + +var _ propagation.TextMapPropagator = Metadata{} + +// Inject sets metadata key-values from ctx into the carrier. +func (b Metadata) Inject(ctx context.Context, carrier propagation.TextMapCarrier) { + app, ok := kratos.FromContext(ctx) + if ok { + carrier.Set(serviceHeader, app.Name()) + } +} + +// Extract returns a copy of parent with the metadata from the carrier added. +func (b Metadata) Extract(parent context.Context, carrier propagation.TextMapCarrier) context.Context { + name := carrier.Get(serviceHeader) + if name == "" { + return parent + } + if md, ok := metadata.FromServerContext(parent); ok { + md.Set(serviceHeader, name) + return parent + } + md := metadata.New() + md.Set(serviceHeader, name) + parent = metadata.NewServerContext(parent, md) + return parent +} + +// Fields returns the keys whose values are set with Inject. +func (b Metadata) Fields() []string { + return []string{serviceHeader} +} diff --git a/contrib/otel/tracing/metadata_test.go b/contrib/otel/tracing/metadata_test.go new file mode 100644 index 0000000..05ee858 --- /dev/null +++ b/contrib/otel/tracing/metadata_test.go @@ -0,0 +1,107 @@ +package tracing + +import ( + "context" + "reflect" + "testing" + + "github.com/go-kratos/kratos/v3" + "github.com/go-kratos/kratos/v3/metadata" + + "go.opentelemetry.io/otel/propagation" +) + +func TestMetadata_Inject(t *testing.T) { + type args struct { + appName string + carrier propagation.TextMapCarrier + } + tests := []struct { + name string + args args + want string + }{ + { + name: "https://go-kratos.dev", + args: args{"https://go-kratos.dev", propagation.HeaderCarrier{}}, + want: "https://go-kratos.dev", + }, + { + name: "https://github.com/go-kratos/kratos", + args: args{"https://github.com/go-kratos/kratos", propagation.HeaderCarrier{"mode": []string{"test"}}}, + want: "https://github.com/go-kratos/kratos", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := kratos.New(kratos.Name(tt.args.appName)) + ctx := kratos.NewContext(context.Background(), a) + m := new(Metadata) + m.Inject(ctx, tt.args.carrier) + if res := tt.args.carrier.Get(serviceHeader); tt.want != res { + t.Errorf("Get(serviceHeader) :%s want: %s", res, tt.want) + } + }) + } +} + +func TestMetadata_Extract(t *testing.T) { + type args struct { + parent context.Context + carrier propagation.TextMapCarrier + } + tests := []struct { + name string + args args + want string + crash bool + }{ + { + name: "https://go-kratos.dev", + args: args{ + parent: context.Background(), + carrier: propagation.HeaderCarrier{"X-Md-Service-Name": []string{"https://go-kratos.dev"}}, + }, + want: "https://go-kratos.dev", + }, + { + name: "https://github.com/go-kratos/kratos", + args: args{ + parent: metadata.NewServerContext(context.Background(), metadata.Metadata{}), + carrier: propagation.HeaderCarrier{"X-Md-Service-Name": []string{"https://github.com/go-kratos/kratos"}}, + }, + want: "https://github.com/go-kratos/kratos", + }, + { + name: "https://github.com/go-kratos/kratos", + args: args{ + parent: metadata.NewServerContext(context.Background(), metadata.Metadata{}), + carrier: propagation.HeaderCarrier{"X-Md-Service-Name": nil}, + }, + crash: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := Metadata{} + ctx := b.Extract(tt.args.parent, tt.args.carrier) + md, ok := metadata.FromServerContext(ctx) + if !ok { + if tt.crash { + return + } + t.Errorf("expect %v, got %v", true, ok) + } + if !reflect.DeepEqual(md.Get(serviceHeader), tt.want) { + t.Errorf("expect %v, got %v", tt.want, md.Get(serviceHeader)) + } + }) + } +} + +func TestFields(t *testing.T) { + b := Metadata{} + if !reflect.DeepEqual(b.Fields(), []string{"x-md-service-name"}) { + t.Errorf("expect %v, got %v", []string{"x-md-service-name"}, b.Fields()) + } +} diff --git a/contrib/otel/tracing/span.go b/contrib/otel/tracing/span.go new file mode 100644 index 0000000..e95240e --- /dev/null +++ b/contrib/otel/tracing/span.go @@ -0,0 +1,152 @@ +package tracing + +import ( + "context" + "net" + "net/url" + "strings" + + "github.com/go-kratos/kratos/v3/metadata" + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/http" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/peer" + "google.golang.org/protobuf/proto" +) + +func setClientSpan(ctx context.Context, span trace.Span, m any) { + var ( + attrs []attribute.KeyValue + remote string + operation string + rpcKind string + ) + tr, ok := transport.FromClientContext(ctx) + if ok { + operation = tr.Operation() + rpcKind = tr.Kind().String() + switch tr.Kind() { + case transport.KindHTTP: + if ht, ok := tr.(http.Transporter); ok { + method := ht.Request().Method + route := ht.PathTemplate() + path := ht.Request().URL.Path + attrs = append(attrs, semconv.HTTPMethodKey.String(method)) + attrs = append(attrs, semconv.HTTPRouteKey.String(route)) + attrs = append(attrs, semconv.HTTPTargetKey.String(path)) + remote = ht.Request().Host + } + case transport.KindGRPC: + remote, _ = parseTarget(tr.Endpoint()) + } + } + attrs = append(attrs, semconv.RPCSystemKey.String(rpcKind)) + _, mAttrs := parseFullMethod(operation) + attrs = append(attrs, mAttrs...) + if remote != "" { + attrs = append(attrs, peerAttr(remote)...) + } + if p, ok := m.(proto.Message); ok { + attrs = append(attrs, attribute.Key("send_msg.size").Int(proto.Size(p))) + } + + span.SetAttributes(attrs...) +} + +func setServerSpan(ctx context.Context, span trace.Span, m any) { + var ( + attrs []attribute.KeyValue + remote string + operation string + rpcKind string + ) + tr, ok := transport.FromServerContext(ctx) + if ok { + operation = tr.Operation() + rpcKind = tr.Kind().String() + switch tr.Kind() { + case transport.KindHTTP: + if ht, ok := tr.(http.Transporter); ok { + method := ht.Request().Method + route := ht.PathTemplate() + path := ht.Request().URL.Path + attrs = append(attrs, semconv.HTTPMethodKey.String(method)) + attrs = append(attrs, semconv.HTTPRouteKey.String(route)) + attrs = append(attrs, semconv.HTTPTargetKey.String(path)) + remote = ht.Request().RemoteAddr + } + case transport.KindGRPC: + if p, ok := peer.FromContext(ctx); ok { + remote = p.Addr.String() + } + } + } + attrs = append(attrs, semconv.RPCSystemKey.String(rpcKind)) + _, mAttrs := parseFullMethod(operation) + attrs = append(attrs, mAttrs...) + attrs = append(attrs, peerAttr(remote)...) + if p, ok := m.(proto.Message); ok { + attrs = append(attrs, attribute.Key("recv_msg.size").Int(proto.Size(p))) + } + if md, ok := metadata.FromServerContext(ctx); ok { + attrs = append(attrs, semconv.PeerServiceKey.String(md.Get(serviceHeader))) + } + + span.SetAttributes(attrs...) +} + +// parseFullMethod returns a span name following the OpenTelemetry semantic +// conventions as well as all applicable span attribute.KeyValue attributes based +// on a gRPC's FullMethod. +func parseFullMethod(fullMethod string) (string, []attribute.KeyValue) { + name := strings.TrimLeft(fullMethod, "/") + parts := strings.SplitN(name, "/", 2) + if len(parts) != 2 { //nolint:mnd + // Invalid format, does not follow `/package.service/method`. + return name, []attribute.KeyValue{attribute.Key("rpc.operation").String(fullMethod)} + } + + var attrs []attribute.KeyValue + if service := parts[0]; service != "" { + attrs = append(attrs, semconv.RPCServiceKey.String(service)) + } + if method := parts[1]; method != "" { + attrs = append(attrs, semconv.RPCMethodKey.String(method)) + } + return name, attrs +} + +// peerAttr returns attributes about the peer address. +func peerAttr(addr string) []attribute.KeyValue { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return []attribute.KeyValue(nil) + } + + if host == "" { + host = "127.0.0.1" + } + + return []attribute.KeyValue{ + semconv.NetPeerIPKey.String(host), + semconv.NetPeerPortKey.String(port), + } +} + +func parseTarget(endpoint string) (address string, err error) { + var u *url.URL + u, err = url.Parse(endpoint) + if err != nil { + if u, err = url.Parse("http://" + endpoint); err != nil { + return "", err + } + return u.Host, nil + } + if len(u.Path) > 1 { + return u.Path[1:], nil + } + return endpoint, nil +} diff --git a/contrib/otel/tracing/span_test.go b/contrib/otel/tracing/span_test.go new file mode 100644 index 0000000..c4025f8 --- /dev/null +++ b/contrib/otel/tracing/span_test.go @@ -0,0 +1,251 @@ +package tracing + +import ( + "context" + "net" + "net/http" + "reflect" + "testing" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "google.golang.org/grpc/peer" + + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/go-kratos/kratos/v3/metadata" + "github.com/go-kratos/kratos/v3/transport" +) + +func Test_parseFullMethod(t *testing.T) { + tests := []struct { + name string + fullMethod string + want string + wantAttr []attribute.KeyValue + }{ + { + name: "/foo.bar/hello", + fullMethod: "/foo.bar/hello", + want: "foo.bar/hello", + wantAttr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("foo.bar"), + semconv.RPCMethodKey.String("hello"), + }, + }, + { + name: "/foo.bar/hello/world", + fullMethod: "/foo.bar/hello/world", + want: "foo.bar/hello/world", + wantAttr: []attribute.KeyValue{ + semconv.RPCServiceKey.String("foo.bar"), + semconv.RPCMethodKey.String("hello/world"), + }, + }, + { + name: "/hello", + fullMethod: "/hello", + want: "hello", + wantAttr: []attribute.KeyValue{attribute.Key("rpc.operation").String("/hello")}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := parseFullMethod(tt.fullMethod) + if got != tt.want { + t.Errorf("parseFullMethod() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.wantAttr) { + t.Errorf("parseFullMethod() got1 = %v, want %v", got1, tt.wantAttr) + } + }) + } +} + +func Test_peerAttr(t *testing.T) { + tests := []struct { + name string + addr string + want []attribute.KeyValue + }{ + { + name: "nil addr", + addr: ":8080", + want: []attribute.KeyValue{ + semconv.NetPeerIPKey.String("127.0.0.1"), + semconv.NetPeerPortKey.String("8080"), + }, + }, + { + name: "normal addr without port", + addr: "192.168.0.1", + want: []attribute.KeyValue(nil), + }, + { + name: "normal addr with port", + addr: "192.168.0.1:8080", + want: []attribute.KeyValue{ + semconv.NetPeerIPKey.String("192.168.0.1"), + semconv.NetPeerPortKey.String("8080"), + }, + }, + { + name: "dns addr", + addr: "foo:8080", + want: []attribute.KeyValue{ + semconv.NetPeerIPKey.String("foo"), + semconv.NetPeerPortKey.String("8080"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := peerAttr(tt.addr); !reflect.DeepEqual(got, tt.want) { + t.Errorf("peerAttr() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseTarget(t *testing.T) { + tests := []struct { + name string + endpoint string + wantAddress string + wantErr bool + }{ + { + name: "http", + endpoint: "http://foo.bar:8080", + wantAddress: "http://foo.bar:8080", + wantErr: false, + }, + { + name: "http", + endpoint: "http://127.0.0.1:8080", + wantAddress: "http://127.0.0.1:8080", + wantErr: false, + }, + { + name: "without protocol", + endpoint: "foo.bar:8080", + wantAddress: "foo.bar:8080", + wantErr: false, + }, + { + name: "grpc", + endpoint: "grpc://foo.bar", + wantAddress: "grpc://foo.bar", + wantErr: false, + }, + { + name: "with path", + endpoint: "/foo", + wantAddress: "foo", + wantErr: false, + }, + { + name: "with path", + endpoint: "http://127.0.0.1/hello", + wantAddress: "hello", + wantErr: false, + }, + { + name: "empty", + endpoint: "%%", + wantAddress: "", + wantErr: true, + }, + { + name: "invalid path", + endpoint: "//%2F/#%2Fanother", + wantAddress: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotAddress, err := parseTarget(tt.endpoint) + if (err != nil) != tt.wantErr { + t.Errorf("parseTarget() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotAddress != tt.wantAddress { + t.Errorf("parseTarget() = %v, want %v", gotAddress, tt.wantAddress) + } + }) + } +} + +func TestSetServerSpan(_ *testing.T) { + ctx := context.Background() + _, span := noop.NewTracerProvider().Tracer("Tracer").Start(ctx, "Spanname") + + // Handle without Transport context + setServerSpan(ctx, span, nil) + + // Handle with proto message + m := &emptypb.Empty{} + setServerSpan(ctx, span, m) + + // Handle with metadata context + ctx = metadata.NewServerContext(ctx, metadata.New()) + setServerSpan(ctx, span, m) + + // Handle with KindHTTP transport context + mt := &mockTransport{ + kind: transport.KindHTTP, + } + mt.request, _ = http.NewRequest(http.MethodGet, "/endpoint", nil) + ctx = transport.NewServerContext(ctx, mt) + setServerSpan(ctx, span, m) + + // Handle with KindGRPC transport context + mt.kind = transport.KindGRPC + ctx = transport.NewServerContext(ctx, mt) + ip, _ := net.ResolveIPAddr("ip", "1.1.1.1") + ctx = peer.NewContext(ctx, &peer.Peer{ + Addr: ip, + }) + setServerSpan(ctx, span, m) +} + +func TestSetClientSpan(_ *testing.T) { + ctx := context.Background() + _, span := noop.NewTracerProvider().Tracer("Tracer").Start(ctx, "Spanname") + + // Handle without Transport context + setClientSpan(ctx, span, nil) + + // Handle with proto message + m := &emptypb.Empty{} + setClientSpan(ctx, span, m) + + // Handle with metadata context + ctx = metadata.NewClientContext(ctx, metadata.New()) + setClientSpan(ctx, span, m) + + // Handle with KindHTTP transport context + mt := &mockTransport{ + kind: transport.KindHTTP, + } + mt.request, _ = http.NewRequest(http.MethodGet, "/endpoint", nil) + mt.request.Host = "MyServer" + ctx = transport.NewClientContext(ctx, mt) + setClientSpan(ctx, span, m) + + // Handle with KindGRPC transport context + mt.kind = transport.KindGRPC + ctx = transport.NewClientContext(ctx, mt) + ip, _ := net.ResolveIPAddr("ip", "1.1.1.1") + ctx = peer.NewContext(ctx, &peer.Peer{ + Addr: ip, + }) + setClientSpan(ctx, span, m) + + // Handle without Host request + ctx = transport.NewClientContext(ctx, mt) + setClientSpan(ctx, span, m) +} diff --git a/contrib/otel/tracing/statshandler.go b/contrib/otel/tracing/statshandler.go new file mode 100644 index 0000000..39598f1 --- /dev/null +++ b/contrib/otel/tracing/statshandler.go @@ -0,0 +1,43 @@ +package tracing + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/stats" +) + +// ClientHandler is tracing ClientHandler +type ClientHandler struct{} + +// HandleConn exists to satisfy gRPC stats.Handler. +func (c *ClientHandler) HandleConn(_ context.Context, _ stats.ConnStats) { + fmt.Println("Handle connection.") +} + +// TagConn exists to satisfy gRPC stats.Handler. +func (c *ClientHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context { + return ctx +} + +// HandleRPC implements per-RPC tracing and stats instrumentation. +func (c *ClientHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { + if _, ok := rs.(*stats.OutHeader); !ok { + return + } + p, ok := peer.FromContext(ctx) + if !ok { + return + } + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + span.SetAttributes(peerAttr(p.Addr.String())...) + } +} + +// TagRPC implements per-RPC context management. +func (c *ClientHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context { + return ctx +} diff --git a/contrib/otel/tracing/statshandler_test.go b/contrib/otel/tracing/statshandler_test.go new file mode 100644 index 0000000..f6d94e8 --- /dev/null +++ b/contrib/otel/tracing/statshandler_test.go @@ -0,0 +1,80 @@ +package tracing + +import ( + "context" + "net" + "testing" + + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/stats" +) + +type ctxKey string + +const testKey ctxKey = "MY_TEST_KEY" + +func TestClient_HandleConn(_ *testing.T) { + (&ClientHandler{}).HandleConn(context.Background(), nil) +} + +func TestClient_TagConn(t *testing.T) { + client := &ClientHandler{} + ctx := context.WithValue(context.Background(), testKey, 123) + + if client.TagConn(ctx, nil).Value(testKey) != 123 { + t.Errorf(`The context value must be 123 for the "MY_KEY_TEST" key, %v given.`, client.TagConn(ctx, nil).Value(testKey)) + } +} + +func TestClient_TagRPC(t *testing.T) { + client := &ClientHandler{} + ctx := context.WithValue(context.Background(), testKey, 123) + + if client.TagRPC(ctx, nil).Value(testKey) != 123 { + t.Errorf(`The context value must be 123 for the "MY_KEY_TEST" key, %v given.`, client.TagConn(ctx, nil).Value(testKey)) + } +} + +type mockSpan struct { + trace.Span + mockSpanCtx *trace.SpanContext +} + +func (m *mockSpan) SpanContext() trace.SpanContext { + return *m.mockSpanCtx +} + +func TestClient_HandleRPC(_ *testing.T) { + client := &ClientHandler{} + ctx := context.Background() + rs := stats.OutHeader{} + + // Handle stats.RPCStats is not type of stats.OutHeader case + client.HandleRPC(context.TODO(), nil) + + // Handle context doesn't have the peerkey filled with a Peer instance + client.HandleRPC(ctx, &rs) + + // Handle context with the peerkey filled with a Peer instance + ip, _ := net.ResolveIPAddr("ip", "1.1.1.1") + ctx = peer.NewContext(ctx, &peer.Peer{ + Addr: ip, + }) + client.HandleRPC(ctx, &rs) + + // Handle context with Span + _, span := noop.NewTracerProvider().Tracer("Tracer").Start(ctx, "Spanname") + spanCtx := trace.SpanContext{} + spanID := [8]byte{12, 12, 12, 12, 12, 12, 12, 12} + traceID := [16]byte{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12} + spanCtx = spanCtx.WithTraceID(traceID) + spanCtx = spanCtx.WithSpanID(spanID) + mSpan := mockSpan{ + Span: span, + mockSpanCtx: &spanCtx, + } + ctx = trace.ContextWithSpan(ctx, &mSpan) + client.HandleRPC(ctx, &rs) +} diff --git a/contrib/otel/tracing/tracer.go b/contrib/otel/tracing/tracer.go new file mode 100644 index 0000000..eccc721 --- /dev/null +++ b/contrib/otel/tracing/tracer.go @@ -0,0 +1,82 @@ +package tracing + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v3/errors" +) + +// Tracer is otel span tracer +type Tracer struct { + tracer trace.Tracer + kind trace.SpanKind + opt *options +} + +// NewTracer create tracer instance +func NewTracer(kind trace.SpanKind, opts ...Option) *Tracer { + op := options{ + propagator: propagation.NewCompositeTextMapPropagator(Metadata{}, propagation.Baggage{}, propagation.TraceContext{}), + tracerName: "kratos", + } + for _, o := range opts { + o(&op) + } + if op.tracerProvider == nil { + op.tracerProvider = otel.GetTracerProvider() + } + + switch kind { + case trace.SpanKindClient: + return &Tracer{tracer: op.tracerProvider.Tracer(op.tracerName), kind: kind, opt: &op} + case trace.SpanKindServer: + return &Tracer{tracer: op.tracerProvider.Tracer(op.tracerName), kind: kind, opt: &op} + default: + panic(fmt.Sprintf("unsupported span kind: %v", kind)) + } +} + +// Start start tracing span +func (t *Tracer) Start(ctx context.Context, operation string, carrier propagation.TextMapCarrier) (context.Context, trace.Span) { + if t.kind == trace.SpanKindServer { + ctx = t.opt.propagator.Extract(ctx, carrier) + } + ctx, span := t.tracer.Start(ctx, + operation, + trace.WithSpanKind(t.kind), + ) + if t.kind == trace.SpanKindClient { + t.opt.propagator.Inject(ctx, carrier) + } + return ctx, span +} + +// End finish tracing span +func (t *Tracer) End(_ context.Context, span trace.Span, m any, err error) { + if err != nil { + span.RecordError(err) + if e := errors.FromError(err); e != nil { + span.SetAttributes(attribute.Key("rpc.status_code").Int64(int64(e.Code))) + } + span.SetStatus(codes.Error, err.Error()) + } else { + span.SetStatus(codes.Ok, "OK") + } + + if p, ok := m.(proto.Message); ok { + if t.kind == trace.SpanKindServer { + span.SetAttributes(attribute.Key("send_msg.size").Int(proto.Size(p))) + } else { + span.SetAttributes(attribute.Key("recv_msg.size").Int(proto.Size(p))) + } + } + span.End() +} diff --git a/contrib/otel/tracing/tracer_test.go b/contrib/otel/tracing/tracer_test.go new file mode 100644 index 0000000..a05ba5a --- /dev/null +++ b/contrib/otel/tracing/tracer_test.go @@ -0,0 +1,55 @@ +package tracing + +import ( + "context" + "errors" + "testing" + + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestNewTracer(t *testing.T) { + tracer := NewTracer(trace.SpanKindClient, func(o *options) { + o.tracerProvider = noop.NewTracerProvider() + }) + + if tracer.kind != trace.SpanKindClient { + t.Errorf("The tracer kind must be equal to trace.SpanKindClient, %v given.", tracer.kind) + } + + defer func() { + if recover() == nil { + t.Error("The NewTracer with an invalid SpanKindMustCrash must panic") + } + }() + _ = NewTracer(666, func(o *options) { + o.tracerProvider = noop.NewTracerProvider() + }) +} + +func TestTracer_End(_ *testing.T) { + tracer := NewTracer(trace.SpanKindClient, func(o *options) { + o.tracerProvider = noop.NewTracerProvider() + }) + ctx, span := noop.NewTracerProvider().Tracer("noop").Start(context.Background(), "noopSpan") + + // Handle with error case + tracer.End(ctx, span, nil, errors.New("dummy error")) + + // Handle without error case + tracer.End(ctx, span, nil, nil) + + m := &emptypb.Empty{} + + // Handle the trace KindServer + tracer = NewTracer(trace.SpanKindServer, func(o *options) { + o.tracerProvider = noop.NewTracerProvider() + }) + tracer.End(ctx, span, m, nil) + tracer = NewTracer(trace.SpanKindClient, func(o *options) { + o.tracerProvider = noop.NewTracerProvider() + }) + tracer.End(ctx, span, m, nil) +} diff --git a/contrib/otel/tracing/tracing.go b/contrib/otel/tracing/tracing.go new file mode 100644 index 0000000..1e2ecd3 --- /dev/null +++ b/contrib/otel/tracing/tracing.go @@ -0,0 +1,99 @@ +package tracing + +import ( + "context" + "log/slog" + + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +// Option is tracing option. +type Option func(*options) + +type options struct { + tracerName string + tracerProvider trace.TracerProvider + propagator propagation.TextMapPropagator +} + +// WithPropagator with tracer propagator. +func WithPropagator(propagator propagation.TextMapPropagator) Option { + return func(opts *options) { + opts.propagator = propagator + } +} + +// WithTracerProvider with tracer provider. +// By default, it uses the global provider that is set by otel.SetTracerProvider(provider). +func WithTracerProvider(provider trace.TracerProvider) Option { + return func(opts *options) { + opts.tracerProvider = provider + } +} + +// WithTracerName with tracer name +func WithTracerName(tracerName string) Option { + return func(opts *options) { + opts.tracerName = tracerName + } +} + +// Server returns a new server middleware for OpenTelemetry. +func Server(opts ...Option) middleware.Middleware { + tracer := NewTracer(trace.SpanKindServer, opts...) + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if tr, ok := transport.FromServerContext(ctx); ok { + var span trace.Span + ctx, span = tracer.Start(ctx, tr.Operation(), tr.RequestHeader()) + setServerSpan(ctx, span, req) + defer func() { tracer.End(ctx, span, reply, err) }() + } + return handler(ctx, req) + } + } +} + +// Client returns a new client middleware for OpenTelemetry. +func Client(opts ...Option) middleware.Middleware { + tracer := NewTracer(trace.SpanKindClient, opts...) + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if tr, ok := transport.FromClientContext(ctx); ok { + var span trace.Span + ctx, span = tracer.Start(ctx, tr.Operation(), tr.RequestHeader()) + setClientSpan(ctx, span, req) + defer func() { tracer.End(ctx, span, reply, err) }() + } + return handler(ctx, req) + } + } +} + +// TraceID returns the trace ID from ctx. +func TraceID(ctx context.Context) string { + if span := trace.SpanContextFromContext(ctx); span.HasTraceID() { + return span.TraceID().String() + } + return "" +} + +// SpanID returns the span ID from ctx. +func SpanID(ctx context.Context) string { + if span := trace.SpanContextFromContext(ctx); span.HasSpanID() { + return span.SpanID().String() + } + return "" +} + +// TraceAttrs returns slog attributes for the trace and span IDs in ctx. +func TraceAttrs(ctx context.Context) []slog.Attr { + return []slog.Attr{ + slog.String("trace_id", TraceID(ctx)), + slog.String("span_id", SpanID(ctx)), + } +} diff --git a/contrib/otel/tracing/tracing_test.go b/contrib/otel/tracing/tracing_test.go new file mode 100644 index 0000000..fe925dd --- /dev/null +++ b/contrib/otel/tracing/tracing_test.go @@ -0,0 +1,231 @@ +package tracing + +import ( + "context" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "go.opentelemetry.io/otel/propagation" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + + "github.com/go-kratos/kratos/v3/transport" +) + +var _ transport.Transporter = (*mockTransport)(nil) + +type headerCarrier http.Header + +// Get returns the value associated with the passed key. +func (hc headerCarrier) Get(key string) string { + return http.Header(hc).Get(key) +} + +// Set stores the key-value pair. +func (hc headerCarrier) Set(key string, value string) { + http.Header(hc).Set(key, value) +} + +// Add value to the key-value pair. +func (hc headerCarrier) Add(key string, value string) { + http.Header(hc).Add(key, value) +} + +// Keys lists the keys stored in this carrier. +func (hc headerCarrier) Keys() []string { + keys := make([]string, 0, len(hc)) + for k := range http.Header(hc) { + keys = append(keys, k) + } + return keys +} + +// Values returns a slice value associated with the passed key. +func (hc headerCarrier) Values(key string) []string { + return http.Header(hc).Values(key) +} + +type mockTransport struct { + kind transport.Kind + endpoint string + operation string + header headerCarrier + request *http.Request +} + +func (tr *mockTransport) Kind() transport.Kind { return tr.kind } +func (tr *mockTransport) Endpoint() string { return tr.endpoint } +func (tr *mockTransport) Operation() string { return tr.operation } +func (tr *mockTransport) RequestHeader() transport.Header { return tr.header } +func (tr *mockTransport) ReplyHeader() transport.Header { return tr.header } +func (tr *mockTransport) Request() *http.Request { + if tr.request == nil { + rq, _ := http.NewRequest(http.MethodGet, "/endpoint", nil) + + return rq + } + + return tr.request +} +func (tr *mockTransport) PathTemplate() string { return "" } +func (tr *mockTransport) Response() http.ResponseWriter { + return httptest.NewRecorder() +} + +func TestTracer(t *testing.T) { + carrier := headerCarrier{} + tp := tracesdk.NewTracerProvider(tracesdk.WithSampler(tracesdk.TraceIDRatioBased(0))) + + // caller use Inject + cliTracer := NewTracer( + trace.SpanKindClient, + WithTracerProvider(tp), + WithPropagator( + propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{}), + ), + ) + + ts := &mockTransport{kind: transport.KindHTTP, header: carrier} + + ctx, aboveSpan := cliTracer.Start(transport.NewClientContext(context.Background(), ts), ts.Operation(), ts.RequestHeader()) + defer cliTracer.End(ctx, aboveSpan, nil, nil) + + // server use Extract fetch traceInfo from carrier + svrTracer := NewTracer(trace.SpanKindServer, WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{}))) + ts = &mockTransport{kind: transport.KindHTTP, header: carrier} + + ctx, span := svrTracer.Start(transport.NewServerContext(ctx, ts), ts.Operation(), ts.RequestHeader()) + defer svrTracer.End(ctx, span, nil, nil) + + if aboveSpan.SpanContext().TraceID() != span.SpanContext().TraceID() { + t.Fatalf("TraceID failed to deliver") + } + + if v, ok := transport.FromClientContext(ctx); !ok || len(v.RequestHeader().Keys()) == 0 { + t.Fatalf("traceHeader failed to deliver") + } +} + +func TestServer(t *testing.T) { + tr := &mockTransport{ + kind: transport.KindHTTP, + endpoint: "server:2233", + operation: "/test.server/hello", + header: headerCarrier{}, + } + + tracer := NewTracer( + trace.SpanKindClient, + WithTracerProvider(tracesdk.NewTracerProvider()), + ) + + var ( + childSpanID string + childTraceID string + ) + next := func(ctx context.Context, req any) (any, error) { + attrs := TraceAttrs(ctx) + if len(attrs) != 2 { + t.Fatalf("TraceAttrs len = %d, want 2", len(attrs)) + } + childSpanID = SpanID(ctx) + childTraceID = TraceID(ctx) + return req.(string) + "https://go-kratos.dev", nil + } + + var ctx context.Context + ctx, span := tracer.Start( + transport.NewServerContext(context.Background(), tr), + tr.Operation(), + tr.RequestHeader(), + ) + + _, err := Server( + WithTracerProvider(tracesdk.NewTracerProvider()), + WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})), + )(next)(ctx, "test server: ") + + span.End() + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if childSpanID == "" { + t.Errorf("expected empty, got %v", childSpanID) + } + if reflect.DeepEqual(span.SpanContext().SpanID().String(), childSpanID) { + t.Errorf("span.SpanContext().SpanID().String()(%v) is not equal to childSpanID(%v)", span.SpanContext().SpanID().String(), childSpanID) + } + if !reflect.DeepEqual(span.SpanContext().TraceID().String(), childTraceID) { + t.Errorf("expected %v, got %v", childTraceID, span.SpanContext().TraceID().String()) + } + + _, err = Server( + WithTracerProvider(tracesdk.NewTracerProvider()), + WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})), + )(next)(context.Background(), "test server: ") + if err != nil { + t.Errorf("expected error, got nil") + } + if childSpanID != "" { + t.Errorf("expected empty, got %v", childSpanID) + } + if childTraceID != "" { + t.Errorf("expected empty, got %v", childTraceID) + } +} + +func TestClient(t *testing.T) { + tr := &mockTransport{ + kind: transport.KindHTTP, + endpoint: "server:2233", + operation: "/test.server/hello", + header: headerCarrier{}, + } + + tracer := NewTracer( + trace.SpanKindClient, + WithTracerProvider(tracesdk.NewTracerProvider()), + ) + + var ( + childSpanID string + childTraceID string + ) + next := func(ctx context.Context, req any) (any, error) { + attrs := TraceAttrs(ctx) + if len(attrs) != 2 { + t.Fatalf("TraceAttrs len = %d, want 2", len(attrs)) + } + childSpanID = SpanID(ctx) + childTraceID = TraceID(ctx) + return req.(string) + "https://go-kratos.dev", nil + } + + var ctx context.Context + ctx, span := tracer.Start( + transport.NewClientContext(context.Background(), tr), + tr.Operation(), + tr.RequestHeader(), + ) + + _, err := Client( + WithTracerProvider(tracesdk.NewTracerProvider()), + WithPropagator(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})), + )(next)(ctx, "test client: ") + + span.End() + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if childSpanID == "" { + t.Errorf("expected empty, got %v", childSpanID) + } + if reflect.DeepEqual(span.SpanContext().SpanID().String(), childSpanID) { + t.Errorf("span.SpanContext().SpanID().String()(%v) is not equal to childSpanID(%v)", span.SpanContext().SpanID().String(), childSpanID) + } + if !reflect.DeepEqual(span.SpanContext().TraceID().String(), childTraceID) { + t.Errorf("expected %v, got %v", childTraceID, span.SpanContext().TraceID().String()) + } +} diff --git a/contrib/polaris/config.go b/contrib/polaris/config.go new file mode 100644 index 0000000..8e7d62f --- /dev/null +++ b/contrib/polaris/config.go @@ -0,0 +1,127 @@ +package polaris + +import ( + "context" + "path/filepath" + "strings" + + "github.com/polarismesh/polaris-go" + "github.com/polarismesh/polaris-go/pkg/model" + + "github.com/go-kratos/kratos/v3/config" + "github.com/go-kratos/kratos/v3/log" +) + +// ConfigOption is polaris config option. +type ConfigOption func(o *configOptions) + +type configOptions struct { + namespace string + files []File + configFile []polaris.ConfigFile +} + +// WithConfigFile with polaris config file +func WithConfigFile(file ...File) ConfigOption { + return func(o *configOptions) { + o.files = file + } +} + +type File struct { + Name string + Group string +} + +type source struct { + client polaris.ConfigAPI + options *configOptions +} + +// Load return the config values +func (s *source) Load() ([]*config.KeyValue, error) { + kvs := make([]*config.KeyValue, 0, len(s.options.files)) + for _, file := range s.options.files { + configFile, err := s.client.FetchConfigFile(&polaris.GetConfigFileRequest{ + GetConfigFileRequest: &model.GetConfigFileRequest{ + Namespace: s.options.namespace, + FileGroup: file.Group, + FileName: file.Name, + Subscribe: true, + }, + }) + if err != nil { + return nil, err + } + s.options.configFile = append(s.options.configFile, configFile) + kvs = append(kvs, &config.KeyValue{ + Key: file.Name, + Value: []byte(configFile.GetContent()), + Format: strings.TrimPrefix(filepath.Ext(file.Name), "."), + }) + } + return kvs, nil +} + +// Watch return the watcher +func (s *source) Watch() (config.Watcher, error) { + return newConfigWatcher(s.options.configFile), nil +} + +type ConfigWatcher struct { + event chan model.ConfigFileChangeEvent + cfg []*config.KeyValue +} + +func receive(event chan model.ConfigFileChangeEvent) func(m model.ConfigFileChangeEvent) { + return func(m model.ConfigFileChangeEvent) { + defer func() { + if err := recover(); err != nil { + log.Error("panic recovered", "err", err) + } + }() + event <- m + } +} + +func newConfigWatcher(configFile []polaris.ConfigFile) *ConfigWatcher { + w := &ConfigWatcher{ + event: make(chan model.ConfigFileChangeEvent, len(configFile)), + } + for _, file := range configFile { + w.cfg = append(w.cfg, &config.KeyValue{ + Key: file.GetFileName(), + Value: []byte(file.GetContent()), + Format: strings.TrimPrefix(filepath.Ext(file.GetFileName()), "."), + }) + } + for _, file := range configFile { + file.AddChangeListener(receive(w.event)) + } + return w +} + +func (w *ConfigWatcher) Next() ([]*config.KeyValue, error) { + if event, ok := <-w.event; ok { + m := make(map[string]*config.KeyValue) + for _, file := range w.cfg { + m[file.Key] = file + } + m[event.ConfigFileMetadata.GetFileName()] = &config.KeyValue{ + Key: event.ConfigFileMetadata.GetFileName(), + Value: []byte(event.NewValue), + Format: strings.TrimPrefix(filepath.Ext(event.ConfigFileMetadata.GetFileName()), "."), + } + w.cfg = make([]*config.KeyValue, 0, len(m)) + for _, kv := range m { + w.cfg = append(w.cfg, kv) + } + return w.cfg, nil + } + return nil, context.Canceled +} + +func (w *ConfigWatcher) Stop() error { + close(w.event) + return nil +} diff --git a/contrib/polaris/config_test.go b/contrib/polaris/config_test.go new file mode 100644 index 0000000..1d1b319 --- /dev/null +++ b/contrib/polaris/config_test.go @@ -0,0 +1,386 @@ +package polaris + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strings" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/config" + + "github.com/polarismesh/polaris-go" +) + +var ( + testNamespace = "default" + testFileGroup = "test" + testOriginContent = `server: + port: 8080` + testUpdatedContent = `server: + port: 8090` + testCenterURL = "http://127.0.0.1:8090" +) + +func makeJSONRequest(uri string, data string, method string, headers map[string]string) ([]byte, error) { + client := http.Client{} + req, err := http.NewRequest(method, uri, strings.NewReader(data)) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + for k, v := range headers { + req.Header.Add(k, v) + } + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + return io.ReadAll(res.Body) +} + +type commonRes struct { + Code int32 `json:"code"` +} + +type LoginRes struct { + Code int32 `json:"code"` + LoginResponse struct { + Token string `json:"token"` + } `json:"loginResponse"` +} + +type configClient struct { + token string +} + +func newConfigClient() (*configClient, error) { + token, err := getToken(testCenterURL) + if err != nil { + return nil, err + } + return &configClient{ + token: token, + }, nil +} + +func getToken(testCenterURL string) (string, error) { + data, err := json.Marshal(map[string]string{ + "name": "polaris", + "password": "polaris", + }) + if err != nil { + return "", err + } + // login use default user + res, err := makeJSONRequest(fmt.Sprintf("%s/core/v1/user/login", testCenterURL), string(data), http.MethodPost, map[string]string{}) + if err != nil { + return "", nil + } + var loginRes LoginRes + if err = json.Unmarshal(res, &loginRes); err != nil { + return "", err + } + return loginRes.LoginResponse.Token, nil +} + +func (client *configClient) createConfigFile(name string) error { + data, err := json.Marshal(map[string]string{ + "name": name, + "namespace": testNamespace, + "group": testFileGroup, + "content": testOriginContent, + "modifyBy": "polaris", + "format": "yaml", + }) + if err != nil { + return err + } + res, err := makeJSONRequest(fmt.Sprintf("%s/config/v1/configfiles", testCenterURL), string(data), http.MethodPost, map[string]string{ + "X-Polaris-Token": client.token, + }) + if err != nil { + return err + } + + var resJSON commonRes + err = json.Unmarshal(res, &resJSON) + if err != nil { + return err + } + if resJSON.Code != 200000 { + return fmt.Errorf("create error, res: %s", string(res)) + } + return nil +} + +func (client *configClient) updateConfigFile(name string) error { + data, err := json.Marshal(map[string]string{ + "name": name, + "namespace": testNamespace, + "group": testFileGroup, + "content": testUpdatedContent, + "modifyBy": "polaris", + "format": "yaml", + }) + if err != nil { + return err + } + res, err := makeJSONRequest(fmt.Sprintf("%s/config/v1/configfiles", testCenterURL), string(data), http.MethodPut, map[string]string{ + "X-Polaris-Token": client.token, + }) + if err != nil { + return err + } + var resJSON commonRes + err = json.Unmarshal(res, &resJSON) + if err != nil { + return err + } + if resJSON.Code != 200000 { + return fmt.Errorf("update error, res: %s", string(res)) + } + return nil +} + +func (client *configClient) deleteConfigFile(name string) error { + data, err := json.Marshal(map[string]string{}) + if err != nil { + return err + } + url := fmt.Sprintf("%s/config/v1/configfiles?namespace=%s&group=%s&name=%s", testCenterURL, testNamespace, testFileGroup, name) + res, err := makeJSONRequest(url, string(data), http.MethodDelete, map[string]string{ + "X-Polaris-Token": client.token, + }) + if err != nil { + return err + } + var resJSON commonRes + err = json.Unmarshal(res, &resJSON) + if err != nil { + return err + } + if resJSON.Code != 200000 { + return fmt.Errorf("delete error, res: %s", string(res)) + } + return nil +} + +func (client *configClient) publishConfigFile(name string) error { + data, err := json.Marshal(map[string]string{ + "namespace": testNamespace, + "group": testFileGroup, + "fileName": name, + "name": name, + }) + if err != nil { + return err + } + res, err := makeJSONRequest(fmt.Sprintf("%s/config/v1/configfiles/release", testCenterURL), string(data), http.MethodPost, map[string]string{ + "X-Polaris-Token": client.token, + }) + if err != nil { + return err + } + var resJSON commonRes + err = json.Unmarshal(res, &resJSON) + if err != nil { + return err + } + if resJSON.Code != 200000 { + return fmt.Errorf("publish error, res: %s", string(res)) + } + return nil +} + +func TestConfig(t *testing.T) { + name := "kratos-polaris-test.yaml" + client, err := newConfigClient() + if err != nil { + t.Fatal(err) + } + _ = client.deleteConfigFile(name) + if err = client.createConfigFile(name); err != nil { + t.Fatal(err) + } + time.Sleep(5 * time.Second) + if err = client.publishConfigFile(name); err != nil { + t.Fatal(err) + } + + time.Sleep(5 * time.Second) + + // Always remember clear test resource + sdk, err := polaris.NewSDKContextByAddress("127.0.0.1:8091") + if err != nil { + t.Fatal(err) + } + p := New(sdk) + config, err := p.Config(WithConfigFile(File{Name: name, Group: testFileGroup})) + if err != nil { + t.Fatal(err) + } + kv, err := config.Load() + if err != nil { + t.Fatal(err) + } + + for _, value := range kv { + t.Logf("key: %s, value: %s", value.Key, value.Value) + } + if len(kv) != 1 || kv[0].Key != name || string(kv[0].Value) != testOriginContent { + t.Fatal("config error") + } + + w, err := config.Watch() + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err = client.deleteConfigFile(name) + if err != nil { + t.Fatal(err) + } + }) + + if err = client.updateConfigFile(name); err != nil { + t.Fatal(err) + } + + if err = client.publishConfigFile(name); err != nil { + t.Fatal(err) + } + + if kv, err = w.Next(); err != nil { + t.Fatal(err) + } + + for _, value := range kv { + t.Log(value.Key, string(value.Value)) + } + + if len(kv) != 1 || kv[0].Key != name || string(kv[0].Value) != testUpdatedContent { + t.Fatal("config error") + } +} + +func TestExtToFormat(t *testing.T) { + name := "kratos-polaris-ext.yaml" + client, err := newConfigClient() + if err != nil { + t.Fatal(err) + } + _ = client.deleteConfigFile(name) + if err = client.createConfigFile(name); err != nil { + t.Fatal(err) + } + if err = client.publishConfigFile(name); err != nil { + t.Fatal(err) + } + + // Always remember clear test resource + t.Cleanup(func() { + if err = client.deleteConfigFile(name); err != nil { + t.Fatal(err) + } + }) + + sdk, err := polaris.NewSDKContextByAddress("127.0.0.1:8091") + if err != nil { + t.Fatal(err) + } + p := New(sdk) + + cfg, err := p.Config(WithConfigFile(File{Name: name, Group: testFileGroup})) + if err != nil { + t.Fatal(err) + } + + kv, err := cfg.Load() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(len(kv), 1) { + t.Errorf("len(kvs) = %d", len(kv)) + } + if !reflect.DeepEqual(name, kv[0].Key) { + t.Errorf("kvs[0].Key is %s", kv[0].Key) + } + if !reflect.DeepEqual(testOriginContent, string(kv[0].Value)) { + t.Errorf("kvs[0].Value is %s", kv[0].Value) + } + if !reflect.DeepEqual("yaml", kv[0].Format) { + t.Errorf("kvs[0].Format is %s", kv[0].Format) + } +} + +func TestGetMultipleConfig(t *testing.T) { + client, err := newConfigClient() + files := make([]File, 0, 3) + for i := 0; i < 3; i++ { + name := fmt.Sprintf("kratos-polaris-test-%d.yaml", i) + if err != nil { + t.Fatal(err) + } + _ = client.deleteConfigFile(name) + if err = client.createConfigFile(name); err != nil { + t.Fatal(err) + } + if err = client.publishConfigFile(name); err != nil { + t.Fatal(err) + } + files = append(files, File{Name: name, Group: testFileGroup}) + } + + sdk, err := polaris.NewSDKContextByAddress("127.0.0.1:8091") + if err != nil { + t.Fatal(err) + } + p := New(sdk, WithNamespace("default")) + + cfg, err := p.Config(WithConfigFile(files...)) + if err != nil { + t.Fatal(err) + } + + kvs, err := cfg.Load() + if err != nil { + t.Fatal(err) + } + for _, kv := range kvs { + t.Logf("key: %s, value: %s", kv.Key, kv.Value) + } + + w, err := cfg.Watch() + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + if err = client.publishConfigFile(file.Name); err != nil { + t.Fatal(err) + } + kvs, err := w.Next() + if err != nil { + t.Fatal(err) + } + m := make(map[string]*config.KeyValue) + for _, kv := range kvs { + m[kv.Key] = kv + } + if !reflect.DeepEqual(file.Name, m[file.Name].Key) { + t.Errorf("m[file.Name].Key is %s", m[file.Name].Key) + } + if !reflect.DeepEqual(testOriginContent, string(m[file.Name].Value)) { + t.Errorf("m[file.Name].Value is %s", m[file.Name].Value) + } + if !reflect.DeepEqual("yaml", m[file.Name].Format) { + t.Errorf("m[file.Name].Format is %s", m[file.Name].Format) + } + } +} diff --git a/contrib/polaris/go.mod b/contrib/polaris/go.mod new file mode 100644 index 0000000..d180dd5 --- /dev/null +++ b/contrib/polaris/go.mod @@ -0,0 +1,49 @@ +module github.com/go-kratos/kratos/contrib/polaris/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/google/uuid v1.6.0 + github.com/polarismesh/polaris-go v1.7.0 + github.com/polarismesh/specification v1.8.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/go-playground/form/v4 v4.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/natefinch/lumberjack v2.0.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // 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/procfs v0.20.1 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../ diff --git a/contrib/polaris/go.sum b/contrib/polaris/go.sum new file mode 100644 index 0000000..b38d8c5 --- /dev/null +++ b/contrib/polaris/go.sum @@ -0,0 +1,1022 @@ +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 v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +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/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +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/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +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/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +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= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +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.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +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/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= +github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg= +github.com/gonum/integrate v0.0.0-20181209220457-a422b5c0fdf2/go.mod h1:pDgmNM6seYpwvPos3q+zxlXMsbve6mOIPucUnUOrI7Y= +github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks= +github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A= +github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw= +github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b/go.mod h1:Z4GIJBJO3Wa4gD4vbwQxXXZ+WHmW6E9ixmNrwvs0iZs= +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/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/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/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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/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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polarismesh/polaris-go v1.7.0 h1:iiaKcYWXrRWIOqoBBgAQVswhPxwOkIjI6P6nMVh+d14= +github.com/polarismesh/polaris-go v1.7.0/go.mod h1:up/F6sUGOXzd9pMDkkxtPFrMq3lmdz0U8BvEtvWLKCA= +github.com/polarismesh/specification v1.7.0/go.mod h1:rDvMMtl5qebPmqiBLNa5Ps0XtwkP31ZLirbH4kXA0YU= +github.com/polarismesh/specification v1.8.0 h1:k112R8eUZfdv1tVthWcf5AbE0SR/afyETz6Eii5OFw8= +github.com/polarismesh/specification v1.8.0/go.mod h1:Gb9OLXOELG4VwhxPh4DK6C0C3Z7FFYCmVkTW/d6+kvM= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/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.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-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-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/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-20181114220301-adae6a3d119a/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-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-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +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-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +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-20220601150217-0de741cfad7f/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20200106162015-b016eb3dc98e/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/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-20210124154548-22da62e12c0c/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +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-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-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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/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/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= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +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.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +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-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +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.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-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/contrib/polaris/limiter.go b/contrib/polaris/limiter.go new file mode 100644 index 0000000..dc20548 --- /dev/null +++ b/contrib/polaris/limiter.go @@ -0,0 +1,105 @@ +package polaris + +import ( + "time" + + "github.com/go-kratos/kratos/v3/middleware/ratelimit" + + "github.com/polarismesh/polaris-go" + "github.com/polarismesh/polaris-go/pkg/model" +) + +type ( + // LimiterOption function for polaris limiter + LimiterOption func(*limiterOptions) +) + +type limiterOptions struct { + // required, polaris limit namespace + namespace string + + // required, polaris limit service name + service string + + // optional, polaris limit request timeout + // max value is (1+RetryCount) * Timeout + timeout time.Duration + + // optional, polaris limit retryCount + // init by polaris config + retryCount int + + // optional, request limit quota + token uint32 +} + +// WithLimiterNamespace with limiter namespace. +func WithLimiterNamespace(namespace string) LimiterOption { + return func(o *limiterOptions) { + o.namespace = namespace + } +} + +// WithLimiterService with limiter service. +func WithLimiterService(service string) LimiterOption { + return func(o *limiterOptions) { + o.service = service + } +} + +// WithLimiterTimeout with limiter arguments. +func WithLimiterTimeout(timeout time.Duration) LimiterOption { + return func(o *limiterOptions) { + o.timeout = timeout + } +} + +// WithLimiterRetryCount with limiter retryCount. +func WithLimiterRetryCount(retryCount int) LimiterOption { + return func(o *limiterOptions) { + o.retryCount = retryCount + } +} + +// WithLimiterToken with limiter token. +func WithLimiterToken(token uint32) LimiterOption { + return func(o *limiterOptions) { + o.token = token + } +} + +type Limiter struct { + // polaris limit api + limitAPI polaris.LimitAPI + + opts limiterOptions +} + +// init quotaRequest +func buildRequest(opts limiterOptions) polaris.QuotaRequest { + quotaRequest := polaris.NewQuotaRequest() + quotaRequest.SetNamespace(opts.namespace) + quotaRequest.SetRetryCount(opts.retryCount) + quotaRequest.SetService(opts.service) + quotaRequest.SetTimeout(opts.timeout) + quotaRequest.SetToken(opts.token) + return quotaRequest +} + +// Allow interface impl +func (l *Limiter) Allow(method string, argument ...model.Argument) (ratelimit.DoneFunc, error) { + request := buildRequest(l.opts) + request.SetMethod(method) + for _, arg := range argument { + request.AddArgument(arg) + } + resp, err := l.limitAPI.GetQuota(request) + if err != nil { + // ignore err + return func(ratelimit.DoneInfo) {}, nil + } + if resp.Get().Code == model.QuotaResultOk { + return func(ratelimit.DoneInfo) {}, nil + } + return nil, ratelimit.ErrLimitExceed +} diff --git a/contrib/polaris/polaris.go b/contrib/polaris/polaris.go new file mode 100644 index 0000000..e6023fc --- /dev/null +++ b/contrib/polaris/polaris.go @@ -0,0 +1,101 @@ +package polaris + +import ( + "errors" + + "github.com/polarismesh/polaris-go" + "github.com/polarismesh/polaris-go/api" + + "github.com/go-kratos/kratos/v3/config" +) + +type Polaris struct { + router polaris.RouterAPI + config polaris.ConfigAPI + limit polaris.LimitAPI + registry polaris.ProviderAPI + discovery polaris.ConsumerAPI + namespace string + service string +} + +// Option is polaris option. +type Option func(o *Polaris) + +// WithNamespace with polaris global testNamespace +func WithNamespace(ns string) Option { + return func(o *Polaris) { + o.namespace = ns + } +} + +// WithService set the current service name +func WithService(service string) Option { + return func(o *Polaris) { + o.service = service + } +} + +// New polaris Service governance. +func New(sdk api.SDKContext, opts ...Option) Polaris { + op := Polaris{ + router: polaris.NewRouterAPIByContext(sdk), + config: polaris.NewConfigAPIByContext(sdk), + limit: polaris.NewLimitAPIByContext(sdk), + registry: polaris.NewProviderAPIByContext(sdk), + discovery: polaris.NewConsumerAPIByContext(sdk), + namespace: "default", + } + for _, option := range opts { + option(&op) + } + return op +} + +func (p *Polaris) Config(opts ...ConfigOption) (config.Source, error) { + options := &configOptions{ + namespace: p.namespace, + } + + for _, opt := range opts { + opt(options) + } + + if len(options.files) == 0 { + return nil, errors.New("fileNames invalid") + } + + return &source{ + client: p.config, + options: options, + }, nil +} + +func (p *Polaris) Registry(opts ...RegistryOption) (r *Registry) { + op := registryOptions{ + Namespace: p.namespace, + Healthy: true, + } + for _, option := range opts { + option(&op) + } + return &Registry{ + opt: op, + provider: p.registry, + consumer: p.discovery, + } +} + +func (p *Polaris) Limiter(opts ...LimiterOption) (r Limiter) { + op := limiterOptions{ + namespace: p.namespace, + service: p.service, + } + for _, option := range opts { + option(&op) + } + return Limiter{ + limitAPI: p.limit, + opts: op, + } +} diff --git a/contrib/polaris/ratelimit.go b/contrib/polaris/ratelimit.go new file mode 100644 index 0000000..6949bad --- /dev/null +++ b/contrib/polaris/ratelimit.go @@ -0,0 +1,52 @@ +package polaris + +import ( + "context" + "strings" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/middleware/ratelimit" + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/http" + + "github.com/polarismesh/polaris-go/pkg/model" +) + +// ErrLimitExceed is service unavailable due to rate limit exceeded. +var ( + ErrLimitExceed = errors.New(429, "RATELIMIT", "service unavailable due to rate limit exceeded") +) + +// Ratelimit Request rate limit middleware +func Ratelimit(l Limiter) middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if tr, ok := transport.FromServerContext(ctx); ok { + var args []model.Argument + headers := tr.RequestHeader() + // handle header + for _, header := range headers.Keys() { + args = append(args, model.BuildHeaderArgument(header, headers.Get(header))) + } + // handle http + if ht, ok := tr.(*http.Transport); ok { + // url query + for key, values := range ht.Request().URL.Query() { + args = append(args, model.BuildQueryArgument(key, strings.Join(values, ","))) + } + } + done, e := l.Allow(tr.Operation(), args...) + if e != nil { + // rejected + return nil, ErrLimitExceed + } + // allowed + reply, err = handler(ctx, req) + done(ratelimit.DoneInfo{Err: err}) + return + } + return reply, nil + } + } +} diff --git a/contrib/polaris/registry.go b/contrib/polaris/registry.go new file mode 100644 index 0000000..cb3e285 --- /dev/null +++ b/contrib/polaris/registry.go @@ -0,0 +1,394 @@ +package polaris + +import ( + "context" + "net" + "net/url" + "strconv" + "time" + + "github.com/google/uuid" + "github.com/polarismesh/polaris-go" + "github.com/polarismesh/polaris-go/pkg/model" + + "github.com/go-kratos/kratos/v3/registry" +) + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +type registryOptions struct { + // required, testNamespace in polaris + Namespace string + + // required, service access token + ServiceToken string + + // service weight in polaris. Default value is 100, 0 <= weight <= 10000 + Weight int + + // service priority. Default value is 0. The smaller the value, the lower the priority + Priority int + + // To show service is healthy or not. Default value is True . + Healthy bool + + // To show service is isolate or not. Default value is False . + Isolate bool + + // TTL timeout. if node needs to use heartbeat to report,required. If not set,server will throw ErrorCode-400141 + TTL int + + // optional, Timeout for single query. Default value is global config + // Total is (1+RetryCount) * Timeout + Timeout time.Duration + + // optional, retry count. Default value is global config + RetryCount int +} + +// RegistryOption is polaris option. +type RegistryOption func(o *registryOptions) + +// Registry is polaris registry. +type Registry struct { + opt registryOptions + provider polaris.ProviderAPI + consumer polaris.ConsumerAPI +} + +// WithRegistryServiceToken with ServiceToken option. +func WithRegistryServiceToken(serviceToken string) RegistryOption { + return func(o *registryOptions) { o.ServiceToken = serviceToken } +} + +// WithRegistryWeight with Weight option. +func WithRegistryWeight(weight int) RegistryOption { + return func(o *registryOptions) { o.Weight = weight } +} + +// WithRegistryHealthy with Healthy option. +func WithRegistryHealthy(healthy bool) RegistryOption { + return func(o *registryOptions) { o.Healthy = healthy } +} + +// WithRegistryIsolate with Isolate option. +func WithRegistryIsolate(isolate bool) RegistryOption { + return func(o *registryOptions) { o.Isolate = isolate } +} + +// WithRegistryTTL with TTL option. +func WithRegistryTTL(TTL int) RegistryOption { + return func(o *registryOptions) { o.TTL = TTL } +} + +// WithRegistryTimeout with Timeout option. +func WithRegistryTimeout(timeout time.Duration) RegistryOption { + return func(o *registryOptions) { o.Timeout = timeout } +} + +// WithRegistryRetryCount with RetryCount option. +func WithRegistryRetryCount(retryCount int) RegistryOption { + return func(o *registryOptions) { o.RetryCount = retryCount } +} + +// Register the registration. +func (r *Registry) Register(_ context.Context, instance *registry.ServiceInstance) error { + id := uuid.NewString() + for _, endpoint := range instance.Endpoints { + u, err := url.Parse(endpoint) + if err != nil { + return err + } + + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return err + } + + portNum, err := strconv.Atoi(port) + if err != nil { + return err + } + + // metadata + rmd := mapClone(instance.Metadata) + if rmd == nil { + rmd = make(map[string]string) + } + rmd["merge"] = id + if _, ok := rmd["weight"]; !ok { + rmd["weight"] = strconv.Itoa(r.opt.Weight) + } + + weight, _ := strconv.Atoi(rmd["weight"]) + + _, err = r.provider.RegisterInstance( + &polaris.InstanceRegisterRequest{ + InstanceRegisterRequest: model.InstanceRegisterRequest{ + Service: instance.Name, + ServiceToken: r.opt.ServiceToken, + Namespace: r.opt.Namespace, + Host: host, + Port: portNum, + Protocol: &u.Scheme, + Weight: &weight, + Priority: &r.opt.Priority, + Version: &instance.Version, + Metadata: rmd, + Healthy: &r.opt.Healthy, + Isolate: &r.opt.Isolate, + TTL: &r.opt.TTL, + Timeout: &r.opt.Timeout, + RetryCount: &r.opt.RetryCount, + }, + }, + ) + if err != nil { + return err + } + } + return nil +} + +// Deregister the registration. +func (r *Registry) Deregister(_ context.Context, serviceInstance *registry.ServiceInstance) error { + for _, endpoint := range serviceInstance.Endpoints { + // get url + u, err := url.Parse(endpoint) + if err != nil { + return err + } + + // get host and port + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return err + } + + // port to int + portNum, err := strconv.Atoi(port) + if err != nil { + return err + } + // Deregister + err = r.provider.Deregister( + &polaris.InstanceDeRegisterRequest{ + InstanceDeRegisterRequest: model.InstanceDeRegisterRequest{ + Service: serviceInstance.Name, + ServiceToken: r.opt.ServiceToken, + Namespace: r.opt.Namespace, + Host: host, + Port: portNum, + Timeout: &r.opt.Timeout, + RetryCount: &r.opt.RetryCount, + }, + }, + ) + if err != nil { + return err + } + } + return nil +} + +// GetService return the service instances in memory according to the service name. +func (r *Registry) GetService(_ context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + // get all instances + instancesResponse, err := r.consumer.GetAllInstances(&polaris.GetAllInstancesRequest{ + GetAllInstancesRequest: model.GetAllInstancesRequest{ + Service: serviceName, + Namespace: r.opt.Namespace, + Timeout: &r.opt.Timeout, + RetryCount: &r.opt.RetryCount, + }, + }) + if err != nil { + return nil, err + } + + serviceInstances := instancesToServiceInstances(merge(instancesResponse.GetInstances())) + + return serviceInstances, nil +} + +func merge(instances []model.Instance) map[string][]model.Instance { + m := make(map[string][]model.Instance) + for _, instance := range instances { + if v, ok := m[instance.GetMetadata()["merge"]]; ok { + m[instance.GetMetadata()["merge"]] = append(v, instance) + } else { + m[instance.GetMetadata()["merge"]] = []model.Instance{instance} + } + } + return m +} + +// Watch creates a watcher according to the service name. +func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + return newWatcher(ctx, r.opt.Namespace, serviceName, r.consumer) +} + +type Watcher struct { + ServiceName string + Namespace string + Ctx context.Context + Cancel context.CancelFunc + Channel <-chan model.SubScribeEvent + service *model.InstancesResponse + ServiceInstances map[string][]model.Instance + first bool +} + +func newWatcher(ctx context.Context, namespace string, serviceName string, consumer polaris.ConsumerAPI) (*Watcher, error) { + watchServiceResponse, err := consumer.WatchService(&polaris.WatchServiceRequest{ + WatchServiceRequest: model.WatchServiceRequest{ + Key: model.ServiceKey{ + Namespace: namespace, + Service: serviceName, + }, + }, + }) + if err != nil { + return nil, err + } + + w := &Watcher{ + Namespace: namespace, + ServiceName: serviceName, + Channel: watchServiceResponse.EventChannel, + service: watchServiceResponse.GetAllInstancesResp, + ServiceInstances: merge(watchServiceResponse.GetAllInstancesResp.GetInstances()), + } + w.Ctx, w.Cancel = context.WithCancel(ctx) + return w, nil +} + +// Next returns services in the following two cases: +// 1.the first time to watch and the service instance list is not empty. +// 2.any service instance changes found. +// if the above two conditions are not met, it will block until context deadline exceeded or canceled +func (w *Watcher) Next() ([]*registry.ServiceInstance, error) { + if !w.first { + w.first = true + if len(w.ServiceInstances) > 0 { + return instancesToServiceInstances(w.ServiceInstances), nil + } + } + select { + case <-w.Ctx.Done(): + return nil, w.Ctx.Err() + case event := <-w.Channel: + if event.GetSubScribeEventType() == model.EventInstance { + // this always true, but we need to check it to make sure EventType not change + if instanceEvent, ok := event.(*model.InstanceEvent); ok { + // handle DeleteEvent + if instanceEvent.DeleteEvent != nil { + for _, instance := range instanceEvent.DeleteEvent.Instances { + delete(w.ServiceInstances, instance.GetMetadata()["merge"]) + } + } + // handle UpdateEvent + if instanceEvent.UpdateEvent != nil { + for _, update := range instanceEvent.UpdateEvent.UpdateList { + if v, ok := w.ServiceInstances[update.After.GetMetadata()["merge"]]; ok { + var nv []model.Instance + m := map[string]model.Instance{} + for _, ins := range v { + m[ins.GetId()] = ins + } + m[update.After.GetId()] = update.After + for _, ins := range m { + if ins.IsHealthy() { + nv = append(nv, ins) + } + } + w.ServiceInstances[update.After.GetMetadata()["merge"]] = nv + if len(nv) == 0 { + delete(w.ServiceInstances, update.After.GetMetadata()["merge"]) + } + } else { + if update.After.IsHealthy() { + w.ServiceInstances[update.After.GetMetadata()["merge"]] = []model.Instance{update.After} + } + } + } + } + // handle AddEvent + if instanceEvent.AddEvent != nil { + for _, instance := range instanceEvent.AddEvent.Instances { + if v, ok := w.ServiceInstances[instance.GetMetadata()["merge"]]; ok { + var nv []model.Instance + m := map[string]model.Instance{} + for _, ins := range v { + m[ins.GetId()] = ins + } + m[instance.GetId()] = instance + for _, ins := range m { + if ins.IsHealthy() { + nv = append(nv, ins) + } + } + if len(nv) != 0 { + w.ServiceInstances[instance.GetMetadata()["merge"]] = nv + } + } else { + if instance.IsHealthy() { + w.ServiceInstances[instance.GetMetadata()["merge"]] = []model.Instance{instance} + } + } + } + } + } + return instancesToServiceInstances(w.ServiceInstances), nil + } + } + return instancesToServiceInstances(w.ServiceInstances), nil +} + +// Stop close the watcher. +func (w *Watcher) Stop() error { + w.Cancel() + return nil +} + +func instancesToServiceInstances(instances map[string][]model.Instance) []*registry.ServiceInstance { + serviceInstances := make([]*registry.ServiceInstance, 0, len(instances)) + for _, inss := range instances { + if len(inss) == 0 { + continue + } + ins := ®istry.ServiceInstance{ + ID: inss[0].GetId(), + Name: inss[0].GetService(), + Version: inss[0].GetVersion(), + Metadata: inss[0].GetMetadata(), + } + for _, item := range inss { + if item.IsHealthy() { + ins.Endpoints = append(ins.Endpoints, item.GetProtocol()+"://"+net.JoinHostPort(item.GetHost(), strconv.FormatUint(uint64(item.GetPort()), 10))) + } + } + if len(ins.Endpoints) != 0 { + serviceInstances = append(serviceInstances, ins) + } + } + return serviceInstances +} + +// Clone returns a copy of m. This is a shallow clone: +// the new keys and values are set using ordinary assignment. +func mapClone[M ~map[K]V, K comparable, V any](m M) M { + // Preserve nil in case it matters. + if m == nil { + return nil + } + // Make a shallow copy of the map. + m2 := make(M, len(m)) + for k, v := range m { + m2[k] = v + } + return m2 +} diff --git a/contrib/polaris/registry_test.go b/contrib/polaris/registry_test.go new file mode 100644 index 0000000..f7eb078 --- /dev/null +++ b/contrib/polaris/registry_test.go @@ -0,0 +1,73 @@ +package polaris + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/polarismesh/polaris-go" + + "github.com/go-kratos/kratos/v3/registry" +) + +// TestRegistry +func TestRegistry(t *testing.T) { + sdk, err := polaris.NewSDKContextByAddress("127.0.0.1:8091") + if err != nil { + t.Fatal(err) + } + + p := New(sdk) + + r := p.Registry( + WithRegistryTimeout(time.Second), + WithRegistryHealthy(true), + WithRegistryIsolate(false), + WithRegistryRetryCount(3), + WithRegistryWeight(100), + WithRegistryTTL(1000), + ) + + mm := map[string]string{ + "test1": "test1", + } + ins := ®istry.ServiceInstance{ + ID: "test-ut", + Name: "test-ut", + Version: "v1.0.0", + Endpoints: []string{ + "grpc://127.0.0.1:8080", + "http://127.0.0.1:9090", + }, + Metadata: mm, + } + + go func() { + for i := 0; true; i++ { + str := "test" + strconv.Itoa(i) + _ = mm[str] + if i > 100 { + i = 0 + } + } + }() + + err = r.Register(context.Background(), ins) + + t.Cleanup(func() { + if err = r.Deregister(context.Background(), ins); err != nil { + t.Fatal(err) + } + }) + + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 3) + service, err := r.GetService(context.Background(), "test-ut") + if err != nil { + t.Fatal(err) + } + t.Log(service) +} diff --git a/contrib/polaris/router.go b/contrib/polaris/router.go new file mode 100644 index 0000000..c0922ae --- /dev/null +++ b/contrib/polaris/router.go @@ -0,0 +1,257 @@ +package polaris + +import ( + "context" + "net" + "strconv" + "strings" + + "github.com/polarismesh/polaris-go" + polarisconfig "github.com/polarismesh/polaris-go/pkg/config" + "github.com/polarismesh/polaris-go/pkg/model" + "github.com/polarismesh/polaris-go/pkg/model/local" + "github.com/polarismesh/polaris-go/pkg/model/pb" + "github.com/polarismesh/polaris-go/pkg/plugin/common" + "github.com/polarismesh/polaris-go/pkg/plugin/servicerouter" + v1 "github.com/polarismesh/specification/source/go/api/v1/service_manage" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/go-kratos/kratos/v3" + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/http" +) + +type router struct { + service string +} + +type RouterOption func(o *router) + +// WithRouterService set the caller service name used by the route +func WithRouterService(service string) RouterOption { + return func(o *router) { + o.service = service + } +} + +// NodeFilter polaris dynamic router selector +func (p *Polaris) NodeFilter(opts ...RouterOption) selector.NodeFilter { + o := router{service: p.service} + for _, opt := range opts { + opt(&o) + } + return func(ctx context.Context, nodes []selector.Node) []selector.Node { + if len(nodes) == 0 { + return nodes + } + sourceService := &model.ServiceInfo{ + Namespace: p.namespace, + Service: o.service, + Metadata: map[string]string{}, + } + if appInfo, ok := kratos.FromContext(ctx); ok { + sourceService.Service = appInfo.Name() + } + + sourceService.AddArgument(model.BuildCallerServiceArgument(p.namespace, sourceService.Service)) + + // process transport + if tr, ok := transport.FromClientContext(ctx); ok { + sourceService.AddArgument(model.BuildMethodArgument(tr.Operation())) + sourceService.AddArgument(model.BuildPathArgument(tr.Operation())) + + for _, key := range tr.RequestHeader().Keys() { + sourceService.AddArgument(model.BuildHeaderArgument(strings.ToLower(key), tr.RequestHeader().Get(key))) + } + + // http + if ht, ok := tr.(http.Transporter); ok { + sourceService.AddArgument(model.BuildPathArgument(ht.Request().URL.Path)) + sourceService.AddArgument(model.BuildCallerIPArgument(ht.Request().RemoteAddr)) + + // cookie + for _, cookie := range ht.Request().Cookies() { + sourceService.AddArgument(model.BuildCookieArgument(cookie.Name, cookie.Value)) + } + + // url query + for key, values := range ht.Request().URL.Query() { + sourceService.AddArgument(model.BuildQueryArgument(key, strings.Join(values, ","))) + } + } + } + + n := make(map[string]selector.Node, len(nodes)) + for _, node := range nodes { + n[node.Address()] = node + } + + instances, err := p.processRouters(sourceService, buildPolarisInstance(p.namespace, nodes)) + if err != nil { + log.Error("polaris process routers failed", "error", err) + return nodes + } + + newNode := make([]selector.Node, 0, len(instances)) + for _, ins := range instances { + if v, ok := n[net.JoinHostPort(ins.GetHost(), strconv.FormatUint(uint64(ins.GetPort()), 10))]; ok { + newNode = append(newNode, v) + } + } + if len(newNode) == 0 { + return nodes + } + return newNode + } +} + +func (p *Polaris) processRouters(sourceService *model.ServiceInfo, dstInstances *pb.ServiceInstancesInProto) ([]model.Instance, error) { + if dstInstances == nil { + return nil, nil + } + plugins := p.discovery.SDKContext().GetPlugins() + ruleBasedRouter, err := plugins.GetPlugin(common.TypeServiceRouter, polarisconfig.DefaultServiceRouterRuleBased) + if err != nil { + return nil, err + } + filterOnlyRouter, err := plugins.GetPlugin(common.TypeServiceRouter, polarisconfig.DefaultServiceRouterFilterOnly) + if err != nil { + return nil, err + } + dstRouteRule, err := p.getRouteRule(dstInstances) + if err != nil { + return nil, err + } + routeInfo := &servicerouter.RouteInfo{ + SourceService: sourceService, + DestService: dstInstances, + DestRouteRule: dstRouteRule, + FilterOnlyRouter: filterOnlyRouter.(servicerouter.ServiceRouter), + } + if sourceService.HasService() { + sourceRouteRule, routeErr := p.getRouteRule(sourceService) + if routeErr != nil { + return nil, routeErr + } + routeInfo.SourceRouteRule = sourceRouteRule + } + routeInfo.Init(plugins) + result, err := servicerouter.GetFilterCluster( + p.discovery.SDKContext().GetValueContext(), + []servicerouter.ServiceRouter{ruleBasedRouter.(servicerouter.ServiceRouter)}, + routeInfo, + dstInstances.GetServiceClusters(), + ) + if err != nil { + return nil, err + } + if result == nil || result.OutputCluster == nil { + return nil, nil + } + instances, _ := result.OutputCluster.GetInstances() + return instances, nil +} + +func (p *Polaris) getRouteRule(service model.ServiceMetadata) (model.ServiceRule, error) { + resp, err := p.discovery.GetRouteRule(&polaris.GetServiceRuleRequest{ + GetServiceRuleRequest: model.GetServiceRuleRequest{ + Namespace: service.GetNamespace(), + Service: service.GetService(), + }, + }) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return &serviceRuleResponse{resp: resp}, nil +} + +type serviceRuleResponse struct { + resp *model.ServiceRuleResponse +} + +func (r serviceRuleResponse) GetType() model.EventType { + return r.resp.Type +} + +func (r serviceRuleResponse) IsInitialized() bool { + return true +} + +func (r serviceRuleResponse) GetRevision() string { + return r.resp.Revision +} + +func (r serviceRuleResponse) GetHashValue() uint64 { + return r.resp.HashValue +} + +func (r serviceRuleResponse) IsNotExists() bool { + return r.resp.NotExists +} + +func (r serviceRuleResponse) GetNamespace() string { + return r.resp.Service.Namespace +} + +func (r serviceRuleResponse) GetService() string { + return r.resp.Service.Service +} + +func (r serviceRuleResponse) GetValue() interface{} { + return r.resp.Value +} + +func (r serviceRuleResponse) GetRuleCache() model.RuleCache { + return r.resp.RuleCache +} + +func (r serviceRuleResponse) GetValidateError() error { + return r.resp.ValidateError +} + +func (r serviceRuleResponse) IsCacheLoaded() bool { + return false +} + +func buildPolarisInstance(namespace string, nodes []selector.Node) *pb.ServiceInstancesInProto { + ins := make([]*v1.Instance, 0, len(nodes)) + for _, node := range nodes { + host, port, err := net.SplitHostPort(node.Address()) + if err != nil { + log.Error("split host port failed", "error", err) + return nil + } + portUint64, err := strconv.ParseUint(port, 10, 32) + if err != nil { + log.Error("parse port failed", "error", err) + return nil + } + ins = append(ins, &v1.Instance{ + Id: wrapperspb.String(node.Metadata()["merge"]), + Service: wrapperspb.String(node.ServiceName()), + Namespace: wrapperspb.String(namespace), + Host: wrapperspb.String(host), + Port: wrapperspb.UInt32(uint32(portUint64)), + Protocol: wrapperspb.String(node.Scheme()), + Version: wrapperspb.String(node.Version()), + Weight: wrapperspb.UInt32(uint32(*node.InitialWeight())), + Metadata: node.Metadata(), + }) + } + + d := &v1.DiscoverResponse{ + Code: wrapperspb.UInt32(1), + Info: wrapperspb.String("ok"), + Type: v1.DiscoverResponse_INSTANCE, + Service: &v1.Service{Name: wrapperspb.String(nodes[0].ServiceName()), Namespace: wrapperspb.String(namespace)}, + Instances: ins, + } + return pb.NewServiceInstancesInProto(d, func(string) local.InstanceLocalValue { + return local.NewInstanceLocalValue() + }, &pb.SvcPluginValues{Routers: nil, Loadbalancer: nil}, nil) +} diff --git a/contrib/polaris/router_test.go b/contrib/polaris/router_test.go new file mode 100644 index 0000000..982d7d3 --- /dev/null +++ b/contrib/polaris/router_test.go @@ -0,0 +1,205 @@ +package polaris + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/polarismesh/polaris-go" + + "github.com/go-kratos/kratos/v3" + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" +) + +func TestRouter(t *testing.T) { + token, err := getToken("http://127.0.0.1:8090") + if err != nil { + t.Fatal(err) + } + data := ` +[ + { + "name":"kratos", + "enable":false, + "description":"123", + "priority":2, + "routing_config":{ + "@type":"type.googleapis.com/v2.RuleRoutingConfig", + "sources":[ + { + "service":"*", + "namespace":"*", + "arguments":[ + ] + } + ], + "destinations":[ + { + "labels":{ + "az":{ + "value":"1", + "value_type":"TEXT", + "type":"EXACT" + } + }, + "weight":100, + "priority":1, + "isolate":false, + "name":"instance-group-1", + "namespace":"default", + "service":"test-ut" + } + ] + } + } +] +` + res, err := makeJSONRequest("http://127.0.0.1:8090/naming/v2/routings", data, http.MethodPost, map[string]string{ + "X-Polaris-Token": token, + }) + if err != nil { + t.Fatal(err) + } + + resJSON := struct { + Code int `json:"code"` + Responses []struct { + Data struct { + ID string `json:"id"` + } + } `json:"responses"` + }{} + + err = json.Unmarshal(res, &resJSON) + if err != nil { + t.Fatal(err, string(res)) + } + if resJSON.Code != 200000 { + t.Fatal("create failed", string(res)) + } + + // enable router + enableData := fmt.Sprintf(`[{"id":"%s","enable":true}]`, resJSON.Responses[0].Data.ID) + res, err = makeJSONRequest("http://127.0.0.1:8090/naming/v2/routings/enable", enableData, http.MethodPut, map[string]string{ + "X-Polaris-Token": token, + }) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(res, &resJSON) + if err != nil { + t.Fatal(err) + } + if resJSON.Code != 200000 { + t.Fatal("enable failed", string(res)) + } + + t.Cleanup(func() { + enableData := fmt.Sprintf(`[{"id":"%s"}]`, resJSON.Responses[0].Data.ID) + res, err = makeJSONRequest("http://127.0.0.1:8090/naming/v2/routings/delete", enableData, http.MethodPost, map[string]string{ + "X-Polaris-Token": token, + }) + resJSON := &commonRes{} + err = json.Unmarshal(res, resJSON) + if err != nil { + t.Fatal(err, string(res)) + } + if resJSON.Code != 200000 { + t.Fatal("delete failed", string(res)) + } + }) + + sdk, err := polaris.NewSDKContextByAddress("127.0.0.1:8091") + if err != nil { + t.Fatal(err) + } + + p := New(sdk) + + r := p.Registry( + WithRegistryTimeout(time.Second), + WithRegistryHealthy(true), + WithRegistryIsolate(false), + WithRegistryRetryCount(0), + WithRegistryWeight(100), + WithRegistryTTL(10), + ) + + ins := ®istry.ServiceInstance{ + ID: "kratos", + Name: "kratos", + Version: "v1.0.0", + Endpoints: []string{ + "grpc://127.0.0.1:8080", + "http://127.0.0.1:9090", + }, + } + + err = r.Register(context.Background(), ins) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 5) + nodes := []selector.Node{ + selector.NewNode("grpc", "127.0.0.1:9000", ®istry.ServiceInstance{ + ID: "123", + Name: "test-ut", + Version: "v1.0.0", + Metadata: map[string]string{"weight": "100", "az": "1"}, + Endpoints: []string{"grpc://127.0.0.1:9000"}, + }), + selector.NewNode("grpc", "127.0.0.2:9000", ®istry.ServiceInstance{ + ID: "123", + Name: "test-ut", + Version: "v1.0.0", + Metadata: map[string]string{"weight": "100", "az": "2"}, + Endpoints: []string{"grpc://127.0.0.2:9000"}, + }), + selector.NewNode("grpc", "127.0.0.3:9000", ®istry.ServiceInstance{ + ID: "123", + Name: "test-ut", + Version: "v1.0.0", + Metadata: map[string]string{"weight": "100", "az": "1"}, + Endpoints: []string{"grpc://127.0.0.3:9000"}, + }), + } + + f := p.NodeFilter() + ctx := kratos.NewContext(context.Background(), &mockApp{}) + n := f(ctx, nodes) + for _, node := range n { + if node.Metadata()["az"] != "1" { + t.Fatal("node filter result wrong") + } + t.Log(node) + } + if len(n) != 2 { + t.Fatal("node filter result wrong") + } +} + +type mockApp struct{} + +func (m mockApp) ID() string { + return "1" +} + +func (m mockApp) Name() string { + return "kratos" +} + +func (m mockApp) Version() string { + return "v2.0.0" +} + +func (m mockApp) Metadata() map[string]string { + return map[string]string{} +} + +func (m mockApp) Endpoint() []string { + return []string{"grpc://123.123.123.123:9090"} +} diff --git a/contrib/registry/consul/client.go b/contrib/registry/consul/client.go new file mode 100644 index 0000000..fe0dbc9 --- /dev/null +++ b/contrib/registry/consul/client.go @@ -0,0 +1,310 @@ +package consul + +import ( + "context" + "errors" + "fmt" + "math/rand/v2" + "net" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/registry" + + "github.com/hashicorp/consul/api" +) + +type Datacenter string + +const ( + SingleDatacenter Datacenter = "SINGLE" + MultiDatacenter Datacenter = "MULTI" +) + +// Client is consul client config +type Client struct { + dc Datacenter + cli *api.Client + + // resolve service entry endpoints + resolver ServiceResolver + // healthcheck time interval in seconds + healthcheckInterval int + // heartbeat enable heartbeat + heartbeat bool + // deregisterCriticalServiceAfter time interval in seconds + deregisterCriticalServiceAfter int + // serviceChecks user custom checks + serviceChecks api.AgentServiceChecks + // tags is service tags + tags []string + + // used to control heartbeat + lock sync.RWMutex + cancelers map[string]*canceler +} + +type canceler struct { + ctx context.Context + cancel context.CancelFunc + done chan struct{} +} + +func defaultResolver(_ context.Context, entries []*api.ServiceEntry) []*registry.ServiceInstance { + services := make([]*registry.ServiceInstance, 0, len(entries)) + for _, entry := range entries { + var version string + for _, tag := range entry.Service.Tags { + ss := strings.SplitN(tag, "=", 2) + if len(ss) == 2 && ss[0] == "version" { + version = ss[1] + } + } + endpoints := make([]string, 0) + for scheme, addr := range entry.Service.TaggedAddresses { + if scheme == "lan_ipv4" || scheme == "wan_ipv4" || scheme == "lan_ipv6" || scheme == "wan_ipv6" { + continue + } + endpoints = append(endpoints, addr.Address) + } + if len(endpoints) == 0 && entry.Service.Address != "" && entry.Service.Port != 0 { + endpoints = append(endpoints, "http://"+net.JoinHostPort(entry.Service.Address, strconv.FormatUint(uint64(entry.Service.Port), 10))) + } + services = append(services, ®istry.ServiceInstance{ + ID: entry.Service.ID, + Name: entry.Service.Service, + Metadata: entry.Service.Meta, + Version: version, + Endpoints: endpoints, + }) + } + + return services +} + +// ServiceResolver is used to resolve service endpoints +type ServiceResolver func(ctx context.Context, entries []*api.ServiceEntry) []*registry.ServiceInstance + +// Service get services from consul +func (c *Client) Service(ctx context.Context, service string, index uint64, passingOnly bool) ([]*registry.ServiceInstance, uint64, error) { + if c.dc == MultiDatacenter { + return c.multiDCService(ctx, service, index, passingOnly) + } + + opts := &api.QueryOptions{ + WaitIndex: index, + WaitTime: time.Second * 55, + Datacenter: string(c.dc), + } + opts = opts.WithContext(ctx) + + if c.dc == SingleDatacenter { + opts.Datacenter = "" + } + + entries, meta, err := c.singleDCEntries(service, "", passingOnly, opts) + if err != nil { + return nil, 0, err + } + return c.resolver(ctx, entries), meta.LastIndex, nil +} + +func (c *Client) multiDCService(ctx context.Context, service string, index uint64, passingOnly bool) ([]*registry.ServiceInstance, uint64, error) { + opts := &api.QueryOptions{ + WaitIndex: index, + WaitTime: time.Second * 55, + } + opts = opts.WithContext(ctx) + + var instances []*registry.ServiceInstance + + dcs, err := c.cli.Catalog().Datacenters() + if err != nil { + return nil, 0, err + } + + for _, dc := range dcs { + opts.Datacenter = dc + e, m, err := c.singleDCEntries(service, "", passingOnly, opts) + if err != nil { + return nil, 0, err + } + + ins := c.resolver(ctx, e) + for _, in := range ins { + if in.Metadata == nil { + in.Metadata = make(map[string]string, 1) + } + in.Metadata["dc"] = dc + } + + instances = append(instances, ins...) + opts.WaitIndex = m.LastIndex + } + + return instances, opts.WaitIndex, nil +} + +func (c *Client) singleDCEntries(service, tag string, passingOnly bool, opts *api.QueryOptions) ([]*api.ServiceEntry, *api.QueryMeta, error) { + return c.cli.Health().Service(service, tag, passingOnly, opts) +} + +// Register register service instance to consul +func (c *Client) Register(ctx context.Context, svc *registry.ServiceInstance, enableHealthCheck bool) error { + addresses := make(map[string]api.ServiceAddress, len(svc.Endpoints)) + checkAddresses := make([]string, 0, len(svc.Endpoints)) + for _, endpoint := range svc.Endpoints { + raw, err := url.Parse(endpoint) + if err != nil { + return err + } + addr := raw.Hostname() + port, _ := strconv.ParseUint(raw.Port(), 10, 16) + + checkAddresses = append(checkAddresses, net.JoinHostPort(addr, strconv.FormatUint(port, 10))) + addresses[raw.Scheme] = api.ServiceAddress{Address: endpoint, Port: int(port)} + } + tags := []string{fmt.Sprintf("version=%s", svc.Version)} + if len(c.tags) > 0 { + tags = append(tags, c.tags...) + } + asr := &api.AgentServiceRegistration{ + ID: svc.ID, + Name: svc.Name, + Meta: svc.Metadata, + Tags: tags, + TaggedAddresses: addresses, + } + if len(checkAddresses) > 0 { + host, portRaw, _ := net.SplitHostPort(checkAddresses[0]) + port, _ := strconv.ParseInt(portRaw, 10, 32) + asr.Address = host + asr.Port = int(port) + } + if enableHealthCheck { + for _, address := range checkAddresses { + asr.Checks = append(asr.Checks, &api.AgentServiceCheck{ + TCP: address, + Interval: fmt.Sprintf("%ds", c.healthcheckInterval), + DeregisterCriticalServiceAfter: fmt.Sprintf("%ds", c.deregisterCriticalServiceAfter), + Timeout: "5s", + }) + } + // custom checks + asr.Checks = append(asr.Checks, c.serviceChecks...) + } + if c.heartbeat { + asr.Checks = append(asr.Checks, &api.AgentServiceCheck{ + CheckID: "service:" + svc.ID, + TTL: fmt.Sprintf("%ds", c.healthcheckInterval*2), + DeregisterCriticalServiceAfter: fmt.Sprintf("%ds", c.deregisterCriticalServiceAfter), + }) + } + + c.lock.Lock() + if cc, ok := c.cancelers[svc.ID]; ok { + cc.cancel() + <-cc.done + } + var cc *canceler + if c.heartbeat { + cancelCtx, cancel := context.WithCancel(context.Background()) + cc = &canceler{ + ctx: cancelCtx, + cancel: cancel, + done: make(chan struct{}), + } + c.cancelers[svc.ID] = cc + go func() { + <-cc.done + cc.cancel() + c.lock.Lock() + if c.cancelers[svc.ID] == cc { + delete(c.cancelers, svc.ID) + } + c.lock.Unlock() + }() + } + c.lock.Unlock() + + err := c.cli.Agent().ServiceRegisterOpts(asr, api.ServiceRegisterOpts{}.WithContext(ctx)) + if err != nil { + if c.heartbeat { + close(cc.done) + } + return err + } + + if c.heartbeat { + go func() { + defer close(cc.done) + err = c.cli.Agent().UpdateTTL("service:"+svc.ID, "pass", "pass") + if err != nil { + log.Error("[Consul] update ttl heartbeat to consul failed", "error", err) + } + ticker := time.NewTicker(time.Second * time.Duration(c.healthcheckInterval)) + defer ticker.Stop() + for { + select { + case <-cc.ctx.Done(): + _ = c.cli.Agent().ServiceDeregister(svc.ID) + return + case <-ticker.C: + err = c.cli.Agent().UpdateTTLOpts("service:"+svc.ID, "pass", "pass", new(api.QueryOptions).WithContext(cc.ctx)) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + _ = c.cli.Agent().ServiceDeregister(svc.ID) + return + } + if err != nil { + log.Error("[Consul] update ttl heartbeat to consul failed", "error", err) + // when the previous report fails, try to re register the service + if err := sleepCtx(cc.ctx, time.Duration(rand.IntN(5))*time.Second); err != nil { + _ = c.cli.Agent().ServiceDeregister(svc.ID) + return + } + if err := c.cli.Agent().ServiceRegisterOpts(asr, api.ServiceRegisterOpts{}.WithContext(cc.ctx)); err != nil { + log.Error("[Consul] re registry service failed", "error", err) + } else { + log.Warn("[Consul] re registry of service occurred success") + } + } + } + } + }() + } + return nil +} + +func sleepCtx(ctx context.Context, d time.Duration) error { + t := time.NewTimer(d) + defer t.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + return nil + } +} + +// Deregister service by service ID +func (c *Client) Deregister(ctx context.Context, serviceID string) error { + c.lock.RLock() + cc, ok := c.cancelers[serviceID] + c.lock.RUnlock() + if ok { + cc.cancel() + <-cc.done + } + + err := c.cli.Agent().ServiceDeregisterOpts(serviceID, new(api.QueryOptions).WithContext(ctx)) + var se api.StatusError + if errors.As(err, &se) && se.Code == 404 { + // not found + err = nil + } + return err +} diff --git a/contrib/registry/consul/go.mod b/contrib/registry/consul/go.mod new file mode 100644 index 0000000..ba1a8c3 --- /dev/null +++ b/contrib/registry/consul/go.mod @@ -0,0 +1,30 @@ +module github.com/go-kratos/kratos/contrib/registry/consul/v3 + +go 1.26 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/hashicorp/consul/api v1.34.2 +) + +require ( + github.com/armon/go-metrics v0.4.1 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/serf v0.10.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/sys v0.44.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/consul/go.sum b/contrib/registry/consul/go.sum new file mode 100644 index 0000000..fe9b535 --- /dev/null +++ b/contrib/registry/consul/go.sum @@ -0,0 +1,230 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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.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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/consul/api v1.34.2 h1:B5jqSSKwWyY8U8WiGS5vmPEPkkF0bAvrECykdZkDR80= +github.com/hashicorp/consul/api v1.34.2/go.mod h1:+gAdHQa2zvgYX3ZfcgITtnYCSj6AgS/cgotvCKaE+b8= +github.com/hashicorp/consul/sdk v0.18.1 h1:RDTeBvAeOveI2xI86sV+8WkaN7OkP4zz+cG3fOobDCM= +github.com/hashicorp/consul/sdk v0.18.1/go.mod h1:XdP2tEJmAvlK4jgoKTTtohGkRJlS4mU44mv9/sjU21s= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0= +github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU= +github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI= +github.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4= +github.com/hashicorp/serf v0.10.2 h1:m5IORhuNSjaxeljg5DeQVDlQyVkhRIjJDimbkCa8aAc= +github.com/hashicorp/serf v0.10.2/go.mod h1:T1CmSGfSeGfnfNy/w0odXQUR1rfECGd2Qdsp84DjOiY= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/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/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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-20190911185100-cd5d95a43a6e/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +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.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/registry/consul/registry.go b/contrib/registry/consul/registry.go new file mode 100644 index 0000000..18a293d --- /dev/null +++ b/contrib/registry/consul/registry.go @@ -0,0 +1,297 @@ +package consul + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/hashicorp/consul/api" + + "github.com/go-kratos/kratos/v3/registry" +) + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +// Option is consul registry option. +type Option func(*Registry) + +// WithHealthCheck with registry health check option. +func WithHealthCheck(enable bool) Option { + return func(o *Registry) { + o.enableHealthCheck = enable + } +} + +// WithTimeout with get services timeout option. +func WithTimeout(timeout time.Duration) Option { + return func(o *Registry) { + o.timeout = timeout + } +} + +// WithDatacenter with registry datacenter option +func WithDatacenter(dc Datacenter) Option { + return func(o *Registry) { + o.cli.dc = dc + } +} + +// WithHeartbeat enable or disable heartbeat +func WithHeartbeat(enable bool) Option { + return func(o *Registry) { + if o.cli != nil { + o.cli.heartbeat = enable + } + } +} + +// WithServiceResolver with endpoint function option. +func WithServiceResolver(fn ServiceResolver) Option { + return func(o *Registry) { + if o.cli != nil { + o.cli.resolver = fn + } + } +} + +// WithHealthCheckInterval with healthcheck interval in seconds. +func WithHealthCheckInterval(interval int) Option { + return func(o *Registry) { + if o.cli != nil { + o.cli.healthcheckInterval = interval + } + } +} + +// WithDeregisterCriticalServiceAfter with deregister-critical-service-after in seconds. +func WithDeregisterCriticalServiceAfter(interval int) Option { + return func(o *Registry) { + if o.cli != nil { + o.cli.deregisterCriticalServiceAfter = interval + } + } +} + +// WithServiceCheck with service checks +func WithServiceCheck(checks ...*api.AgentServiceCheck) Option { + return func(o *Registry) { + if o.cli != nil { + o.cli.serviceChecks = checks + } + } +} + +// WithTags with service tags. +func WithTags(tags []string) Option { + return func(o *Registry) { + if o.cli != nil { + o.cli.tags = tags + } + } +} + +// Config is consul registry config +type Config struct { + *api.Config +} + +// Registry is consul registry +type Registry struct { + cli *Client + enableHealthCheck bool + registry map[string]*serviceSet + lock sync.RWMutex + timeout time.Duration +} + +// New creates consul registry +func New(apiClient *api.Client, opts ...Option) *Registry { + r := &Registry{ + registry: make(map[string]*serviceSet), + enableHealthCheck: true, + timeout: 10 * time.Second, + cli: &Client{ + dc: SingleDatacenter, + cli: apiClient, + resolver: defaultResolver, + healthcheckInterval: 10, + heartbeat: true, + deregisterCriticalServiceAfter: 600, + cancelers: make(map[string]*canceler), + }, + } + for _, o := range opts { + o(r) + } + return r +} + +// Register register service +func (r *Registry) Register(ctx context.Context, svc *registry.ServiceInstance) error { + return r.cli.Register(ctx, svc, r.enableHealthCheck) +} + +// Deregister deregister service +func (r *Registry) Deregister(ctx context.Context, svc *registry.ServiceInstance) error { + return r.cli.Deregister(ctx, svc.ID) +} + +// GetService return service by name +func (r *Registry) GetService(ctx context.Context, name string) ([]*registry.ServiceInstance, error) { + r.lock.RLock() + set := r.registry[name] + r.lock.RUnlock() + + getRemote := func() []*registry.ServiceInstance { + services, _, err := r.cli.Service(ctx, name, 0, true) + if err == nil && len(services) > 0 { + return services + } + return nil + } + + if set == nil { + if s := getRemote(); len(s) > 0 { + return s, nil + } + return nil, fmt.Errorf("service %s not resolved in registry", name) + } + ss, _ := set.services.Load().([]*registry.ServiceInstance) + if ss == nil { + if s := getRemote(); len(s) > 0 { + return s, nil + } + return nil, fmt.Errorf("service %s not found in registry", name) + } + return ss, nil +} + +// ListServices return service list. +func (r *Registry) ListServices() (allServices map[string][]*registry.ServiceInstance, err error) { + r.lock.RLock() + defer r.lock.RUnlock() + allServices = make(map[string][]*registry.ServiceInstance) + for name, set := range r.registry { + ss, _ := set.services.Load().([]*registry.ServiceInstance) + if ss == nil { + continue + } + services := make([]*registry.ServiceInstance, 0, len(ss)) + services = append(services, ss...) + allServices[name] = services + } + return +} + +// Watch resolve service by name +func (r *Registry) Watch(ctx context.Context, name string) (registry.Watcher, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + r.lock.Lock() + set, ok := r.registry[name] + if !ok { + cancelCtx, cancel := context.WithCancel(context.Background()) + set = &serviceSet{ + registry: r, + watcher: make(map[*watcher]struct{}), + services: &atomic.Value{}, + serviceName: name, + ctx: cancelCtx, + cancel: cancel, + } + r.registry[name] = set + } + set.ref.Add(1) + r.lock.Unlock() + + // init watcher + w := &watcher{ + event: make(chan struct{}, 1), + } + w.ctx, w.cancel = context.WithCancel(ctx) + w.set = set + set.lock.Lock() + set.watcher[w] = struct{}{} + set.lock.Unlock() + + ss, _ := set.services.Load().([]*registry.ServiceInstance) + if len(ss) > 0 { + // If the service has a value, it needs to be pushed to the watcher, + // otherwise the initial data may be blocked forever during the watch. + select { + case w.event <- struct{}{}: + default: + } + } + + if !ok { + if err := r.resolve(ctx, set); err != nil { + return nil, err + } + } + return w, nil +} + +func (r *Registry) resolve(ctx context.Context, ss *serviceSet) error { + listServices := r.cli.Service + if r.timeout > 0 { + listServices = func(ctx context.Context, service string, index uint64, passingOnly bool) ([]*registry.ServiceInstance, uint64, error) { + timeoutCtx, cancel := context.WithTimeout(ctx, r.timeout) + defer cancel() + + return r.cli.Service(timeoutCtx, service, index, passingOnly) + } + } + + services, idx, err := listServices(ctx, ss.serviceName, 0, true) + if err != nil { + return err + } + if len(services) > 0 { + ss.broadcast(services) + } + + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + tmpService, tmpIdx, err := listServices(ss.ctx, ss.serviceName, idx, true) + if err != nil { + if err := sleepCtx(ss.ctx, time.Second); err != nil { + return + } + continue + } + if len(tmpService) != 0 && tmpIdx != idx { + services = tmpService + ss.broadcast(services) + } + idx = tmpIdx + case <-ss.ctx.Done(): + return + } + } + }() + + return nil +} + +func (r *Registry) tryDelete(ss *serviceSet) bool { + r.lock.Lock() + defer r.lock.Unlock() + if ss.ref.Add(-1) != 0 { + return false + } + ss.cancel() + delete(r.registry, ss.serviceName) + return true +} diff --git a/contrib/registry/consul/registry_test.go b/contrib/registry/consul/registry_test.go new file mode 100644 index 0000000..5b4c545 --- /dev/null +++ b/contrib/registry/consul/registry_test.go @@ -0,0 +1,1029 @@ +package consul + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "reflect" + "strconv" + "sync" + "testing" + "time" + + "github.com/hashicorp/consul/api" + + "github.com/go-kratos/kratos/v3/registry" +) + +func tcpServer(lis net.Listener) { + for { + conn, err := lis.Accept() + if err != nil { + return + } + go func() { + _, _ = io.Copy(io.Discard, conn) + _ = conn.Close() + }() + } +} + +func TestRegistry_Register(t *testing.T) { + opts := []Option{ + WithHealthCheck(false), + } + + type args struct { + ctx context.Context + serverName string + server []*registry.ServiceInstance + } + + test := []struct { + name string + args args + want []*registry.ServiceInstance + wantErr bool + }{ + { + name: "normal", + args: args{ + ctx: context.Background(), + serverName: "server-1", + server: []*registry.ServiceInstance{ + { + ID: "1", + Name: "server-1", + Version: "v0.0.1", + Metadata: nil, + Endpoints: []string{"http://127.0.0.1:8000"}, + }, + }, + }, + want: []*registry.ServiceInstance{ + { + ID: "1", + Name: "server-1", + Version: "v0.0.1", + Metadata: nil, + Endpoints: []string{"http://127.0.0.1:8000"}, + }, + }, + wantErr: false, + }, + { + name: "registry new service replace old service", + args: args{ + ctx: context.Background(), + serverName: "server-1", + server: []*registry.ServiceInstance{ + { + ID: "2", + Name: "server-1", + Version: "v0.0.1", + Metadata: nil, + Endpoints: []string{"http://127.0.0.1:8000"}, + }, + { + ID: "2", + Name: "server-1", + Version: "v0.0.2", + Metadata: nil, + Endpoints: []string{"http://127.0.0.1:8000"}, + }, + }, + }, + want: []*registry.ServiceInstance{ + { + ID: "2", + Name: "server-1", + Version: "v0.0.2", + Metadata: nil, + Endpoints: []string{"http://127.0.0.1:8000"}, + }, + }, + wantErr: false, + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500"}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + + r := New(cli, opts...) + + for _, instance := range tt.args.server { + instance := instance + err = r.Register(tt.args.ctx, instance) + if err != nil { + t.Error(err) + } + defer func() { + err = r.Deregister(tt.args.ctx, instance) + if err != nil { + t.Error(err) + } + }() + } + watchCtx, watchCancel := context.WithCancel(context.Background()) + watch, err := r.Watch(watchCtx, tt.args.serverName) + if err != nil { + t.Error(err) + watchCancel() + return + } + + got, err := watch.Next() + if (err != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", got) + watchCancel() + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetService() got = %v, want %v", got, tt.want) + } + + err = watch.Stop() + if err != nil { + t.Error(err) + } + watchCancel() + }) + } +} + +func TestRegistry_GetService(t *testing.T) { + addr := fmt.Sprintf("%s:9091", getIntranetIP()) + lis, err := net.Listen("tcp", addr) + if err != nil { + t.Errorf("listen tcp %s failed!", addr) + t.Fail() + } + defer lis.Close() + go tcpServer(lis) + time.Sleep(time.Millisecond * 100) + cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500"}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + opts := []Option{ + WithHeartbeat(true), + WithHealthCheck(true), + WithHealthCheckInterval(5), + } + r := New(cli, opts...) + + instance1 := ®istry.ServiceInstance{ + ID: "1", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + + instance2 := ®istry.ServiceInstance{ + ID: "2", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + serviceName string + } + tests := []struct { + name string + fields fields + args args + want []*registry.ServiceInstance + wantErr bool + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + }{ + { + name: "normal", + fields: fields{r}, + args: args{ + ctx: context.Background(), + serviceName: "server-1", + }, + want: []*registry.ServiceInstance{instance1}, + wantErr: false, + preFunc: func(t *testing.T) { + if err := r.Register(context.Background(), instance1); err != nil { + t.Error(err) + } + watchCtx, watchCancel := context.WithCancel(context.Background()) + watch, err := r.Watch(watchCtx, instance1.Name) + if err != nil { + t.Error(err) + } + _, err = watch.Next() + if err != nil { + t.Error(err) + } + err = watch.Stop() + if err != nil { + t.Error(err) + } + watchCancel() + }, + deferFunc: func(t *testing.T) { + err := r.Deregister(context.Background(), instance1) + if err != nil { + t.Error(err) + } + }, + }, + { + name: "can't get any", + fields: fields{r}, + args: args{ + ctx: context.Background(), + serviceName: "server-x", + }, + want: nil, + wantErr: true, + preFunc: func(t *testing.T) { + if err := r.Register(context.Background(), instance2); err != nil { + t.Error(err) + } + watchCtx, watchCancel := context.WithCancel(context.Background()) + watch, err := r.Watch(watchCtx, instance2.Name) + if err != nil { + t.Error(err) + } + _, err = watch.Next() + if err != nil { + t.Error(err) + } + err = watch.Stop() + if err != nil { + t.Error(err) + } + watchCancel() + }, + deferFunc: func(t *testing.T) { + err := r.Deregister(context.Background(), instance2) + if err != nil { + t.Error(err) + } + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.preFunc != nil { + test.preFunc(t) + } + if test.deferFunc != nil { + defer test.deferFunc(t) + } + + service, err := test.fields.registry.GetService(context.Background(), test.args.serviceName) + if (err != nil) != test.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, test.wantErr) + t.Errorf("GetService() got = %v", service) + return + } + if !reflect.DeepEqual(service, test.want) { + t.Errorf("GetService() got = %v, want %v", service, test.want) + } + }) + } +} + +func TestRegistry_Watch(t *testing.T) { + addr := fmt.Sprintf("%s:9091", getIntranetIP()) + lis, err := net.Listen("tcp", addr) + if err != nil { + t.Errorf("listen tcp %s failed!", addr) + return + } + defer lis.Close() + go tcpServer(lis) + + cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500", WaitTime: 2 * time.Second}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + + instance1 := ®istry.ServiceInstance{ + ID: "1", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + + instance2 := ®istry.ServiceInstance{ + ID: "2", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + + instance3 := ®istry.ServiceInstance{ + ID: "3", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + + type args struct { + ctx context.Context + cancel func() + opts []Option + instance *registry.ServiceInstance + } + canceledCtx, cancel := context.WithCancel(context.Background()) + + tests := []struct { + name string + args args + want []*registry.ServiceInstance + wantErr bool + preFunc func(t *testing.T) + }{ + { + name: "normal", + args: args{ + ctx: context.Background(), + instance: instance1, + opts: []Option{ + WithHealthCheck(false), + }, + }, + want: []*registry.ServiceInstance{instance1}, + wantErr: false, + preFunc: func(*testing.T) {}, + }, + { + name: "ctx has been canceled", + args: args{ + ctx: canceledCtx, + cancel: cancel, + instance: instance2, + opts: []Option{ + WithHealthCheck(false), + }, + }, + want: nil, + wantErr: true, + preFunc: func(*testing.T) {}, + }, + { + name: "register with healthCheck", + args: args{ + ctx: context.Background(), + instance: instance3, + opts: []Option{ + WithHeartbeat(true), + WithHealthCheck(true), + WithHealthCheckInterval(5), + }, + }, + want: []*registry.ServiceInstance{instance3}, + wantErr: false, + preFunc: func(*testing.T) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc(t) + } + + r := New(cli, tt.args.opts...) + + err := r.Register(tt.args.ctx, tt.args.instance) + if err != nil { + t.Error(err) + return + } + defer func() { + err = r.Deregister(context.Background(), tt.args.instance) + if err != nil { + t.Error(err) + } + }() + watch, err := r.Watch(tt.args.ctx, tt.args.instance.Name) + if err != nil { + t.Error(err) + } + + if tt.args.cancel != nil { + tt.args.cancel() + } + + service, err := watch.Next() + + if (err != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", service) + return + } + if !reflect.DeepEqual(service, tt.want) { + t.Errorf("GetService() got = %v, want %v", service, tt.want) + } + err = watch.Stop() + if err != nil { + t.Error(err) + } + }) + } +} + +func TestRegistry_IdleAndWatch(t *testing.T) { + addr := fmt.Sprintf("%s:9091", getIntranetIP()) + + time.Sleep(time.Millisecond * 100) + cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500", WaitTime: 2 * time.Second}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + + r := New(cli, []Option{ + WithHealthCheck(false), + }...) + + instance1 := ®istry.ServiceInstance{ + ID: "1", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + instance2 := ®istry.ServiceInstance{ + ID: "1", + Name: "server-1", + Version: "v0.0.2", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + + type args struct { + ctx context.Context + instance *registry.ServiceInstance + changeInstance *registry.ServiceInstance + } + + tests := []struct { + name string + args args + want1 []*registry.ServiceInstance + want2 []*registry.ServiceInstance + }{ + { + name: "many client, one idle", + args: args{ + ctx: context.Background(), + instance: instance1, + changeInstance: instance2, + }, + want1: []*registry.ServiceInstance{instance1}, + want2: []*registry.ServiceInstance{instance2}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var watchs []registry.Watcher + for i := 0; i < 10; i++ { + watch, err := r.Watch(tt.args.ctx, tt.args.instance.Name) //nolint + if err != nil { + t.Error(err) + } + defer func() { + _ = watch.Stop() + }() + + watchs = append(watchs, watch) + } + + err = r.Register(tt.args.ctx, tt.args.instance) + if err != nil { + t.Error(err) + } + defer func() { + err = r.Deregister(context.Background(), tt.args.instance) + if err != nil { + t.Error(err) + } + }() + + var wg1 sync.WaitGroup + for _, watch := range watchs { + wg1.Add(1) + go func(watch registry.Watcher, want []*registry.ServiceInstance) { + defer wg1.Done() + + // first + service, err := watch.Next() //nolint + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(service, want) { + t.Errorf("GetService() got = %v, want = %v", service, want) + return + } + }(watch, tt.want1) + } + wg1.Wait() + + err = r.Register(tt.args.ctx, tt.args.changeInstance) + if err != nil { + t.Error(err) + } + defer func() { + err := r.Deregister(context.Background(), tt.args.changeInstance) + if err != nil { + t.Error(err) + } + }() + + var wg2 sync.WaitGroup + for _, watch := range watchs { + wg2.Add(1) + go func(watch registry.Watcher, want []*registry.ServiceInstance) { + defer wg2.Done() + + // instance changes + service, err := watch.Next() //nolint + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(service, want) { + t.Errorf("GetService() got = %v, want = %v", service, want) + } + }(watch, tt.want2) + } + wg2.Wait() + }) + } +} + +func TestRegistry_IdleAndWatch2(t *testing.T) { + addr := fmt.Sprintf("%s:9091", getIntranetIP()) + + time.Sleep(time.Millisecond * 100) + cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500", WaitTime: 2 * time.Second}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + + instance1 := ®istry.ServiceInstance{ + ID: "1", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + type args struct { + ctx context.Context + opts []Option + instance *registry.ServiceInstance + } + + tests := []struct { + name string + args args + want []*registry.ServiceInstance + wantErr bool + }{ + { + name: "all clients are idle, create a new one", + args: args{ + ctx: context.Background(), + instance: instance1, + opts: []Option{ + WithHealthCheck(false), + }, + }, + want: []*registry.ServiceInstance{instance1}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := New(cli, tt.args.opts...) + + err = r.Register(tt.args.ctx, tt.args.instance) + if err != nil { + t.Error(err) + } + defer func() { + err = r.Deregister(tt.args.ctx, tt.args.instance) + if err != nil { + t.Error(err) + } + }() + + ctx, cancel := context.WithCancel(context.Background()) + for i := 0; i < 10; i++ { + stopCtx, stopCancel := context.WithCancel(ctx) + watch, err1 := r.Watch(stopCtx, tt.args.instance.Name) + if err1 != nil { + t.Error(err1) + } + go func(_ int) { + // first + service, err2 := watch.Next() + if (err2 != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", service) + return + } + }(i) + go func() { + select { + case <-stopCtx.Done(): + err1 = watch.Stop() + if err1 != nil { + t.Errorf("watch stop err:%v", err) + } + return + case <-time.After(time.Minute): + stopCancel() + err1 = watch.Stop() + if err1 != nil { + t.Errorf("watch stop err:%v", err) + } + return + } + }() + } + time.Sleep(time.Second * 3) + cancel() + time.Sleep(time.Second * 2) + // Everything is idle. Add new watch. + watchCtx, watchCancel := context.WithCancel(context.Background()) + watch, err := r.Watch(watchCtx, tt.args.instance.Name) + if err != nil { + t.Error(err) + } + service, err := watch.Next() + if (err != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", service) + watchCancel() + return + } + if !reflect.DeepEqual(service, tt.want) { + t.Errorf("GetService() got = %v, want %v", service, tt.want) + } + watchCancel() + }) + } +} + +func TestRegistry_ExitOldResolverAndReWatch(t *testing.T) { + addr := fmt.Sprintf("%s:9091", getIntranetIP()) + + time.Sleep(time.Millisecond * 100) + cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500", WaitTime: 2 * time.Second}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + + instance1 := ®istry.ServiceInstance{ + ID: "1", + Name: "server-1", + Version: "v0.0.1", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + instance2 := ®istry.ServiceInstance{ + ID: "2", + Name: "server-1", + Version: "v0.0.2", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + } + type args struct { + ctx context.Context + opts []Option + instance *registry.ServiceInstance + initialInstance *registry.ServiceInstance + } + + tests := []struct { + name string + args args + want []*registry.ServiceInstance + wantErr bool + }{ + { + name: "When it has entered idle mode, but the old resolver has not completely exited, the watch will be re-established due to new requests coming in.", + args: args{ + ctx: context.Background(), + initialInstance: instance1, + instance: instance2, + opts: []Option{ + WithHealthCheck(false), + WithTimeout(time.Second * 2), + }, + }, + want: []*registry.ServiceInstance{instance2}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := New(cli, tt.args.opts...) + + err = r.Register(tt.args.ctx, tt.args.initialInstance) + if err != nil { + t.Error(err) + } + // first watch + ctx, cancel := context.WithCancel(context.Background()) + watch, err := r.Watch(ctx, tt.args.instance.Name) + if err != nil { + t.Error(err) + } + service, err := watch.Next() + if (err != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", service) + } + + time.Sleep(time.Second * 3) + // The simulation entered idle mode first, but the old resolver was not closed yet, and new requests triggered a new Watch. + watchCtx := context.Background() + // old resolver cancel + err = watch.Stop() + if err != nil { + t.Errorf("watch stop err:%v", err) + } + cancel() + // If it sleeps for a period of time, the old resolve goroutine will exit before the new Watch is processed, and there will be no problems at this time. + // time.Sleep(time.Second * 8) + newWatch, err := r.Watch(watchCtx, tt.args.instance.Name) + if err != nil { + t.Error(err) + } + service, err = newWatch.Next() + if (err != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", service) + } + // change register info + time.Sleep(time.Second * 1) + err = r.Deregister(tt.args.ctx, tt.args.initialInstance) + if err != nil { + t.Error(err) + } + time.Sleep(time.Second * 5) + err = r.Register(tt.args.ctx, tt.args.instance) + if err != nil { + t.Error(err) + } + defer func() { + err = r.Deregister(tt.args.ctx, tt.args.instance) + if err != nil { + t.Error(err) + } + }() + + time.Sleep(time.Second * 2) + + newWatchCtx, newWatchCancel := context.WithCancel(context.Background()) + c := make(chan struct{}, 1) + + go func() { + service, err = newWatch.Next() + if (err != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", service) + return + } + if !reflect.DeepEqual(service, tt.want) { + t.Errorf("GetService() got = %v, want %v", service, tt.want) + } + c <- struct{}{} + }() + time.AfterFunc(time.Second*10, newWatchCancel) + select { + case <-newWatchCtx.Done(): + t.Errorf("Timeout getservice. May be no new resolve goroutine to obtain the latest service information") + case <-c: + return + } + }) + } +} + +func TestRegistry_ShareServiceSet(t *testing.T) { + lastIndex := uint64(0) + serviceName := "share-service-set" + mux := http.NewServeMux() + mux.HandleFunc("/v1/health/service/"+serviceName, func(w http.ResponseWriter, r *http.Request) { + var index uint64 + if s := r.URL.Query().Get("index"); s != "" { + val, err := strconv.ParseUint(s, 10, 64) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + index = val + } + + if index < lastIndex { + msg := "repeated request, not the same ServiceSet" + http.Error(w, msg, http.StatusBadRequest) + t.Error(msg) + t.FailNow() + return + } + + lastIndex = index + 1 + w.Header().Set("X-Consul-Index", strconv.FormatUint(lastIndex, 10)) + + out := []*api.ServiceEntry{ + { + Service: &api.AgentService{ + ID: "1", + Service: serviceName, + }, + }, + } + err := json.NewEncoder(w).Encode(out) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + ts := httptest.NewServer(mux) + defer ts.Close() + + cli, err := api.NewClient(&api.Config{Address: ts.URL, WaitTime: 2 * time.Second}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + + var prev registry.Watcher + r := New(cli, WithHealthCheck(false), WithHeartbeat(false)) + for i := 0; i < 100; i++ { + w, err := r.Watch(context.Background(), serviceName) //nolint + if err != nil { + t.Error(err) + return + } + // close previous watcher + if prev != nil { + if err = prev.Stop(); err != nil { + t.Error(err) + return + } + } + prev = w + } + + time.Sleep(time.Second * 5) + + if prev != nil { + if err = prev.Stop(); err != nil { + t.Error(err) + return + } + } +} + +func TestRegistry_MultiWatch(t *testing.T) { + cli, err := api.NewClient(&api.Config{Address: "127.0.0.1:8500", WaitTime: 2 * time.Second}) + if err != nil { + t.Fatalf("create consul client failed: %v", err) + } + + serviceName := "multi-watch" + addr := fmt.Sprintf("%s:9091", getIntranetIP()) + instances := []*registry.ServiceInstance{ + { + ID: "1", + Name: serviceName, + Version: "v1.0.0", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + }, + { + ID: "2", + Name: serviceName, + Version: "v1.0.0", + Endpoints: []string{fmt.Sprintf("tcp://%s?isSecure=false", addr)}, + }, + } + + r := New(cli, WithHealthCheck(false), WithHeartbeat(true)) + err = r.Register(context.Background(), instances[0]) + if err != nil { + t.Error(err) + return + } + defer func() { + err = r.Deregister(context.Background(), instances[0]) + if err != nil { + t.Error(err) + } + }() + + watch1, err := r.Watch(context.Background(), serviceName) + if err != nil { + t.Error(err) + return + } + defer func() { + if err = watch1.Stop(); err != nil { + t.Error(err) + } + }() + + watch2, err := r.Watch(context.Background(), serviceName) + if err != nil { + t.Error(err) + return + } + defer func() { + if err = watch2.Stop(); err != nil { + t.Error(err) + } + }() + + got1, err := watch1.Next() + if err != nil { + t.Error(err) + return + } + + got2, err := watch2.Next() + if err != nil { + t.Error(err) + return + } + + if !reflect.DeepEqual(got1, instances[:1]) { + t.Errorf("got = %v, want = %v", got1, instances[:1]) + return + } + if !reflect.DeepEqual(got2, instances[:1]) { + t.Errorf("got = %v, want = %v", got2, instances[:1]) + return + } + + // close first watcher + if err = watch1.Stop(); err != nil { + t.Error(err) + return + } + + // register a new instance + err = r.Register(context.Background(), instances[1]) + if err != nil { + t.Error(err) + return + } + defer func() { + err = r.Deregister(context.Background(), instances[1]) + if err != nil { + t.Error(err) + } + }() + + // second watcher should get the new instance + got, err := watch2.Next() + if err != nil { + t.Error(err) + return + } + + if !reflect.DeepEqual(got, instances[:2]) { + t.Errorf("got = %v, want = %v", got, instances[:2]) + return + } +} + +func getIntranetIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "127.0.0.1" + } + + for _, address := range addrs { + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + return "127.0.0.1" +} diff --git a/contrib/registry/consul/service.go b/contrib/registry/consul/service.go new file mode 100644 index 0000000..a345d0f --- /dev/null +++ b/contrib/registry/consul/service.go @@ -0,0 +1,41 @@ +package consul + +import ( + "context" + "sync" + "sync/atomic" + + "github.com/go-kratos/kratos/v3/registry" +) + +type serviceSet struct { + registry *Registry + serviceName string + watcher map[*watcher]struct{} + ref atomic.Int32 + services *atomic.Value + lock sync.RWMutex + + // for cancel + ctx context.Context + cancel context.CancelFunc +} + +func (s *serviceSet) broadcast(ss []*registry.ServiceInstance) { + s.services.Store(ss) + s.lock.RLock() + defer s.lock.RUnlock() + for k := range s.watcher { + select { + case k.event <- struct{}{}: + default: + } + } +} + +func (s *serviceSet) delete(w *watcher) { + s.lock.Lock() + delete(s.watcher, w) + s.lock.Unlock() + s.registry.tryDelete(s) +} diff --git a/contrib/registry/consul/watcher.go b/contrib/registry/consul/watcher.go new file mode 100644 index 0000000..8be4909 --- /dev/null +++ b/contrib/registry/consul/watcher.go @@ -0,0 +1,44 @@ +package consul + +import ( + "context" + + "github.com/go-kratos/kratos/v3/registry" +) + +type watcher struct { + event chan struct{} + set *serviceSet + + // for cancel + ctx context.Context + cancel context.CancelFunc +} + +func (w *watcher) Next() (services []*registry.ServiceInstance, err error) { + if err = w.ctx.Err(); err != nil { + return + } + + select { + case <-w.ctx.Done(): + err = w.ctx.Err() + return + case <-w.event: + } + + ss, ok := w.set.services.Load().([]*registry.ServiceInstance) + if ok { + services = append(services, ss...) + } + return +} + +func (w *watcher) Stop() error { + if w.cancel != nil { + w.cancel() + w.cancel = nil + w.set.delete(w) + } + return nil +} diff --git a/contrib/registry/discovery/README.md b/contrib/registry/discovery/README.md new file mode 100644 index 0000000..767b5a2 --- /dev/null +++ b/contrib/registry/discovery/README.md @@ -0,0 +1,93 @@ +## Discovery Registry + +This module implements a `registry.Registrar` and `registry.Discovery` interface in kratos based `bilibili/discovery`. + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/go-kratos/kratos/contrib/registry/discovery/v3) + +### Quick Start + +**_Register a service_** + +```go +import ( + "github.com/go-kratos/kratos/contrib/registry/discovery/v3" +) + +func main() { + // initialize a registry + r := discovery.New(&discovery.Config{ + Nodes: []string{"0.0.0.0:7171"}, + Env: "dev", + Region: "sh1", + Zone: "zone1", + Host: "hostname", + }) + + // construct srv instance + // ... + + app := kratos.New( + kratos.Name("helloworld"), + kratos.Server( + httpSrv, + grpcSrv, + ), + kratos.Metadata(map[string]string{"color": "gray"}), + // use Registrar + kratos.Registrar(r), + ) + + if err := app.Run(); err != nil { + log.Fatal(err) + } +} +``` + +**_Discover a service_** + +```go +import ( + "github.com/go-kratos/kratos/contrib/registry/discovery/v3" + "github.com/go-kratos/kratos/v3/transport/grpc" +) + +func main() { + // initialize a discovery + r := discovery.New(&discovery.Config{ + Nodes: []string{"0.0.0.0:7171"}, + Env: "dev", + Region: "sh1", + Zone: "zone1", + Host: "localhost", + }, nil) + + conn, err := grpc.NewClient( + context.Background(), + grpc.WithEndpoint("discovery:///appid"), + // use discovery + grpc.WithDiscovery(r), + ) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + // request and log +} +``` + +### Config explain + +```go +type Config struct { + Nodes []string // discovery nodes address + Region string // region of the service, sh + Zone string // zone of region, sh001 + Env string // env of service, dev, prod and etc + Host string // hostname of service +} +``` + +### References + +- [bilibili/discovery](https://github.com/bilibili/discovery) diff --git a/contrib/registry/discovery/discovery.go b/contrib/registry/discovery/discovery.go new file mode 100644 index 0000000..6233d94 --- /dev/null +++ b/contrib/registry/discovery/discovery.go @@ -0,0 +1,451 @@ +package discovery + +import ( + "context" + "fmt" + "math/rand/v2" + "net/url" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/go-resty/resty/v2" + "github.com/pkg/errors" + + "github.com/go-kratos/kratos/v3/log" +) + +type Discovery struct { + config *Config + once sync.Once + ctx context.Context + cancelFunc context.CancelFunc + httpClient *resty.Client + + node atomic.Value + nodeIdx atomic.Uint64 + + mutex sync.RWMutex + apps map[string]*appInfo + registry map[string]struct{} + lastHost string + cancelPolls context.CancelFunc +} + +type appInfo struct { + resolver map[*Resolve]struct{} + zoneIns atomic.Value + lastTs int64 // latest timestamp +} + +// New construct a Discovery instance which implements registry.Registrar, +// registry.Discovery and registry.Watcher. +func New(c *Config) *Discovery { + if c == nil { + c = new(Config) + } + if err := fixConfig(c); err != nil { + panic(err) + } + ctx, cancel := context.WithCancel(context.Background()) + d := &Discovery{ + config: c, + ctx: ctx, + cancelFunc: cancel, + apps: map[string]*appInfo{}, + registry: map[string]struct{}{}, + } + + d.httpClient = resty.New(). + SetTimeout(40 * time.Second) + + // Discovery self found and watch + r := d.resolveBuild(_discoveryAppID) + event := r.Watch() + _, ok := <-event + if !ok { + panic("Discovery watch self failed") + } + discoveryIns, ok := r.fetch(context.Background()) + if ok { + d.newSelf(discoveryIns.Instances) + } + go d.selfProc(r, event) + + return d +} + +// Close stop all running process including Discovery and register +func (d *Discovery) Close() error { + d.cancelFunc() + return nil +} + +// selfProc start a goroutine to refresh Discovery self registration information. +func (d *Discovery) selfProc(resolver *Resolve, event <-chan struct{}) { + for { + _, ok := <-event + if !ok { + return + } + zones, ok := resolver.fetch(context.Background()) + if ok { + d.newSelf(zones.Instances) + } + } +} + +// newSelf +func (d *Discovery) newSelf(zones map[string][]*discoveryInstance) { + ins, ok := zones[d.config.Zone] + if !ok { + return + } + var nodes []string + for _, in := range ins { + for _, addr := range in.Addrs { + u, err := url.Parse(addr) + if err == nil && u.Scheme == "http" { + nodes = append(nodes, u.Host) + } + } + } + // diff old nodes + var olds int + if node, ok := d.node.Load().([]string); ok { + for _, n := range nodes { + for _, o := range node { + if o == n { + olds++ + break + } + } + } + } + if len(nodes) == olds { + return + } + + rand.Shuffle(len(nodes), func(i, j int) { + nodes[i], nodes[j] = nodes[j], nodes[i] + }) + d.node.Store(nodes) +} + +// resolveBuild Discovery resolver builder. +func (d *Discovery) resolveBuild(appID string) *Resolve { + r := &Resolve{ + id: appID, + d: d, + event: make(chan struct{}, 1), + } + + d.mutex.Lock() + app, ok := d.apps[appID] + if !ok { + app = &appInfo{ + resolver: make(map[*Resolve]struct{}), + } + d.apps[appID] = app + cancel := d.cancelPolls + if cancel != nil { + cancel() + } + } + app.resolver[r] = struct{}{} + d.mutex.Unlock() + if ok { + select { + case r.event <- struct{}{}: + default: + } + } + + log.Debug("Discovery: AddWatch already watched", "appid", appID, "already_watch", ok) + d.once.Do(func() { + go d.serverProc() + }) + return r +} + +func (d *Discovery) serverProc() { + defer log.Debug("Discovery serverProc quit") + + var ( + ctx context.Context + cancel context.CancelFunc + ) + + ticker := time.NewTicker(time.Minute * 30) + defer ticker.Stop() + + for { + if ctx == nil { + ctx, cancel = context.WithCancel(d.ctx) + d.mutex.Lock() + d.cancelPolls = cancel + d.mutex.Unlock() + } + select { + case <-d.ctx.Done(): + return + case <-ticker.C: + d.switchNode() + default: + } + + apps, err := d.polls(ctx) + if err != nil { + d.switchNode() + if errors.Is(ctx.Err(), context.Canceled) { + ctx = nil + continue + } + time.Sleep(time.Second) + continue + } + d.broadcast(apps) + } +} + +func (d *Discovery) pickNode() string { + nodes, ok := d.node.Load().([]string) + if !ok || len(nodes) == 0 { + return d.config.Nodes[rand.IntN(len(d.config.Nodes))] + } + return nodes[d.nodeIdx.Load()%uint64(len(nodes))] +} + +func (d *Discovery) switchNode() { + d.nodeIdx.Add(1) +} + +// renew an instance with Discovery +func (d *Discovery) renew(ctx context.Context, ins *discoveryInstance) (err error) { + d.mutex.RLock() + c := d.config + d.mutex.RUnlock() + + res := new(discoveryCommonResp) + uri := fmt.Sprintf(_renewURL, d.pickNode()) + + // construct parameters to renew + p := newParams(d.config) + p.Set(_paramKeyAppID, ins.AppID) + + // send request to Discovery server. + if _, err = d.httpClient.R(). + SetContext(ctx). + SetQueryParamsFromValues(p). + SetResult(&res). + Post(uri); err != nil { + d.switchNode() + log.Error("Discovery: renew client.Get failed", "uri", uri, "env", c.Env, "appid", ins.AppID, "hostname", c.Host, "error", err) + return + } + + if res.Code != _codeOK { + err = fmt.Errorf("discovery.renew failed ErrorCode: %d", res.Code) + if res.Code == _codeNotFound { + if err = d.register(ctx, ins); err != nil { + err = errors.Wrap(err, "Discovery.renew instance, and failed to register ins") + } + return + } + + log.Error("Discovery: renew client.Get returned code", "uri", uri, "env", c.Env, "appid", ins.AppID, "hostname", c.Host, "code", res.Code) + } + + return +} + +// cancel Remove the registered instance from Discovery +func (d *Discovery) cancel(ins *discoveryInstance) (err error) { + d.mutex.RLock() + config := d.config + d.mutex.RUnlock() + + res := new(discoveryCommonResp) + uri := fmt.Sprintf(_cancelURL, d.pickNode()) + p := newParams(d.config) + p.Set(_paramKeyAppID, ins.AppID) + + // request + // send request to Discovery server. + if _, err = d.httpClient.R(). + SetContext(context.Background()). + SetQueryParamsFromValues(p). + SetResult(&res). + Post(uri); err != nil { + d.switchNode() + log.Error("Discovery: cancel client.Get failed", "uri", uri, "env", config.Env, "appid", ins.AppID, "hostname", config.Host, "error", err) + return + } + + // handle response error + if res.Code != _codeOK { + if res.Code == _codeNotFound { + return nil + } + + log.Warn("Discovery: cancel client.Get returned code", "uri", uri, "env", config.Env, "appid", ins.AppID, "hostname", config.Host, "code", res.Code) + err = fmt.Errorf("ErrorCode: %d", res.Code) + return + } + + return +} + +func (d *Discovery) broadcast(apps map[string]*disInstancesInfo) { + for appID, v := range apps { + var count int + // v maybe nil in old version(less than v1.1) Discovery, check in case of panic + if v == nil { + continue + } + for zone, ins := range v.Instances { + if len(ins) == 0 { + delete(v.Instances, zone) + } + count += len(ins) + } + if count == 0 { + continue + } + d.mutex.RLock() + app, ok := d.apps[appID] + d.mutex.RUnlock() + if ok { + app.lastTs = v.LastTs + app.zoneIns.Store(v) + d.mutex.RLock() + for rs := range app.resolver { + select { + case rs.event <- struct{}{}: + default: + } + } + d.mutex.RUnlock() + } + } +} + +func (d *Discovery) polls(ctx context.Context) (apps map[string]*disInstancesInfo, err error) { + var ( + lastTss = make([]int64, 0, 4) + appIDs = make([]string, 0, 16) + host = d.pickNode() + changed bool + ) + if host != d.lastHost { + d.lastHost = host + changed = true + } + + d.mutex.RLock() + config := d.config + for k, v := range d.apps { + if changed { + v.lastTs = 0 + } + appIDs = append(appIDs, k) + lastTss = append(lastTss, v.lastTs) + } + d.mutex.RUnlock() + + // if there is no app, polls just return. + if len(appIDs) == 0 { + return + } + + uri := fmt.Sprintf(_pollURL, host) + res := new(discoveryPollsResp) + + // params + p := newParams(nil) + p.Set(_paramKeyEnv, config.Env) + p.Set(_paramKeyHostname, config.Host) + for _, appID := range appIDs { + p.Add(_paramKeyAppID, appID) + } + for _, ts := range lastTss { + p.Add("latest_timestamp", strconv.FormatInt(ts, 10)) + } + + // request + reqURI := uri + "?" + p.Encode() + if _, err = d.httpClient.R(). + SetContext(ctx). + SetQueryParamsFromValues(p). + SetResult(res).Get(uri); err != nil { + d.switchNode() + log.Error("Discovery: client.Get failed", "uri", reqURI, "error", err) + return nil, err + } + + if res.Code != _codeOK { + if res.Code != _codeNotModified { + log.Error("Discovery: client.Get returned code", "uri", reqURI, "code", res.Code) + } + err = fmt.Errorf("discovery.polls failed ErrCode: %d", res.Code) + return + } + + for _, app := range res.Data { + if app.LastTs == 0 { + err = ErrServerError + log.Error("Discovery: client.Get latest_timestamp is 0", "uri", reqURI, "instances", res.Data) + return + } + } + + log.Debug("Discovery: successfully polls", "uri", reqURI, "instances", res.Data) + apps = res.Data + return +} + +// Resolve Discovery resolver. +type Resolve struct { + id string + event chan struct{} + d *Discovery +} + +// Watch instance. +func (r *Resolve) Watch() <-chan struct{} { + return r.event +} + +// fetch resolver instance. +func (r *Resolve) fetch(_ context.Context) (ins *disInstancesInfo, ok bool) { + r.d.mutex.RLock() + app, ok := r.d.apps[r.id] + r.d.mutex.RUnlock() + if ok { + var appIns *disInstancesInfo + appIns, ok = app.zoneIns.Load().(*disInstancesInfo) + if !ok { + return + } + ins = new(disInstancesInfo) + ins.LastTs = appIns.LastTs + ins.Scheduler = appIns.Scheduler + ins.Instances = make(map[string][]*discoveryInstance, len(appIns.Instances)) + for zone, in := range appIns.Instances { + ins.Instances[zone] = in + } + } + return +} + +// Close resolver +func (r *Resolve) Close() error { + r.d.mutex.Lock() + if app, ok := r.d.apps[r.id]; ok && len(app.resolver) != 0 { + delete(app.resolver, r) + // TODO: delete app from builder + } + r.d.mutex.Unlock() + return nil +} diff --git a/contrib/registry/discovery/discovery_helper.go b/contrib/registry/discovery/discovery_helper.go new file mode 100644 index 0000000..e3b7b88 --- /dev/null +++ b/contrib/registry/discovery/discovery_helper.go @@ -0,0 +1,193 @@ +package discovery + +import ( + "fmt" + "net/url" + "os" + "strconv" + "time" + + "github.com/pkg/errors" + + "github.com/go-kratos/kratos/v3/registry" +) + +var ( + ErrDuplication = errors.New("register failed: instance duplicated: ") + ErrServerError = errors.New("server error") +) + +const ( + // Discovery server resource uri + _registerURL = "http://%s/discovery/register" + //_setURL = "http://%s/discovery/set" + _cancelURL = "http://%s/discovery/cancel" + _renewURL = "http://%s/discovery/renew" + _pollURL = "http://%s/discovery/polls" + + // Discovery server error codes + _codeOK = 0 + _codeNotFound = -404 + _codeNotModified = -304 + //_SERVER_ERROR = -500 + + // _registerGap is the gap to renew instance registration. + _registerGap = 30 * time.Second + _statusUP = "1" + _discoveryAppID = "infra.discovery" +) + +// Config Discovery configures. +type Config struct { + Nodes []string + Region string + Zone string + Env string + Host string +} + +func fixConfig(c *Config) error { + if c.Host == "" { + c.Host, _ = os.Hostname() + } + if len(c.Nodes) == 0 || c.Region == "" || c.Zone == "" || c.Env == "" || c.Host == "" { + return fmt.Errorf( + "invalid Discovery config nodes:%+v region:%s zone:%s deployEnv:%s host:%s", + c.Nodes, + c.Region, + c.Zone, + c.Env, + c.Host, + ) + } + return nil +} + +// discoveryInstance represents a server the client connects to. +type discoveryInstance struct { + Region string `json:"region"` // Region is region. + Zone string `json:"zone"` // Zone is IDC. + Env string `json:"env"` // Env prod/pre/uat/fat1 + AppID string `json:"appid"` // AppID is mapping service-tree appId. + Hostname string `json:"hostname"` // Hostname is hostname from docker + Addrs []string `json:"addrs"` // Addrs is the address of app instance format: scheme://host + Version string `json:"version"` // Version is publishing version. + LastTs int64 `json:"latest_timestamp"` // LastTs is instance latest updated timestamp + // Metadata is the information associated with Addr, which may be used to make load balancing decision. + Metadata map[string]string `json:"metadata"` + Status int64 `json:"status"` // Status instance status, eg: 1UP 2Waiting +} + +const _reservedInstanceIDKey = "kratos.v2.serviceinstance.id" + +// fromServerInstance convert registry.ServiceInstance into discoveryInstance +func fromServerInstance(ins *registry.ServiceInstance, config *Config) *discoveryInstance { + if ins == nil { + return nil + } + + metadata := ins.Metadata + if ins.Metadata == nil { + metadata = make(map[string]string, 8) + } + metadata[_reservedInstanceIDKey] = ins.ID + + return &discoveryInstance{ + Region: config.Region, + Zone: config.Zone, + Env: config.Env, + AppID: ins.Name, + Hostname: config.Host, + Addrs: ins.Endpoints, + Version: ins.Version, + LastTs: time.Now().Unix(), + Metadata: metadata, + Status: 1, + } +} + +// toServiceInstance convert discoveryInstance into registry.ServiceInstance +func toServiceInstance(ins *discoveryInstance) *registry.ServiceInstance { + if ins == nil { + return nil + } + + md := map[string]string{ + "region": ins.Region, + "zone": ins.Zone, + "lastTs": strconv.FormatInt(ins.LastTs, 10), + "env": ins.Env, + "hostname": ins.Hostname, + } + + if len(ins.Metadata) != 0 { + for k, v := range ins.Metadata { + md[k] = v + } + } + + return ®istry.ServiceInstance{ + ID: ins.Metadata[_reservedInstanceIDKey], + Name: ins.AppID, + Version: ins.Version, + Metadata: md, + Endpoints: ins.Addrs, + } +} + +// disInstancesInfo instance info. +type disInstancesInfo struct { + Instances map[string][]*discoveryInstance `json:"instances"` + LastTs int64 `json:"latest_timestamp"` + Scheduler *scheduler `json:"scheduler"` +} + +// scheduler scheduler. +type scheduler struct { + Clients map[string]*zoneStrategy `json:"clients"` +} + +// zoneStrategy is the scheduling strategy of all zones +type zoneStrategy struct { + Zones map[string]*strategy `json:"zones"` +} + +// strategy is zone scheduling strategy. +type strategy struct { + Weight int64 `json:"weight"` +} + +const ( + _paramKeyRegion = "region" + _paramKeyZone = "zone" + _paramKeyEnv = "env" + _paramKeyHostname = "hostname" + _paramKeyAppID = "appid" + _paramKeyAddrs = "addrs" + _paramKeyVersion = "version" + _paramKeyStatus = "status" + _paramKeyMetadata = "metadata" +) + +func newParams(c *Config) url.Values { + p := make(url.Values, 8) + if c == nil { + return p + } + + p.Set(_paramKeyRegion, c.Region) + p.Set(_paramKeyZone, c.Zone) + p.Set(_paramKeyEnv, c.Env) + p.Set(_paramKeyHostname, c.Host) + return p +} + +type discoveryCommonResp struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type discoveryPollsResp struct { + Code int `json:"code"` + Data map[string]*disInstancesInfo `json:"data"` +} diff --git a/contrib/registry/discovery/go.mod b/contrib/registry/discovery/go.mod new file mode 100644 index 0000000..03d399d --- /dev/null +++ b/contrib/registry/discovery/go.mod @@ -0,0 +1,16 @@ +module github.com/go-kratos/kratos/contrib/registry/discovery/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/go-resty/resty/v2 v2.17.2 + github.com/pkg/errors v0.9.1 +) + +require ( + golang.org/x/net v0.54.0 // indirect + golang.org/x/time v0.15.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/discovery/go.sum b/contrib/registry/discovery/go.sum new file mode 100644 index 0000000..85a4383 --- /dev/null +++ b/contrib/registry/discovery/go.sum @@ -0,0 +1,8 @@ +github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= +github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/contrib/registry/discovery/impl_discover.go b/contrib/registry/discovery/impl_discover.go new file mode 100644 index 0000000..7fb156e --- /dev/null +++ b/contrib/registry/discovery/impl_discover.go @@ -0,0 +1,88 @@ +package discovery + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + + "github.com/go-kratos/kratos/v3/registry" +) + +func filterInstancesByZone(ins *disInstancesInfo, zone string) []*registry.ServiceInstance { + zoneInstance, ok := ins.Instances[zone] + if !ok || len(zoneInstance) == 0 { + return nil + } + + out := make([]*registry.ServiceInstance, 0, len(zoneInstance)) + for _, v := range zoneInstance { + if v == nil { + continue + } + out = append(out, toServiceInstance(v)) + } + + return out +} + +func (d *Discovery) GetService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + r := d.resolveBuild(serviceName) + ins, ok := r.fetch(ctx) + if !ok { + return nil, errors.New("Discovery.GetService fetch failed") + } + + out := filterInstancesByZone(ins, d.config.Zone) + if len(out) == 0 { + return nil, fmt.Errorf("Discovery.GetService(%s) not found", serviceName) + } + + return out, nil +} + +func (d *Discovery) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + return &watcher{ + resolve: d.resolveBuild(serviceName), + serviceName: serviceName, + cancelCtx: ctx, + }, nil +} + +type watcher struct { + resolve *Resolve + + cancelCtx context.Context + serviceName string +} + +func (w *watcher) Next() ([]*registry.ServiceInstance, error) { + event := w.resolve.Watch() + + select { + case <-event: + // change event come + case <-w.cancelCtx.Done(): + return nil, fmt.Errorf("watch context canceled: %v", w.cancelCtx.Err()) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + ins, ok := w.resolve.fetch(ctx) + if !ok { + return nil, errors.New("Discovery.GetService fetch failed") + } + + out := filterInstancesByZone(ins, w.resolve.d.config.Zone) + if len(out) == 0 { + return nil, fmt.Errorf("Discovery.GetService(%s) not found", w.serviceName) + } + + return out, nil +} + +func (w *watcher) Stop() error { + return w.resolve.Close() +} diff --git a/contrib/registry/discovery/impl_registrar.go b/contrib/registry/discovery/impl_registrar.go new file mode 100644 index 0000000..9f98c6e --- /dev/null +++ b/contrib/registry/discovery/impl_registrar.go @@ -0,0 +1,123 @@ +package discovery + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/pkg/errors" + + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/registry" +) + +func (d *Discovery) Register(ctx context.Context, service *registry.ServiceInstance) (err error) { + ins := fromServerInstance(service, d.config) + + d.mutex.Lock() + if _, ok := d.registry[ins.AppID]; ok { + err = errors.Wrap(ErrDuplication, ins.AppID) + } else { + d.registry[ins.AppID] = struct{}{} + } + d.mutex.Unlock() + if err != nil { + return + } + + ctx, cancel := context.WithCancel(d.ctx) + if err = d.register(ctx, ins); err != nil { + d.mutex.Lock() + delete(d.registry, ins.AppID) + d.mutex.Unlock() + cancel() + return + } + + ch := make(chan struct{}, 1) + d.cancelFunc = func() { + cancel() + <-ch + } + + // renew the current register_service + go func() { + defer log.Warn("Discovery:register_service goroutine quit") + ticker := time.NewTicker(_registerGap) + defer ticker.Stop() + for { + select { + case <-ticker.C: + _ = d.renew(ctx, ins) + case <-ctx.Done(): + _ = d.cancel(ins) + ch <- struct{}{} + return + } + } + }() + + return +} + +// register an instance with Discovery +func (d *Discovery) register(ctx context.Context, ins *discoveryInstance) (err error) { + d.mutex.RLock() + c := d.config + d.mutex.RUnlock() + + var metadata []byte + if ins.Metadata != nil { + if metadata, err = json.Marshal(ins.Metadata); err != nil { + log.Error("Discovery: register instance marshal metadata failed", "metadata", ins.Metadata, "error", err) + } + } + res := new(struct { + Code int `json:"code"` + Message string `json:"message"` + }) + uri := fmt.Sprintf(_registerURL, d.pickNode()) + + // params + p := newParams(d.config) + p.Set(_paramKeyAppID, ins.AppID) + for _, addr := range ins.Addrs { + p.Add(_paramKeyAddrs, addr) + } + p.Set(_paramKeyVersion, ins.Version) + if ins.Status == 0 { + p.Set(_paramKeyStatus, _statusUP) + } else { + p.Set(_paramKeyStatus, strconv.FormatInt(ins.Status, 10)) + } + p.Set(_paramKeyMetadata, string(metadata)) + + // send request to Discovery server. + if _, err = d.httpClient.R(). + SetContext(ctx). + SetQueryParamsFromValues(p). + SetResult(&res). + Post(uri); err != nil { + d.switchNode() + log.Error("Discovery: register client.Get failed", + "uri", uri+"?"+p.Encode(), "zone", c.Zone, "env", c.Env, "appid", ins.AppID, "addrs", ins.Addrs, "error", err) + return + } + + if res.Code != 0 { + err = fmt.Errorf("ErrorCode: %d", res.Code) + log.Error("Discovery: register client.Get returned code", + "uri", uri, "env", c.Env, "appid", ins.AppID, "addrs", ins.Addrs, "code", res.Code) + } + + log.Info("Discovery: register client.Get succeeded", "uri", uri, "env", c.Env, "appid", ins.AppID, "addrs", ins.Addrs) + + return +} + +func (d *Discovery) Deregister(_ context.Context, service *registry.ServiceInstance) error { + ins := fromServerInstance(service, d.config) + return d.cancel(ins) +} diff --git a/contrib/registry/etcd/go.mod b/contrib/registry/etcd/go.mod new file mode 100644 index 0000000..a6dba97 --- /dev/null +++ b/contrib/registry/etcd/go.mod @@ -0,0 +1,29 @@ +module github.com/go-kratos/kratos/contrib/registry/etcd/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + go.etcd.io/etcd/client/v3 v3.6.11 + google.golang.org/grpc v1.81.0 +) + +require ( + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + go.etcd.io/etcd/api/v3 v3.6.11 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.11 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/etcd/go.sum b/contrib/registry/etcd/go.sum new file mode 100644 index 0000000..dd32073 --- /dev/null +++ b/contrib/registry/etcd/go.sum @@ -0,0 +1,100 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +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/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/etcd/api/v3 v3.6.11 h1:XFGTgrJ8nak3kB4NgMG8t7NT+lEeuuvKQAqUHKVgkWQ= +go.etcd.io/etcd/api/v3 v3.6.11/go.mod h1:HYfTh0jyh+uFgp6gMbxJteIDYY97yMuYz85Rnw6Gy9o= +go.etcd.io/etcd/client/pkg/v3 v3.6.11 h1:e41mp315Yn3QMGPmEzCyLsMINgJXTY/dX8kM++1csxU= +go.etcd.io/etcd/client/pkg/v3 v3.6.11/go.mod h1:DysuMe/inqRyC/1tjRR6hReH/VV9Lufs27YKSKBWWJg= +go.etcd.io/etcd/client/v3 v3.6.11 h1:LAByD96VmmeuairkvdAcE0RZnrmGz/q3ceeWePo9bwc= +go.etcd.io/etcd/client/v3 v3.6.11/go.mod h1:vOTDMCo+fGPEClJqcFEFSqZ+8e7WKV7AyqJjX//HR2w= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/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= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/registry/etcd/registry.go b/contrib/registry/etcd/registry.go new file mode 100644 index 0000000..67e4eec --- /dev/null +++ b/contrib/registry/etcd/registry.go @@ -0,0 +1,242 @@ +package etcd + +import ( + "context" + "fmt" + "math/rand/v2" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/go-kratos/kratos/v3/registry" +) + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +// Option is etcd registry option. +type Option func(o *options) + +type options struct { + ctx context.Context + namespace string + ttl time.Duration + maxRetry int +} + +// Context with registry context. +func Context(ctx context.Context) Option { + return func(o *options) { o.ctx = ctx } +} + +// Namespace with registry namespace. +func Namespace(ns string) Option { + return func(o *options) { o.namespace = ns } +} + +// RegisterTTL with register ttl. +func RegisterTTL(ttl time.Duration) Option { + return func(o *options) { o.ttl = ttl } +} + +func MaxRetry(num int) Option { + return func(o *options) { o.maxRetry = num } +} + +// Registry is etcd registry. +type Registry struct { + opts *options + client *clientv3.Client + kv clientv3.KV + lease clientv3.Lease + /* + ctxMap is used to store the context cancel function of each service instance. + When the service instance is deregistered, the corresponding context cancel function is called to stop the heartbeat. + */ + ctxMap map[string]*serviceCancel +} + +type serviceCancel struct { + service *registry.ServiceInstance + cancel context.CancelFunc +} + +// New creates etcd registry +func New(client *clientv3.Client, opts ...Option) (r *Registry) { + op := &options{ + ctx: context.Background(), + namespace: "/microservices", + ttl: time.Second * 15, + maxRetry: 5, + } + for _, o := range opts { + o(op) + } + return &Registry{ + opts: op, + client: client, + kv: clientv3.NewKV(client), + ctxMap: make(map[string]*serviceCancel), + } +} + +// Register the registration. +func (r *Registry) Register(ctx context.Context, service *registry.ServiceInstance) error { + key := r.registerKey(service) + value, err := marshal(service) + if err != nil { + return err + } + if r.lease != nil { + r.lease.Close() + } + r.lease = clientv3.NewLease(r.client) + leaseID, err := r.registerWithKV(ctx, key, value) + if err != nil { + return err + } + + hctx, cancel := context.WithCancel(r.opts.ctx) + r.ctxMap[key] = &serviceCancel{ + service: service, + cancel: cancel, + } + go r.heartBeat(hctx, leaseID, key, value) + return nil +} + +func (r *Registry) registerKey(service *registry.ServiceInstance) string { + return fmt.Sprintf("%s/%s/%s", r.opts.namespace, service.Name, service.ID) +} + +// Deregister the registration. +func (r *Registry) Deregister(ctx context.Context, service *registry.ServiceInstance) error { + defer func() { + if r.lease != nil { + r.lease.Close() + } + }() + // cancel heartbeat + key := r.registerKey(service) + if serviceCancel, ok := r.ctxMap[key]; ok { + serviceCancel.cancel() + delete(r.ctxMap, key) + } + _, err := r.client.Delete(ctx, key) + return err +} + +// GetService return the service instances in memory according to the service name. +func (r *Registry) GetService(ctx context.Context, name string) ([]*registry.ServiceInstance, error) { + key := r.serviceKey(name) + resp, err := r.kv.Get(ctx, key, clientv3.WithPrefix()) + if err != nil { + return nil, err + } + items := make([]*registry.ServiceInstance, 0, len(resp.Kvs)) + for _, kv := range resp.Kvs { + si, err := unmarshal(kv.Value) + if err != nil { + return nil, err + } + if si.Name != name { + continue + } + items = append(items, si) + } + return items, nil +} + +func (r *Registry) serviceKey(name string) string { + return fmt.Sprintf("%s/%s", r.opts.namespace, name) +} + +// Watch creates a watcher according to the service name. +func (r *Registry) Watch(ctx context.Context, name string) (registry.Watcher, error) { + key := r.serviceKey(name) + return newWatcher(ctx, key, name, r.client) +} + +// registerWithKV create a new lease, return current leaseID +func (r *Registry) registerWithKV(ctx context.Context, key string, value string) (clientv3.LeaseID, error) { + grant, err := r.lease.Grant(ctx, int64(r.opts.ttl.Seconds())) + if err != nil { + return 0, err + } + _, err = r.client.Put(ctx, key, value, clientv3.WithLease(grant.ID)) + if err != nil { + return 0, err + } + return grant.ID, nil +} + +func (r *Registry) heartBeat(ctx context.Context, leaseID clientv3.LeaseID, key string, value string) { + curLeaseID := leaseID + kac, err := r.client.KeepAlive(ctx, leaseID) + if err != nil { + curLeaseID = 0 + } + randSource := rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), 0)) + + for { + if curLeaseID == 0 { + // try to registerWithKV + var retreat []int + for retryCnt := 0; retryCnt < r.opts.maxRetry; retryCnt++ { + if ctx.Err() != nil { + return + } + // prevent infinite blocking + idChan := make(chan clientv3.LeaseID, 1) + errChan := make(chan error, 1) + cancelCtx, cancel := context.WithCancel(ctx) + go func() { + defer cancel() + id, registerErr := r.registerWithKV(cancelCtx, key, value) + if registerErr != nil { + errChan <- registerErr + } else { + idChan <- id + } + }() + + select { + case <-time.After(3 * time.Second): + cancel() + continue + case <-errChan: + continue + case curLeaseID = <-idChan: + } + + kac, err = r.client.KeepAlive(ctx, curLeaseID) + if err == nil { + break + } + retreat = append(retreat, 1< heartbeatRetry { + _ = e.registerEndpoint(e.ctx, ep) + retryCount = 0 + } + } + } + } +} + +func (e *Client) cancelHeartbeat(appID string) { + e.lock.Lock() + defer e.lock.Unlock() + if ch, ok := e.keepalive[appID]; ok { + ch <- struct{}{} + } +} + +func (e *Client) filterUp(apps ...Application) (res []Instance) { + for _, app := range apps { + for _, ins := range app.Instance { + if ins.Status == statusUp { + res = append(res, ins) + } + } + } + + return +} + +func (e *Client) pickServer(currentTimes int) string { + return e.urls[currentTimes%e.maxRetry] +} + +func (e *Client) shuffle() { + rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), 0)). + Shuffle(len(e.urls), func(i, j int) { + e.urls[i], e.urls[j] = e.urls[j], e.urls[i] + }) +} + +func (e *Client) buildAPI(currentTimes int, params ...string) string { + if currentTimes == 0 { + e.shuffle() + } + server := e.pickServer(currentTimes) + params = append([]string{server, e.eurekaPath}, params...) + return strings.Join(params, "/") +} + +func (e *Client) request(ctx context.Context, method string, params []string, input io.Reader, output any, i int) (bool, error) { + request, err := http.NewRequestWithContext(ctx, method, e.buildAPI(i, params...), input) + if err != nil { + return false, err + } + request.Header.Add("User-Agent", "go-eureka-client") + request.Header.Add("Accept", "application/json;charset=UTF-8") + request.Header.Add("Content-Type", "application/json;charset=UTF-8") + resp, err := e.client.Do(request) + if err != nil { + return true, err + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + if output != nil && resp.StatusCode/100 == 2 { + data, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + err = json.Unmarshal(data, output) + if err != nil { + return false, err + } + } + + if resp.StatusCode >= http.StatusBadRequest { + return false, fmt.Errorf("response Error %d", resp.StatusCode) + } + + return false, nil +} + +func (e *Client) do(ctx context.Context, method string, params []string, input io.Reader, output any) error { + for i := 0; i < e.maxRetry; i++ { + retry, err := e.request(ctx, method, params, input, output, i) + if retry { + continue + } + if err != nil { + return err + } + return nil + } + return fmt.Errorf("retry after %d times", e.maxRetry) +} diff --git a/contrib/registry/eureka/eureka.go b/contrib/registry/eureka/eureka.go new file mode 100644 index 0000000..530b638 --- /dev/null +++ b/contrib/registry/eureka/eureka.go @@ -0,0 +1,137 @@ +package eureka + +import ( + "context" + "strings" + "sync" + "time" +) + +type subscriber struct { + appID string + callBack func() +} + +type API struct { + cli *Client + allInstances map[string][]Instance + subscribers map[string]*subscriber + refreshInterval time.Duration + lock sync.Mutex +} + +func NewAPI(ctx context.Context, client *Client, refreshInterval time.Duration) *API { + e := &API{ + cli: client, + allInstances: make(map[string][]Instance), + subscribers: make(map[string]*subscriber), + refreshInterval: refreshInterval, + } + + // it is required to broadcast for the first time + go e.broadcast() + + go e.refresh(ctx) + + return e +} + +func (e *API) refresh(ctx context.Context) { + ticker := time.NewTicker(e.refreshInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + e.broadcast() + } + } +} + +func (e *API) broadcast() { + instances := e.cacheAllInstances() + if instances == nil { + return + } + + for _, subscriber := range e.subscribers { + go subscriber.callBack() + } + e.lock.Lock() + e.allInstances = instances + e.lock.Unlock() +} + +func (e *API) cacheAllInstances() map[string][]Instance { + items := make(map[string][]Instance) + instances := e.cli.FetchAllUpInstances(context.Background()) + for _, instance := range instances { + items[e.ToAppID(instance.App)] = append(items[instance.App], instance) + } + + return items +} + +func (e *API) Register(ctx context.Context, serviceName string, endpoints ...Endpoint) error { + appID := e.ToAppID(serviceName) + upInstances := make(map[string]struct{}) + + for _, ins := range e.GetService(ctx, appID) { + upInstances[ins.InstanceID] = struct{}{} + } + + for _, ep := range endpoints { + if _, ok := upInstances[ep.InstanceID]; !ok { + if err := e.cli.Register(ctx, ep); err != nil { + return err + } + go e.cli.Heartbeat(ep) + } + } + + return nil +} + +// Deregister ctx is the same as register ctx +func (e *API) Deregister(ctx context.Context, endpoints []Endpoint) error { + for _, ep := range endpoints { + if err := e.cli.Deregister(ctx, ep.AppID, ep.InstanceID); err != nil { + return err + } + } + + return nil +} + +func (e *API) Subscribe(serverName string, fn func()) error { + e.lock.Lock() + appID := e.ToAppID(serverName) + e.subscribers[appID] = &subscriber{ + appID: appID, + callBack: fn, + } + e.lock.Unlock() + go e.broadcast() + return nil +} + +func (e *API) GetService(ctx context.Context, serverName string) []Instance { + appID := e.ToAppID(serverName) + if ins, ok := e.allInstances[appID]; ok { + return ins + } + + // if not in allInstances of API, you can try to obtain it separately again + return e.cli.FetchAppUpInstances(ctx, appID) +} + +func (e *API) Unsubscribe(serverName string) { + e.lock.Lock() + delete(e.subscribers, e.ToAppID(serverName)) + e.lock.Unlock() +} + +func (e *API) ToAppID(serverName string) string { + return strings.ToUpper(serverName) +} diff --git a/contrib/registry/eureka/go.mod b/contrib/registry/eureka/go.mod new file mode 100644 index 0000000..3436ab1 --- /dev/null +++ b/contrib/registry/eureka/go.mod @@ -0,0 +1,7 @@ +module github.com/go-kratos/kratos/contrib/registry/eureka/v3 + +go 1.25.0 + +require github.com/go-kratos/kratos/v3 v3.0.0 + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/eureka/go.sum b/contrib/registry/eureka/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/contrib/registry/eureka/register.go b/contrib/registry/eureka/register.go new file mode 100644 index 0000000..86d5e5f --- /dev/null +++ b/contrib/registry/eureka/register.go @@ -0,0 +1,148 @@ +package eureka + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/go-kratos/kratos/v3/registry" +) + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +type Option func(o *Registry) + +// WithContext with registry context. +func WithContext(ctx context.Context) Option { + return func(o *Registry) { o.ctx = ctx } +} + +func WithHeartbeat(interval time.Duration) Option { + return func(o *Registry) { o.heartbeatInterval = interval } +} + +func WithRefresh(interval time.Duration) Option { + return func(o *Registry) { o.refreshInterval = interval } +} + +func WithEurekaPath(path string) Option { + return func(o *Registry) { o.eurekaPath = path } +} + +type Registry struct { + ctx context.Context + api *API + heartbeatInterval time.Duration + refreshInterval time.Duration + eurekaPath string +} + +func New(eurekaUrls []string, opts ...Option) (*Registry, error) { + r := &Registry{ + ctx: context.Background(), + heartbeatInterval: heartbeatTime, + refreshInterval: refreshTime, + eurekaPath: "eureka/v2", + } + + for _, o := range opts { + o(r) + } + + client := NewClient(eurekaUrls, + WithHeartbeatInterval(r.heartbeatInterval), + WithClientContext(r.ctx), + WithNamespace(r.eurekaPath), + ) + r.api = NewAPI(r.ctx, client, r.refreshInterval) + return r, nil +} + +// Register registers the service instance. +// The context here is exclusive to each registry instance. +func (r *Registry) Register(ctx context.Context, service *registry.ServiceInstance) error { + return r.api.Register(ctx, service.Name, r.Endpoints(service)...) +} + +// Deregister deregisters the service instance from Eureka. +func (r *Registry) Deregister(ctx context.Context, service *registry.ServiceInstance) error { + return r.api.Deregister(ctx, r.Endpoints(service)) +} + +// GetService gets services from Eureka. +func (r *Registry) GetService(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + instances := r.api.GetService(ctx, serviceName) + items := make([]*registry.ServiceInstance, 0, len(instances)) + for _, instance := range instances { + items = append(items, ®istry.ServiceInstance{ + ID: instance.Metadata["ID"], + Name: instance.Metadata["Name"], + Version: instance.Metadata["Version"], + Endpoints: []string{instance.Metadata["Endpoints"]}, + Metadata: instance.Metadata, + }) + } + + return items, nil +} + +// Watch creates a watcher for the service. It uses an independent context. +func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + return newWatch(ctx, r.api, serviceName) +} + +func (r *Registry) Endpoints(service *registry.ServiceInstance) []Endpoint { + res := make([]Endpoint, 0, len(service.Endpoints)) + for _, ep := range service.Endpoints { + start := strings.Index(ep, "//") + end := strings.LastIndex(ep, ":") + appID := strings.ToUpper(service.Name) + ip := ep[start+2 : end] + sport := ep[end+1:] + port, _ := strconv.Atoi(sport) + securePort := 443 + homePageURL := fmt.Sprintf("%s/", ep) + statusPageURL := fmt.Sprintf("%s/info", ep) + healthCheckURL := fmt.Sprintf("%s/health", ep) + instanceID := strings.Join([]string{ip, appID, sport}, ":") + metadata := make(map[string]string) + if len(service.Metadata) > 0 { + metadata = service.Metadata + } + if s, ok := service.Metadata["securePort"]; ok { + securePort, _ = strconv.Atoi(s) + } + if s, ok := service.Metadata["homePageURL"]; ok { + homePageURL = s + } + if s, ok := service.Metadata["statusPageURL"]; ok { + statusPageURL = s + } + if s, ok := service.Metadata["healthCheckURL"]; ok { + healthCheckURL = s + } + metadata["ID"] = service.ID + metadata["Name"] = service.Name + metadata["Version"] = service.Version + metadata["Endpoints"] = ep + metadata["agent"] = "go-eureka-client" + res = append(res, Endpoint{ + AppID: appID, + IP: ip, + Port: port, + SecurePort: securePort, + HomePageURL: homePageURL, + StatusPageURL: statusPageURL, + HealthCheckURL: healthCheckURL, + InstanceID: instanceID, + MetaData: metadata, + }) + } + + return res +} diff --git a/contrib/registry/eureka/register_test.go b/contrib/registry/eureka/register_test.go new file mode 100644 index 0000000..3d668d3 --- /dev/null +++ b/contrib/registry/eureka/register_test.go @@ -0,0 +1,114 @@ +package eureka + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" +) + +func TestRegistry(_ *testing.T) { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + s1 := ®istry.ServiceInstance{ + ID: "0", + Name: "helloworld", + Endpoints: []string{"http://127.0.0.1:1111"}, + } + s2 := ®istry.ServiceInstance{ + ID: "0", + Name: "helloworld2", + Endpoints: []string{"http://127.0.0.1:222"}, + } + + r, _ := New([]string{"https://127.0.0.1:18761"}, WithContext(ctx), WithHeartbeat(time.Second), WithRefresh(time.Second), WithEurekaPath("eureka")) + + go do(r, s1) + go do(r, s2) + + time.Sleep(time.Second * 20) + cancel() + time.Sleep(time.Second * 1) +} + +func do(r *Registry, s *registry.ServiceInstance) { + w, err := r.Watch(context.Background(), s.Name) + if err != nil { + log.Fatalf("Failed to watch service %q: %v", s.Name, err) + } + defer func() { + _ = w.Stop() + }() + go func() { + for { + res, nextErr := w.Next() + if nextErr != nil { + return + } + log.Printf("watch: %d", len(res)) + for _, r := range res { + log.Printf("next: %+v", r) + } + } + }() + + ctx, cancel := context.WithCancel(context.Background()) + if err = r.Register(ctx, s); err != nil { + log.Fatalf("Failed to register service %q: %v", s.Name, err) + } + + time.Sleep(time.Second * 10) + res, err := r.GetService(ctx, s.Name) + if err != nil { + log.Fatalf("Failed to get service %q: %v", s.Name, err) + } + for i, re := range res { + log.Printf("first %d re:%v\n", i, re) + } + + if len(res) != 1 && res[0].Name != s.Name { + log.Fatalf("not expected: %+v", res) + } + + if err = r.Deregister(ctx, s); err != nil { + log.Fatalf("Failed to deregister service %q: %v", s.Name, err) + } + cancel() + time.Sleep(time.Second * 10) + + res, err = r.GetService(ctx, s.Name) + if err != nil { + log.Fatalf("Failed to get service %q after deregister: %v", s.Name, err) + } + for i, re := range res { + log.Printf("second %d re:%v\n", i, re) + } + if len(res) != 0 { + log.Fatalf("not expected empty") + } +} + +func TestLock(_ *testing.T) { + type me struct { + lock sync.Mutex + } + + a := &me{} + go func() { + defer a.lock.Unlock() + a.lock.Lock() + fmt.Println("This is fmt first.") + time.Sleep(time.Second * 5) + }() + go func() { + defer a.lock.Unlock() + a.lock.Lock() + fmt.Println("This is fmt second.") + time.Sleep(time.Second * 5) + }() + time.Sleep(time.Second * 10) +} diff --git a/contrib/registry/eureka/watcher.go b/contrib/registry/eureka/watcher.go new file mode 100644 index 0000000..db52cec --- /dev/null +++ b/contrib/registry/eureka/watcher.go @@ -0,0 +1,60 @@ +package eureka + +import ( + "context" + + "github.com/go-kratos/kratos/v3/registry" +) + +var _ registry.Watcher = (*watcher)(nil) + +type watcher struct { + ctx context.Context + cancel context.CancelFunc + cli *API + watchChan chan struct{} + serverName string +} + +func newWatch(ctx context.Context, cli *API, serverName string) (*watcher, error) { + w := &watcher{ + ctx: ctx, + cli: cli, + serverName: serverName, + watchChan: make(chan struct{}, 1), + } + w.ctx, w.cancel = context.WithCancel(ctx) + e := w.cli.Subscribe( + serverName, + func() { + w.watchChan <- struct{}{} + }, + ) + return w, e +} + +func (w *watcher) Next() (services []*registry.ServiceInstance, err error) { + select { + case <-w.ctx.Done(): + return nil, w.ctx.Err() + case <-w.watchChan: + instances := w.cli.GetService(w.ctx, w.serverName) + services = make([]*registry.ServiceInstance, 0, len(instances)) + for _, instance := range instances { + services = append(services, ®istry.ServiceInstance{ + ID: instance.Metadata["ID"], + Name: instance.Metadata["Name"], + Version: instance.Metadata["Version"], + Endpoints: []string{instance.Metadata["Endpoints"]}, + Metadata: instance.Metadata, + }) + } + return + } +} + +func (w *watcher) Stop() error { + w.cancel() + w.cli.Unsubscribe(w.serverName) + return nil +} diff --git a/contrib/registry/kubernetes/go.mod b/contrib/registry/kubernetes/go.mod new file mode 100644 index 0000000..8b56a13 --- /dev/null +++ b/contrib/registry/kubernetes/go.mod @@ -0,0 +1,64 @@ +module github.com/go-kratos/kratos/contrib/registry/kubernetes/v3 + +go 1.26.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/json-iterator/go v1.1.12 + k8s.io/api v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.23.1 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/swag v0.26.0 // indirect + github.com/go-openapi/swag/cmdutils v0.26.0 // indirect + github.com/go-openapi/swag/conv v0.26.0 // indirect + github.com/go-openapi/swag/fileutils v0.26.0 // indirect + github.com/go-openapi/swag/jsonname v0.26.0 // indirect + github.com/go-openapi/swag/jsonutils v0.26.0 // indirect + github.com/go-openapi/swag/loading v0.26.0 // indirect + github.com/go-openapi/swag/mangling v0.26.0 // indirect + github.com/go-openapi/swag/netutils v0.26.0 // indirect + github.com/go-openapi/swag/stringutils v0.26.0 // indirect + github.com/go-openapi/swag/typeutils v0.26.0 // indirect + github.com/go-openapi/swag/yamlutils v0.26.0 // indirect + github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 // indirect + github.com/go-openapi/testify/v2 v2.5.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kr/text v0.2.0 // indirect + 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260512234627-ef417d054102 // indirect + k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/kubernetes/go.sum b/contrib/registry/kubernetes/go.sum new file mode 100644 index 0000000..6b86a51 --- /dev/null +++ b/contrib/registry/kubernetes/go.sum @@ -0,0 +1,130 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.0 h1:3hZD1fwydvCx/cc1R2uYNQirHqf2s6lqpKV3FcNTURA= +github.com/go-openapi/testify/enable/yaml/v2 v2.5.0/go.mod h1:TvDZKBH7ZbMaF3EqH2AwTvNQCmzyZq8K1agRjf1B+Nk= +github.com/go-openapi/testify/v2 v2.5.0 h1:UOCr63aAsMIDydZbZGqo5Ev01D4eydItRbekDuZMJLw= +github.com/go-openapi/testify/v2 v2.5.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260512234627-ef417d054102 h1:xs2ux1MvyrOdfKwS3vuFWrGuLgDOHk6id975Twx2Jss= +k8s.io/kube-openapi v0.0.0-20260512234627-ef417d054102/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo= +sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/contrib/registry/kubernetes/registry.go b/contrib/registry/kubernetes/registry.go new file mode 100644 index 0000000..405d8fc --- /dev/null +++ b/contrib/registry/kubernetes/registry.go @@ -0,0 +1,428 @@ +// Package kuberegistry registry simply implements the Kubernetes-based Registry +package kuberegistry + +import ( + "context" + "errors" + "fmt" + "net" + "net/url" + "os" + "strconv" + "strings" + "time" + + jsoniter "github.com/json-iterator/go" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + listerv1 "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + + "github.com/go-kratos/kratos/v3/registry" +) + +// Defines the key name of specific fields +// Kratos needs to cooperate with the following fields to run properly on Kubernetes: +// kratos-service-id: define the ID of the service +// kratos-service-app: define the name of the service +// kratos-service-version: define the version of the service +// kratos-service-metadata: define the metadata of the service +// kratos-service-protocols: define the protocols of the service +// +// Example Deployment: +/* +apiVersion: apps/v1 +kind: Deployment +metadata: +name: nginx +labels: + app: nginx +spec: +replicas: 5 +selector: + matchLabels: + app: nginx +template: + metadata: + labels: + app: nginx + kratos-service-id: "56991810-c77f-4a95-8190-393efa9c1a61" + kratos-service-app: "nginx" + kratos-service-version: "v3.5.0" + annotations: + kratos-service-protocols: | + {"80": "http"} + kratos-service-metadata: | + {"region": "sh", "zone": "sh001", "cluster": "pd"} + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 +*/ +const ( + // LabelsKeyServiceID is used to define the ID of the service + LabelsKeyServiceID = "kratos-service-id" + // LabelsKeyServiceName is used to define the name of the service + LabelsKeyServiceName = "kratos-service-app" + // LabelsKeyServiceVersion is used to define the version of the service + LabelsKeyServiceVersion = "kratos-service-version" + // AnnotationsKeyMetadata is used to define the metadata of the service + AnnotationsKeyMetadata = "kratos-service-metadata" + // AnnotationsKeyProtocolMap is used to define the protocols of the service + // Through the value of this field, Kratos can obtain the application layer protocol corresponding to the port + // Example value: {"80": "http", "8081": "grpc"} + AnnotationsKeyProtocolMap = "kratos-service-protocols" +) + +// The Registry simply implements service discovery based on Kubernetes +// It has not been verified in the production environment and is currently for reference only +type Registry struct { + clientSet *kubernetes.Clientset + informerFactory informers.SharedInformerFactory + podInformer cache.SharedIndexInformer + podLister listerv1.PodLister + + stopCh chan struct{} +} + +// NewRegistry is used to initialize the Registry +func NewRegistry(clientSet *kubernetes.Clientset, namespace string) *Registry { + if namespace == "" { + namespace = metav1.NamespaceAll + } + informerFactory := informers.NewSharedInformerFactoryWithOptions(clientSet, time.Minute*10, informers.WithNamespace(namespace)) + podInformer := informerFactory.Core().V1().Pods().Informer() + podLister := informerFactory.Core().V1().Pods().Lister() + return &Registry{ + clientSet: clientSet, + informerFactory: informerFactory, + podInformer: podInformer, + podLister: podLister, + stopCh: make(chan struct{}), + } +} + +// Register is used to register services +// Note that on Kubernetes, it can only be used to update the id/name/version/metadata/protocols of the current service, +// but it cannot be used to update node. +func (s *Registry) Register(ctx context.Context, service *registry.ServiceInstance) error { + // GetMetadata + metadataVal, err := marshal(service.Metadata) + if err != nil { + return err + } + + // Generate ProtocolMap + protocolMap, err := getProtocolMapByEndpoints(service.Endpoints) + if err != nil { + return err + } + protocolMapVal, err := marshal(protocolMap) + if err != nil { + return err + } + + patchBytes, err := jsoniter.Marshal(map[string]any{ + "metadata": metav1.ObjectMeta{ + Labels: map[string]string{ + LabelsKeyServiceID: service.ID, + LabelsKeyServiceName: service.Name, + LabelsKeyServiceVersion: service.Version, + }, + Annotations: map[string]string{ + AnnotationsKeyMetadata: metadataVal, + AnnotationsKeyProtocolMap: protocolMapVal, + }, + }, + }) + if err != nil { + return err + } + + if _, err = s.clientSet. + CoreV1(). + Pods(GetNamespace()). + Patch(ctx, GetPodName(), types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}); err != nil { + return err + } + return nil +} + +// Deregister the registration. +func (s *Registry) Deregister(ctx context.Context, _ *registry.ServiceInstance) error { + return s.Register(ctx, ®istry.ServiceInstance{ + Metadata: map[string]string{}, + }) +} + +// GetService return the service instances in memory according to the service name. +func (s *Registry) GetService(_ context.Context, name string) ([]*registry.ServiceInstance, error) { + pods, err := s.podLister.List(labels.SelectorFromSet(map[string]string{ + LabelsKeyServiceName: name, + })) + if err != nil { + return nil, err + } + ret := make([]*registry.ServiceInstance, 0, len(pods)) + for _, pod := range pods { + if pod.Status.Phase != corev1.PodRunning { + continue + } + instance, err := getServiceInstanceFromPod(pod) + if err != nil { + return nil, err + } + ret = append(ret, instance) + } + return ret, nil +} + +func (s *Registry) sendLatestInstances(ctx context.Context, name string, announcement chan []*registry.ServiceInstance) { + instances, err := s.GetService(ctx, name) + if err != nil { + panic(err) + } + announcement <- instances +} + +// Watch creates a watcher according to the service name. +func (s *Registry) Watch(ctx context.Context, name string) (registry.Watcher, error) { + stopCh := make(chan struct{}, 1) + announcement := make(chan []*registry.ServiceInstance, 1) + _, err := s.podInformer.AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: func(obj any) bool { + select { + case <-stopCh: + return false + case <-s.stopCh: + return false + default: + pod := obj.(*corev1.Pod) + val := pod.GetLabels()[LabelsKeyServiceName] + return val == name + } + }, + Handler: cache.ResourceEventHandlerFuncs{ + AddFunc: func(any) { + s.sendLatestInstances(ctx, name, announcement) + }, + UpdateFunc: func(any, any) { + s.sendLatestInstances(ctx, name, announcement) + }, + DeleteFunc: func(any) { + s.sendLatestInstances(ctx, name, announcement) + }, + }, + }) + if err != nil { + return nil, err + } + return NewIterator(announcement, stopCh), nil +} + +// Start is used to start the Registry +// It is non-blocking +func (s *Registry) Start() { + s.informerFactory.Start(s.stopCh) + if !cache.WaitForCacheSync(s.stopCh, s.podInformer.HasSynced) { + return + } +} + +// Close is used to close the Registry +// After closing, any callbacks generated by Watch will not be executed +func (s *Registry) Close() { + select { + case <-s.stopCh: + default: + close(s.stopCh) + } +} + +// //////////// K8S Runtime //////////// + +// ServiceAccountNamespacePath defines the location of the namespace file +const ServiceAccountNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + +var currentNamespace = LoadNamespace() + +// LoadNamespace is used to get the current namespace from the file +func LoadNamespace() string { + data, err := os.ReadFile(ServiceAccountNamespacePath) + if err != nil { + return "" + } + return string(data) +} + +// GetNamespace is used to get the namespace of the Pod where the current container is located +func GetNamespace() string { + return currentNamespace +} + +// GetPodName is used to get the name of the Pod where the current container is located +func GetPodName() string { + return os.Getenv("HOSTNAME") +} + +// //////////// ProtocolMap //////////// + +type protocolMap map[string]string + +func (m protocolMap) GetProtocol(port int32) string { + return m[strconv.Itoa(int(port))] +} + +// //////////// Iterator //////////// + +// Iterator performs the conversion from channel to iterator +// It reads the latest changes from the `chan []*registry.ServiceInstance` +// And the outside can sense the closure of Iterator through stopCh +type Iterator struct { + ch chan []*registry.ServiceInstance + stopCh chan struct{} +} + +// NewIterator is used to initialize Iterator +func NewIterator(channel chan []*registry.ServiceInstance, stopCh chan struct{}) *Iterator { + return &Iterator{ + ch: channel, + stopCh: stopCh, + } +} + +// Next will block until ServiceInstance changes +func (iter *Iterator) Next() ([]*registry.ServiceInstance, error) { + select { + case instances := <-iter.ch: + return instances, nil + case <-iter.stopCh: + return nil, ErrIteratorClosed + } +} + +// Stop is used to close the iterator +func (iter *Iterator) Stop() error { + select { + case <-iter.stopCh: + default: + close(iter.stopCh) + } + return nil +} + +// //////////// Helper Func //////////// + +func marshal(in any) (string, error) { + return jsoniter.MarshalToString(in) +} + +func unmarshal(data string, in any) error { + return jsoniter.UnmarshalFromString(data, in) +} + +func isEmptyObjectString(s string) bool { + switch s { + case "", "{}", "null", "nil", "[]": + return true + } + return false +} + +func getProtocolMapByEndpoints(endpoints []string) (protocolMap, error) { + ret := protocolMap{} + for _, endpoint := range endpoints { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + ret[u.Port()] = u.Scheme + } + return ret, nil +} + +func getProtocolMapFromPod(pod *corev1.Pod) (protocolMap, error) { + protoMap := protocolMap{} + if s := pod.Annotations[AnnotationsKeyProtocolMap]; !isEmptyObjectString(s) { + err := unmarshal(s, &protoMap) + if err != nil { + return nil, &ErrorHandleResource{Namespace: pod.Namespace, Name: pod.Name, Reason: err} + } + } + return protoMap, nil +} + +func getMetadataFromPod(pod *corev1.Pod) (map[string]string, error) { + metadata := map[string]string{} + if s := pod.Annotations[AnnotationsKeyMetadata]; !isEmptyObjectString(s) { + err := unmarshal(s, &metadata) + if err != nil { + return nil, &ErrorHandleResource{Namespace: pod.Namespace, Name: pod.Name, Reason: err} + } + } + return metadata, nil +} + +func getServiceInstanceFromPod(pod *corev1.Pod) (*registry.ServiceInstance, error) { + podIP := pod.Status.PodIP + podLabels := pod.GetLabels() + // Get Metadata + metadata, err := getMetadataFromPod(pod) + if err != nil { + return nil, err + } + // Get Protocols Definition + protocolMap, err := getProtocolMapFromPod(pod) + if err != nil { + return nil, err + } + + // Get Endpoints + var endpoints []string + for _, container := range pod.Spec.Containers { + for _, cp := range container.Ports { + port := cp.ContainerPort + protocol := protocolMap.GetProtocol(port) + if protocol == "" { + if cp.Name != "" { + protocol = strings.Split(cp.Name, "-")[0] + } else { + protocol = string(cp.Protocol) + } + } + addr := protocol + "://" + net.JoinHostPort(podIP, strconv.Itoa(int(port))) + endpoints = append(endpoints, addr) + } + } + return ®istry.ServiceInstance{ + ID: podLabels[LabelsKeyServiceID], + Name: podLabels[LabelsKeyServiceName], + Version: podLabels[LabelsKeyServiceVersion], + Metadata: metadata, + Endpoints: endpoints, + }, nil +} + +// //////////// Error Definition //////////// + +// ErrIteratorClosed defines the error that the iterator is closed +var ErrIteratorClosed = errors.New("iterator closed") + +// ErrorHandleResource defines the error that cannot handle K8S resources normally +type ErrorHandleResource struct { + Namespace string + Name string + Reason error +} + +// Error implements the error interface +func (err *ErrorHandleResource) Error() string { + return fmt.Sprintf("failed to handle resource(namespace=%s, name=%s): %s", + err.Namespace, err.Name, err.Reason) +} diff --git a/contrib/registry/kubernetes/registry_test.go b/contrib/registry/kubernetes/registry_test.go new file mode 100644 index 0000000..f51e8a0 --- /dev/null +++ b/contrib/registry/kubernetes/registry_test.go @@ -0,0 +1,185 @@ +package kuberegistry + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + + appsv1 "k8s.io/api/apps/v1" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" +) + +const ( + namespace = "default" + deployName = "hello-deployment" + podName = "hello" +) + +var deployment = appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deployName, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: int32Ptr(1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": podName, + }, + }, + Template: apiv1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": podName, + }, + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "nginx", + Image: "nginx:alpine", + Ports: []apiv1.ContainerPort{ + { + Name: "http", + Protocol: apiv1.ProtocolTCP, + ContainerPort: 80, + }, + }, + Command: []string{ + "nginx", + "-g", + "daemon off;", + }, + }, + }, + }, + }, + }, +} + +func getClientSet() (*kubernetes.Clientset, error) { + restConfig, err := rest.InClusterConfig() + home := homedir.HomeDir() + + if err != nil { + kubeconfig := filepath.Join(home, ".kube", "config") + restConfig, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, err + } + } + clientSet, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, err + } + return clientSet, nil +} + +func int32Ptr(i int32) *int32 { return &i } + +func TestSetEnv(t *testing.T) { + os.Setenv("HOSTNAME", podName) + if os.Getenv("HOSTNAME") != podName { + t.Fatal("error") + } +} + +func TestRegistry(t *testing.T) { + currentNamespace = "default" + + clientSet, err := getClientSet() + if err != nil { + t.Fatal(err) + } + + r := NewRegistry(clientSet, currentNamespace) + r.Start() + + svrHello := ®istry.ServiceInstance{ + ID: "1", + Name: "hello", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:80"}, + } + _, err = clientSet.AppsV1().Deployments(namespace).Create(context.Background(), &deployment, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + watch, err := r.Watch(context.Background(), svrHello.Name) + if err != nil { + t.Fatal(err) + } + + defer func() { + _ = watch.Stop() + }() + + go func() { + for { + res, err1 := watch.Next() + if err1 != nil { + return + } + t.Logf("watch: %d", len(res)) + for _, r := range res { + t.Logf("next: %+v", r) + } + } + }() + time.Sleep(time.Second) + + pod, err := clientSet.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: "app=hello", + }) + if err != nil { + t.Fatal(err) + } + + if len(pod.Items) < 1 { + t.Fatal("fetch resource error") + } + + os.Setenv("HOSTNAME", pod.Items[0].Name) + + // Always remember delete test resource + defer func() { + _ = clientSet.AppsV1().Deployments(namespace).Delete(context.Background(), deployName, metav1.DeleteOptions{}) + }() + + if err = r.Register(context.Background(), svrHello); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + res, err := r.GetService(context.Background(), svrHello.Name) + if err != nil { + t.Fatal(err) + } + + if len(res) != 1 && res[0].Name != svrHello.Name { + t.Fatal(err) + } + + if err1 := r.Deregister(context.Background(), svrHello); err1 != nil { + t.Fatal(err1) + } + + time.Sleep(time.Second) + + res, err = r.GetService(context.Background(), svrHello.Name) + if err != nil { + t.Fatal(err) + } + if len(res) != 0 { + t.Fatal("not expected empty") + } +} diff --git a/contrib/registry/nacos/README.md b/contrib/registry/nacos/README.md new file mode 100644 index 0000000..709de31 --- /dev/null +++ b/contrib/registry/nacos/README.md @@ -0,0 +1,88 @@ +# Nacos Registry + +## example +### server +```go +package main + +import ( + "log" + + "github.com/nacos-group/nacos-sdk-go/v2/clients" + "github.com/nacos-group/nacos-sdk-go/v2/common/constant" + "github.com/nacos-group/nacos-sdk-go/v2/vo" + + "github.com/go-kratos/kratos/contrib/registry/nacos/v3" + "github.com/go-kratos/kratos/v3" +) + +func main() { + sc := []constant.ServerConfig{ + *constant.NewServerConfig("127.0.0.1", 8848), + } + + client, err := clients.NewNamingClient( + vo.NacosClientParam{ + ServerConfigs: sc, + }, + ) + + if err != nil { + log.Panic(err) + } + + r := nacos.New(client) + + // server + app := kratos.New( + kratos.Name("helloworld"), + kratos.Registrar(r), + ) + if err := app.Run(); err != nil { + log.Fatal(err) + } +} +``` +### client +```go +package main + +import ( + "context" + "log" + + "github.com/nacos-group/nacos-sdk-go/v2/clients" + "github.com/nacos-group/nacos-sdk-go/v2/common/constant" + "github.com/nacos-group/nacos-sdk-go/v2/vo" + + "github.com/go-kratos/kratos/contrib/registry/nacos/v3" + "github.com/go-kratos/kratos/v3/transport/grpc" +) + +func main() { + cc := constant.ClientConfig{ + NamespaceId: "public", + TimeoutMs: 5000, + } + + client, err := clients.NewNamingClient( + vo.NacosClientParam{ + ClientConfig: &cc, + }, + ) + + if err != nil { + log.Panic(err) + } + + r := nacos.New(client) + + // client + conn, err := grpc.NewClient( + context.Background(), + grpc.WithEndpoint("discovery:///helloworld"), + grpc.WithDiscovery(r), + ) + defer conn.Close() +} +``` diff --git a/contrib/registry/nacos/fake_client_test.go b/contrib/registry/nacos/fake_client_test.go new file mode 100644 index 0000000..38d3bb5 --- /dev/null +++ b/contrib/registry/nacos/fake_client_test.go @@ -0,0 +1,174 @@ +package nacos + +import ( + "errors" + "sync" + + "github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client" + "github.com/nacos-group/nacos-sdk-go/v2/model" + "github.com/nacos-group/nacos-sdk-go/v2/vo" +) + +// fakeNamingClient is a lightweight in-memory fake implementation of +// naming_client.INamingClient used by tests to avoid needing a running +// Nacos server. +type fakeNamingClient struct { + mu sync.RWMutex + services map[string][]model.Instance // key: group@@serviceName + subscribers map[string][]*vo.SubscribeParam +} + +// NewFakeNamingClient creates a new fake naming client. +func NewFakeNamingClient() naming_client.INamingClient { + return &fakeNamingClient{ + services: make(map[string][]model.Instance), + subscribers: make(map[string][]*vo.SubscribeParam), + } +} + +func (f *fakeNamingClient) notify(serviceKey string) { + // Copy subscribers slice under lock to avoid races if Subscribe/Unsubscribe + // modifies the slice concurrently. + f.mu.RLock() + subs := append([]*vo.SubscribeParam(nil), f.subscribers[serviceKey]...) + // Make a single copy of hosts while still under the read-lock. + hosts := append([]model.Instance(nil), f.services[serviceKey]...) + f.mu.RUnlock() + + for _, sp := range subs { + if sp != nil && sp.SubscribeCallback != nil { + // deliver a copy of hosts per callback to be safe if callback mutates it + sp.SubscribeCallback(append([]model.Instance(nil), hosts...), nil) + } + } +} + +func (f *fakeNamingClient) RegisterInstance(param vo.RegisterInstanceParam) (bool, error) { + if param.ServiceName == "" { + return false, errors.New("service name empty") + } + key := param.GroupName + "@@" + param.ServiceName + cluster := param.ClusterName + if cluster == "" { + cluster = "DEFAULT" + } + inst := model.Instance{ + Ip: param.Ip, + Port: param.Port, + ServiceName: key, // store with group prefix to mimic server GetService behavior + ClusterName: cluster, + Metadata: param.Metadata, + } + // leave InstanceId empty to exercise fallback behavior + f.mu.Lock() + f.services[key] = append(f.services[key], inst) + f.mu.Unlock() + // notify subscribers + go f.notify(key) + return true, nil +} + +func (f *fakeNamingClient) DeregisterInstance(param vo.DeregisterInstanceParam) (bool, error) { + key := param.GroupName + "@@" + param.ServiceName + f.mu.Lock() + defer f.mu.Unlock() + list := f.services[key] + for i := 0; i < len(list); i++ { + if list[i].Ip == param.Ip && list[i].Port == param.Port { + // remove + list = append(list[:i], list[i+1:]...) + i-- + } + } + f.services[key] = list + go f.notify(key) + return true, nil +} + +func (f *fakeNamingClient) SelectInstances(param vo.SelectInstancesParam) ([]model.Instance, error) { + key := param.GroupName + "@@" + param.ServiceName + f.mu.RLock() + defer f.mu.RUnlock() + list := f.services[key] + if len(list) == 0 { + return nil, errors.New("service not found") + } + // return a copy + out := make([]model.Instance, len(list)) + copy(out, list) + return out, nil +} + +func (f *fakeNamingClient) GetService(param vo.GetServiceParam) (model.Service, error) { + key := param.GroupName + "@@" + param.ServiceName + f.mu.RLock() + defer f.mu.RUnlock() + list := f.services[key] + hosts := make([]model.Instance, len(list)) + copy(hosts, list) + // Return empty Service with nil error when no hosts — watcher expects + // an initial empty response instead of an error. + return model.Service{Hosts: hosts}, nil +} + +func (f *fakeNamingClient) Subscribe(param *vo.SubscribeParam) error { + key := param.GroupName + "@@" + param.ServiceName + f.mu.Lock() + f.subscribers[key] = append(f.subscribers[key], param) + f.mu.Unlock() + // call once to prime the watcher + go func() { + f.mu.RLock() + hosts := append([]model.Instance(nil), f.services[key]...) + f.mu.RUnlock() + if param.SubscribeCallback != nil { + param.SubscribeCallback(hosts, nil) + } + }() + return nil +} + +func (f *fakeNamingClient) Unsubscribe(param *vo.SubscribeParam) error { + key := param.GroupName + "@@" + param.ServiceName + f.mu.Lock() + defer f.mu.Unlock() + subs := f.subscribers[key] + for i, s := range subs { + if s == param { + subs = append(subs[:i], subs[i+1:]...) + break + } + } + f.subscribers[key] = subs + return nil +} + +// The rest of the INamingClient methods are not used by tests but +// must be provided to satisfy the interface. Return sensible defaults. +func (f *fakeNamingClient) BatchRegisterInstance(_ vo.BatchRegisterInstanceParam) (bool, error) { + return true, nil +} +func (f *fakeNamingClient) CloseClient() {} +func (f *fakeNamingClient) GetAllServicesInfo(_ vo.GetAllServiceInfoParam) (model.ServiceList, error) { + return model.ServiceList{}, nil +} + +func (f *fakeNamingClient) SelectAllInstances(_ vo.SelectAllInstancesParam) ([]model.Instance, error) { + return nil, nil +} + +func (f *fakeNamingClient) SelectOneHealthyInstance(arg0 vo.SelectOneHealthInstanceParam) (*model.Instance, error) { + // pick the first match if any + key := arg0.GroupName + "@@" + arg0.ServiceName + f.mu.RLock() + defer f.mu.RUnlock() + list := f.services[key] + if len(list) == 0 { + return nil, errors.New("no instance") + } + return &list[0], nil +} +func (f *fakeNamingClient) ServerHealthy() bool { return true } +func (f *fakeNamingClient) UpdateInstance(_ vo.UpdateInstanceParam) (bool, error) { + return true, nil +} diff --git a/contrib/registry/nacos/go.mod b/contrib/registry/nacos/go.mod new file mode 100644 index 0000000..3efb7fd --- /dev/null +++ b/contrib/registry/nacos/go.mod @@ -0,0 +1,60 @@ +module github.com/go-kratos/kratos/contrib/registry/nacos/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/golang/mock v1.6.0 + github.com/nacos-group/nacos-sdk-go/v2 v2.3.5 +) + +require ( + github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect + github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect + github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect + github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect + github.com/alibabacloud-go/openapi-util v0.1.2 // indirect + github.com/alibabacloud-go/tea v1.4.0 // indirect + github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect + github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect + github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.9 // indirect + github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect + github.com/aliyun/credentials-go v1.4.12 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/orcaman/concurrent-map v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // 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/procfs v0.20.1 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/ini.v1 v1.67.2 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/nacos/go.sum b/contrib/registry/nacos/go.sum new file mode 100644 index 0000000..e39a7cf --- /dev/null +++ b/contrib/registry/nacos/go.sum @@ -0,0 +1,391 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= +github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= +github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= +github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= +github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= +github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/openapi-util v0.1.2 h1:aljdyAPotH4xHymo5wzARjcHb3Org0zKnLP4RxS0JGY= +github.com/alibabacloud-go/openapi-util v0.1.2/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA= +github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= +github.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA= +github.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ= +github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU= +github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg= +github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU= +github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.9 h1:GCzdF5XjQ6LED6wdvEq2MKSlBQmQDkvYRw2S8Qnbo4Y= +github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.9/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU= +github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 h1:8S0mtD101RDYa0LXwdoqgN0RxdMmmJYjq8g2mk7/lQ4= +github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5/go.mod h1:M19fxYz3gpm0ETnoKweYyYtqrtnVtrpKFpwsghbw+cQ= +github.com/aliyun/credentials-go v1.4.12 h1:7D8eXGotNwthZuUEgAMgBoqxmIHwfaPVwW+/04LIJSQ= +github.com/aliyun/credentials-go v1.4.12/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= +github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/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/nacos-group/nacos-sdk-go/v2 v2.3.5 h1:Hux7C4N4rWhwBF5Zm4yyYskrs9VTgrRTA8DZjoEhQTs= +github.com/nacos-group/nacos-sdk-go/v2 v2.3.5/go.mod h1:ygUBdt7eGeYBt6Lz2HO3wx7crKXk25Mp80568emGMWU= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= +github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/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-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/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-20210330210617-4fbd30eecc44/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-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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/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-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.1.1/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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +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.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/contrib/registry/nacos/mock_nacos_test.go b/contrib/registry/nacos/mock_nacos_test.go new file mode 100644 index 0000000..4f51b3d --- /dev/null +++ b/contrib/registry/nacos/mock_nacos_test.go @@ -0,0 +1,225 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client (interfaces: INamingClient) + +// Package nacos is a generated GoMock package. +package nacos + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/nacos-group/nacos-sdk-go/v2/model" + vo "github.com/nacos-group/nacos-sdk-go/v2/vo" +) + +// MockINamingClient is a mock of INamingClient interface. +type MockINamingClient struct { + ctrl *gomock.Controller + recorder *MockINamingClientMockRecorder +} + +// MockINamingClientMockRecorder is the mock recorder for MockINamingClient. +type MockINamingClientMockRecorder struct { + mock *MockINamingClient +} + +// NewMockINamingClient creates a new mock instance. +func NewMockINamingClient(ctrl *gomock.Controller) *MockINamingClient { + mock := &MockINamingClient{ctrl: ctrl} + mock.recorder = &MockINamingClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockINamingClient) EXPECT() *MockINamingClientMockRecorder { + return m.recorder +} + +// BatchRegisterInstance mocks base method. +func (m *MockINamingClient) BatchRegisterInstance(arg0 vo.BatchRegisterInstanceParam) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BatchRegisterInstance", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BatchRegisterInstance indicates an expected call of BatchRegisterInstance. +func (mr *MockINamingClientMockRecorder) BatchRegisterInstance(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchRegisterInstance", reflect.TypeOf((*MockINamingClient)(nil).BatchRegisterInstance), arg0) +} + +// CloseClient mocks base method. +func (m *MockINamingClient) CloseClient() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CloseClient") +} + +// CloseClient indicates an expected call of CloseClient. +func (mr *MockINamingClientMockRecorder) CloseClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseClient", reflect.TypeOf((*MockINamingClient)(nil).CloseClient)) +} + +// DeregisterInstance mocks base method. +func (m *MockINamingClient) DeregisterInstance(arg0 vo.DeregisterInstanceParam) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeregisterInstance", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeregisterInstance indicates an expected call of DeregisterInstance. +func (mr *MockINamingClientMockRecorder) DeregisterInstance(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeregisterInstance", reflect.TypeOf((*MockINamingClient)(nil).DeregisterInstance), arg0) +} + +// GetAllServicesInfo mocks base method. +func (m *MockINamingClient) GetAllServicesInfo(arg0 vo.GetAllServiceInfoParam) (model.ServiceList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllServicesInfo", arg0) + ret0, _ := ret[0].(model.ServiceList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllServicesInfo indicates an expected call of GetAllServicesInfo. +func (mr *MockINamingClientMockRecorder) GetAllServicesInfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllServicesInfo", reflect.TypeOf((*MockINamingClient)(nil).GetAllServicesInfo), arg0) +} + +// GetService mocks base method. +func (m *MockINamingClient) GetService(arg0 vo.GetServiceParam) (model.Service, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetService", arg0) + ret0, _ := ret[0].(model.Service) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetService indicates an expected call of GetService. +func (mr *MockINamingClientMockRecorder) GetService(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetService", reflect.TypeOf((*MockINamingClient)(nil).GetService), arg0) +} + +// RegisterInstance mocks base method. +func (m *MockINamingClient) RegisterInstance(arg0 vo.RegisterInstanceParam) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RegisterInstance", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RegisterInstance indicates an expected call of RegisterInstance. +func (mr *MockINamingClientMockRecorder) RegisterInstance(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterInstance", reflect.TypeOf((*MockINamingClient)(nil).RegisterInstance), arg0) +} + +// SelectAllInstances mocks base method. +func (m *MockINamingClient) SelectAllInstances(arg0 vo.SelectAllInstancesParam) ([]model.Instance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SelectAllInstances", arg0) + ret0, _ := ret[0].([]model.Instance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SelectAllInstances indicates an expected call of SelectAllInstances. +func (mr *MockINamingClientMockRecorder) SelectAllInstances(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectAllInstances", reflect.TypeOf((*MockINamingClient)(nil).SelectAllInstances), arg0) +} + +// SelectInstances mocks base method. +func (m *MockINamingClient) SelectInstances(arg0 vo.SelectInstancesParam) ([]model.Instance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SelectInstances", arg0) + ret0, _ := ret[0].([]model.Instance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SelectInstances indicates an expected call of SelectInstances. +func (mr *MockINamingClientMockRecorder) SelectInstances(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectInstances", reflect.TypeOf((*MockINamingClient)(nil).SelectInstances), arg0) +} + +// SelectOneHealthyInstance mocks base method. +func (m *MockINamingClient) SelectOneHealthyInstance(arg0 vo.SelectOneHealthInstanceParam) (*model.Instance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SelectOneHealthyInstance", arg0) + ret0, _ := ret[0].(*model.Instance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SelectOneHealthyInstance indicates an expected call of SelectOneHealthyInstance. +func (mr *MockINamingClientMockRecorder) SelectOneHealthyInstance(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectOneHealthyInstance", reflect.TypeOf((*MockINamingClient)(nil).SelectOneHealthyInstance), arg0) +} + +// ServerHealthy mocks base method. +func (m *MockINamingClient) ServerHealthy() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ServerHealthy") + ret0, _ := ret[0].(bool) + return ret0 +} + +// ServerHealthy indicates an expected call of ServerHealthy. +func (mr *MockINamingClientMockRecorder) ServerHealthy() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerHealthy", reflect.TypeOf((*MockINamingClient)(nil).ServerHealthy)) +} + +// Subscribe mocks base method. +func (m *MockINamingClient) Subscribe(arg0 *vo.SubscribeParam) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Subscribe", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Subscribe indicates an expected call of Subscribe. +func (mr *MockINamingClientMockRecorder) Subscribe(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockINamingClient)(nil).Subscribe), arg0) +} + +// Unsubscribe mocks base method. +func (m *MockINamingClient) Unsubscribe(arg0 *vo.SubscribeParam) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Unsubscribe", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Unsubscribe indicates an expected call of Unsubscribe. +func (mr *MockINamingClientMockRecorder) Unsubscribe(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockINamingClient)(nil).Unsubscribe), arg0) +} + +// UpdateInstance mocks base method. +func (m *MockINamingClient) UpdateInstance(arg0 vo.UpdateInstanceParam) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateInstance", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateInstance indicates an expected call of UpdateInstance. +func (mr *MockINamingClientMockRecorder) UpdateInstance(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstance", reflect.TypeOf((*MockINamingClient)(nil).UpdateInstance), arg0) +} diff --git a/contrib/registry/nacos/registry.go b/contrib/registry/nacos/registry.go new file mode 100644 index 0000000..eae4c53 --- /dev/null +++ b/contrib/registry/nacos/registry.go @@ -0,0 +1,217 @@ +package nacos + +import ( + "context" + "errors" + "fmt" + "maps" + "math" + "net" + "net/url" + "strconv" + + "github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client" + "github.com/nacos-group/nacos-sdk-go/v2/common/constant" + "github.com/nacos-group/nacos-sdk-go/v2/vo" + + "github.com/go-kratos/kratos/v3/registry" +) + +var ErrServiceInstanceNameEmpty = errors.New("kratos/nacos: ServiceInstance.Name can not be empty") + +const ( + defaultKind = "grpc" + kindKey = "kind" + versionKey = "version" +) + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +type options struct { + prefix string + weight float64 + cluster string + group string + kind string +} + +// Option is nacos option. +type Option func(o *options) + +// WithPrefix with prefix path. +func WithPrefix(prefix string) Option { + return func(o *options) { o.prefix = prefix } +} + +// WithWeight with weight option. +func WithWeight(weight float64) Option { + return func(o *options) { o.weight = weight } +} + +// WithCluster with cluster option. +func WithCluster(cluster string) Option { + return func(o *options) { o.cluster = cluster } +} + +// WithGroup with group option. +func WithGroup(group string) Option { + return func(o *options) { o.group = group } +} + +// WithDefaultKind with default kind option. +func WithDefaultKind(kind string) Option { + return func(o *options) { o.kind = kind } +} + +// Registry is nacos registry. +type Registry struct { + opts options + cli naming_client.INamingClient +} + +// New new a nacos registry. +func New(cli naming_client.INamingClient, opts ...Option) (r *Registry) { + op := options{ + prefix: "/microservices", + cluster: "DEFAULT", + group: constant.DEFAULT_GROUP, + weight: 100, + kind: defaultKind, + } + for _, option := range opts { + option(&op) + } + return &Registry{ + opts: op, + cli: cli, + } +} + +// Register the registration. +func (r *Registry) Register(_ context.Context, si *registry.ServiceInstance) error { + if si.Name == "" { + return ErrServiceInstanceNameEmpty + } + for _, endpoint := range si.Endpoints { + u, err := url.Parse(endpoint) + if err != nil { + return err + } + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return err + } + p, err := strconv.Atoi(port) + if err != nil { + return err + } + weight := r.opts.weight + var rmd map[string]string + if si.Metadata == nil { + rmd = map[string]string{ + kindKey: u.Scheme, + versionKey: si.Version, + } + } else { + rmd = maps.Clone(si.Metadata) + rmd[kindKey] = u.Scheme + rmd[versionKey] = si.Version + if w, ok := si.Metadata["weight"]; ok { + weight, err = strconv.ParseFloat(w, 64) + if err != nil { + weight = r.opts.weight + } + } + } + _, e := r.cli.RegisterInstance(vo.RegisterInstanceParam{ + Ip: host, + Port: uint64(p), + ServiceName: si.Name + "." + u.Scheme, + Weight: weight, + Enable: true, + Healthy: true, + Ephemeral: true, + Metadata: rmd, + ClusterName: r.opts.cluster, + GroupName: r.opts.group, + }) + if e != nil { + return fmt.Errorf("RegisterInstance err %v,%v", e, endpoint) + } + } + return nil +} + +// Deregister the registration. +func (r *Registry) Deregister(_ context.Context, service *registry.ServiceInstance) error { + for _, endpoint := range service.Endpoints { + u, err := url.Parse(endpoint) + if err != nil { + return err + } + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return err + } + p, err := strconv.Atoi(port) + if err != nil { + return err + } + if _, err = r.cli.DeregisterInstance(vo.DeregisterInstanceParam{ + Ip: host, + Port: uint64(p), + ServiceName: service.Name + "." + u.Scheme, + GroupName: r.opts.group, + Cluster: r.opts.cluster, + Ephemeral: true, + }); err != nil { + return err + } + } + return nil +} + +// Watch creates a watcher according to the service name. +func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + return newWatcher(ctx, r.cli, serviceName, r.opts.group, r.opts.kind, []string{r.opts.cluster}) +} + +// GetService return the service instances in memory according to the service name. +func (r *Registry) GetService(_ context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + res, err := r.cli.SelectInstances(vo.SelectInstancesParam{ + ServiceName: serviceName, + GroupName: r.opts.group, + HealthyOnly: true, + }) + if err != nil { + return nil, err + } + items := make([]*registry.ServiceInstance, 0, len(res)) + for _, in := range res { + kind := r.opts.kind + weight := r.opts.weight + if k, ok := in.Metadata[kindKey]; ok { + kind = k + } + if in.Weight > 0 { + weight = in.Weight + } + + // derive instance id (instanceKey returns InstanceId when present, + // otherwise falls back to ip#port#cluster#service) + id := instanceKey(&in) + r := ®istry.ServiceInstance{ + ID: id, + Name: in.ServiceName, + Version: in.Metadata[versionKey], + Metadata: in.Metadata, + Endpoints: []string{kind + "://" + net.JoinHostPort(in.Ip, strconv.Itoa(int(in.Port)))}, + } + r.Metadata["weight"] = strconv.FormatInt(int64(math.Ceil(weight)), 10) + items = append(items, r) + } + return items, nil +} diff --git a/contrib/registry/nacos/registry_test.go b/contrib/registry/nacos/registry_test.go new file mode 100644 index 0000000..8f3435c --- /dev/null +++ b/contrib/registry/nacos/registry_test.go @@ -0,0 +1,446 @@ +package nacos + +import ( + "context" + "reflect" + "testing" + + // external nacos client creation removed; tests use fake client + "github.com/go-kratos/kratos/v3/registry" +) + +func TestRegistry_Register(t *testing.T) { + // real Nacos client config omitted; tests use in-memory fake client below + // use in-memory fake client so tests don't need a running nacos server + client := NewFakeNamingClient() + r := New(client) + + testServer := ®istry.ServiceInstance{ + ID: "1", + Name: "test1", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:8080?isSecure=false"}, + } + testServerWithMetadata := ®istry.ServiceInstance{ + ID: "1", + Name: "test1", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:8080?isSecure=false"}, + Metadata: map[string]string{"idc": "shanghai-xs"}, + } + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + service *registry.ServiceInstance + } + tests := []struct { + name string + fields fields + args args + wantErr bool + deferFunc func(t *testing.T) + }{ + { + name: "normal", + fields: fields{ + registry: New(client), + }, + args: args{ + ctx: context.Background(), + service: testServer, + }, + wantErr: false, + deferFunc: func(t *testing.T) { + if err := r.Deregister(context.Background(), testServer); err != nil { + t.Error(err) + } + }, + }, + { + name: "withMetadata", + fields: fields{ + registry: New(client), + }, + args: args{ + ctx: context.Background(), + service: testServerWithMetadata, + }, + wantErr: false, + deferFunc: func(t *testing.T) { + if err := r.Deregister(context.Background(), testServerWithMetadata); err != nil { + t.Error(err) + } + }, + }, + { + name: "error", + fields: fields{ + registry: New(client), + }, + args: args{ + ctx: context.Background(), + service: ®istry.ServiceInstance{ + ID: "1", + Name: "", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:8080?isSecure=false"}, + }, + }, + wantErr: true, + }, + { + name: "urlError", + fields: fields{ + registry: New(client), + }, + args: args{ + ctx: context.Background(), + service: ®istry.ServiceInstance{ + ID: "1", + Name: "test", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + }, + }, + wantErr: true, + }, + { + name: "portError", + fields: fields{ + registry: New(client), + }, + args: args{ + ctx: context.Background(), + service: ®istry.ServiceInstance{ + ID: "1", + Name: "test", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1888"}, + }, + }, + wantErr: true, + }, + { + name: "withCluster", + fields: fields{ + registry: New(client, WithCluster("test")), + }, + args: args{ + ctx: context.Background(), + service: testServer, + }, + wantErr: false, + }, + { + name: "withGroup", + fields: fields{ + registry: New(client, WithGroup("TEST_GROUP")), + }, + args: args{ + ctx: context.Background(), + service: testServer, + }, + wantErr: false, + }, + { + name: "withWeight", + fields: fields{ + registry: New(client, WithWeight(200)), + }, + args: args{ + ctx: context.Background(), + service: testServer, + }, + wantErr: false, + }, + { + name: "withPrefix", + fields: fields{ + registry: New(client, WithPrefix("test")), + }, + args: args{ + ctx: context.Background(), + service: testServer, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.fields.registry + if err := r.Register(tt.args.ctx, tt.args.service); (err != nil) != tt.wantErr { + t.Errorf("Register error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestRegistry_Deregister(t *testing.T) { + testServer := ®istry.ServiceInstance{ + ID: "1", + Name: "test2", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:8080?isSecure=false"}, + } + + type args struct { + ctx context.Context + service *registry.ServiceInstance + } + tests := []struct { + name string + args args + wantErr bool + preFunc func(t *testing.T, r *Registry) + }{ + { + name: "normal", + args: args{ + ctx: context.Background(), + service: testServer, + }, + wantErr: false, + preFunc: func(t *testing.T, r *Registry) { + // register using the same registry/client used by the test + if err := r.Register(context.Background(), testServer); err != nil { + t.Error(err) + } + }, + }, + { + name: "error", + args: args{ + ctx: context.Background(), + service: ®istry.ServiceInstance{ + ID: "1", + Name: "test", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + }, + }, + wantErr: true, + }, + { + name: "errorPort", + args: args{ + ctx: context.Background(), + service: ®istry.ServiceInstance{ + ID: "1", + Name: "notExist", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.18080"}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // use in-memory fake client so tests don't need a running nacos server + client := NewFakeNamingClient() + r := New(client) + if tt.preFunc != nil { + tt.preFunc(t, r) + } + if err := r.Deregister(tt.args.ctx, tt.args.service); (err != nil) != tt.wantErr { + t.Errorf("Deregister error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestRegistry_GetService(t *testing.T) { + // use in-memory fake client so tests don't need a running nacos server + client := NewFakeNamingClient() + r := New(client) + testServer := ®istry.ServiceInstance{ + ID: "1", + Name: "test3", + Version: "v1.0.0", + Endpoints: []string{"grpc://127.0.0.1:8080?isSecure=false"}, + } + + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + serviceName string + } + tests := []struct { + name string + fields fields + args args + want []*registry.ServiceInstance + wantErr bool + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + }{ + { + name: "normal", + preFunc: func(t *testing.T) { + if err := r.Register(context.Background(), testServer); err != nil { + t.Error(err) + } + }, + deferFunc: func(t *testing.T) { + if err := r.Deregister(context.Background(), testServer); err != nil { + t.Error(err) + } + }, + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + serviceName: testServer.Name + "." + "grpc", + }, + want: []*registry.ServiceInstance{{ + ID: "127.0.0.1#8080#DEFAULT#DEFAULT_GROUP@@test3.grpc", + Name: "DEFAULT_GROUP@@test3.grpc", + Version: "v1.0.0", + Metadata: map[string]string{"version": "v1.0.0", "kind": "grpc", "weight": "100"}, + Endpoints: []string{"grpc://127.0.0.1:8080"}, + }}, + wantErr: false, + }, + { + name: "errorNotExist", + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + serviceName: "notExist", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc(t) + } + if tt.deferFunc != nil { + defer tt.deferFunc(t) + } + r := tt.fields.registry + got, err := r.GetService(tt.args.ctx, tt.args.serviceName) + if (err != nil) != tt.wantErr { + t.Errorf("GetService error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService got = %v", got) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetService got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRegistry_Watch(t *testing.T) { + // use in-memory fake client so tests don't need a running nacos server + client := NewFakeNamingClient() + r := New(client) + + testServer := ®istry.ServiceInstance{ + ID: "1", + Name: "test4", + Version: "v1.0.0", + Endpoints: []string{"grpc://127.0.0.1:8080?isSecure=false"}, + } + + cancelCtx, cancel := context.WithCancel(context.Background()) + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + serviceName string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + want []*registry.ServiceInstance + processFunc func(t *testing.T) + }{ + { + name: "normal", + fields: fields{ + registry: New(client), + }, + args: args{ + ctx: context.Background(), + serviceName: testServer.Name + "." + "grpc", + }, + wantErr: false, + want: []*registry.ServiceInstance{{ + ID: "127.0.0.1#8080#DEFAULT#DEFAULT_GROUP@@test4.grpc", + Name: "DEFAULT_GROUP@@test4.grpc", + Version: "v1.0.0", + Metadata: map[string]string{"version": "v1.0.0", "kind": "grpc"}, + Endpoints: []string{"grpc://127.0.0.1:8080"}, + }}, + processFunc: func(t *testing.T) { + if err := r.Register(context.Background(), testServer); err != nil { + t.Error(err) + } + }, + }, + { + name: "ctxCancel", + fields: fields{ + registry: r, + }, + args: args{ + ctx: cancelCtx, + serviceName: testServer.Name, + }, + wantErr: true, + want: nil, + processFunc: func(*testing.T) { + cancel() + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.fields.registry + watch, err := r.Watch(tt.args.ctx, tt.args.serviceName) + if err != nil { + t.Error(err) + return + } + defer func() { + err = watch.Stop() + if err != nil { + t.Error(err) + } + }() + _, err = watch.Next() + if err != nil { + t.Error(err) + return + } + + if tt.processFunc != nil { + tt.processFunc(t) + } + + got, err := watch.Next() + if (err != nil) != tt.wantErr { + t.Errorf("Watch error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Watch watcher = %v, want %v", watch, tt.want) + t.Errorf("Watch got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/contrib/registry/nacos/util.go b/contrib/registry/nacos/util.go new file mode 100644 index 0000000..52a00a4 --- /dev/null +++ b/contrib/registry/nacos/util.go @@ -0,0 +1,17 @@ +package nacos + +import ( + "strconv" + + "github.com/nacos-group/nacos-sdk-go/v2/model" +) + +// instanceKey generates a unique key for a Nacos instance. +// It uses InstanceId if available, otherwise falls back to a combination of IP, Port, ClusterName, and ServiceName. +func instanceKey(in *model.Instance) string { + id := in.InstanceId + if id == "" { + id = in.Ip + "#" + strconv.Itoa(int(in.Port)) + "#" + in.ClusterName + "#" + in.ServiceName + } + return id +} diff --git a/contrib/registry/nacos/watcher.go b/contrib/registry/nacos/watcher.go new file mode 100644 index 0000000..80e0424 --- /dev/null +++ b/contrib/registry/nacos/watcher.go @@ -0,0 +1,106 @@ +package nacos + +import ( + "context" + "net" + "strconv" + + "github.com/nacos-group/nacos-sdk-go/v2/clients/naming_client" + "github.com/nacos-group/nacos-sdk-go/v2/model" + "github.com/nacos-group/nacos-sdk-go/v2/vo" + + "github.com/go-kratos/kratos/v3/registry" +) + +var _ registry.Watcher = (*watcher)(nil) + +type watcher struct { + serviceName string + clusters []string + groupName string + ctx context.Context + cancel context.CancelFunc + watchChan chan struct{} + cli naming_client.INamingClient + kind string + subscribeParam *vo.SubscribeParam +} + +func newWatcher(ctx context.Context, cli naming_client.INamingClient, serviceName, groupName, kind string, clusters []string) (*watcher, error) { + w := &watcher{ + serviceName: serviceName, + clusters: clusters, + groupName: groupName, + cli: cli, + kind: kind, + watchChan: make(chan struct{}, 1), + } + w.ctx, w.cancel = context.WithCancel(ctx) + + w.subscribeParam = &vo.SubscribeParam{ + ServiceName: serviceName, + Clusters: clusters, + GroupName: groupName, + SubscribeCallback: func([]model.Instance, error) { + select { + case w.watchChan <- struct{}{}: + default: + } + }, + } + e := w.cli.Subscribe(w.subscribeParam) + select { + case w.watchChan <- struct{}{}: + default: + } + return w, e +} + +func (w *watcher) Next() ([]*registry.ServiceInstance, error) { + select { + case <-w.ctx.Done(): + return nil, w.ctx.Err() + default: + } + select { + case <-w.ctx.Done(): + return nil, w.ctx.Err() + case <-w.watchChan: + } + select { + case <-w.ctx.Done(): + return nil, w.ctx.Err() + default: + } + res, err := w.cli.GetService(vo.GetServiceParam{ + ServiceName: w.serviceName, + GroupName: w.groupName, + Clusters: w.clusters, + }) + if err != nil { + return nil, err + } + items := make([]*registry.ServiceInstance, 0, len(res.Hosts)) + for _, in := range res.Hosts { + kind := w.kind + if k, ok := in.Metadata[kindKey]; ok { + kind = k + } + // use ip#port#cluster#service as instance id if instance id is empty + id := instanceKey(&in) + items = append(items, ®istry.ServiceInstance{ + ID: id, + Name: in.ServiceName, + Version: in.Metadata[versionKey], + Metadata: in.Metadata, + Endpoints: []string{kind + "://" + net.JoinHostPort(in.Ip, strconv.Itoa(int(in.Port)))}, + }) + } + return items, nil +} + +func (w *watcher) Stop() error { + err := w.cli.Unsubscribe(w.subscribeParam) + w.cancel() + return err +} diff --git a/contrib/registry/polaris/go.mod b/contrib/registry/polaris/go.mod new file mode 100644 index 0000000..6fd489c --- /dev/null +++ b/contrib/registry/polaris/go.mod @@ -0,0 +1,43 @@ +module github.com/go-kratos/kratos/contrib/registry/polaris/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/polarismesh/polaris-go v1.7.0 +) + +require ( + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/natefinch/lumberjack v2.0.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/polarismesh/specification v1.8.0 // 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/procfs v0.20.1 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/polaris/go.sum b/contrib/registry/polaris/go.sum new file mode 100644 index 0000000..4330fcb --- /dev/null +++ b/contrib/registry/polaris/go.sum @@ -0,0 +1,1010 @@ +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 v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +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/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +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/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +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/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +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= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +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.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +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/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= +github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg= +github.com/gonum/integrate v0.0.0-20181209220457-a422b5c0fdf2/go.mod h1:pDgmNM6seYpwvPos3q+zxlXMsbve6mOIPucUnUOrI7Y= +github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks= +github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A= +github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw= +github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b/go.mod h1:Z4GIJBJO3Wa4gD4vbwQxXXZ+WHmW6E9ixmNrwvs0iZs= +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/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/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/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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/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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polarismesh/polaris-go v1.7.0 h1:iiaKcYWXrRWIOqoBBgAQVswhPxwOkIjI6P6nMVh+d14= +github.com/polarismesh/polaris-go v1.7.0/go.mod h1:up/F6sUGOXzd9pMDkkxtPFrMq3lmdz0U8BvEtvWLKCA= +github.com/polarismesh/specification v1.7.0/go.mod h1:rDvMMtl5qebPmqiBLNa5Ps0XtwkP31ZLirbH4kXA0YU= +github.com/polarismesh/specification v1.8.0 h1:k112R8eUZfdv1tVthWcf5AbE0SR/afyETz6Eii5OFw8= +github.com/polarismesh/specification v1.8.0/go.mod h1:Gb9OLXOELG4VwhxPh4DK6C0C3Z7FFYCmVkTW/d6+kvM= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/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.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-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-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/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-20181114220301-adae6a3d119a/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-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-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +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-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +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-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20200106162015-b016eb3dc98e/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/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-20210124154548-22da62e12c0c/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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +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-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-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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/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/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= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +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.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +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-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +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.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-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/contrib/registry/polaris/registry.go b/contrib/registry/polaris/registry.go new file mode 100644 index 0000000..47b874f --- /dev/null +++ b/contrib/registry/polaris/registry.go @@ -0,0 +1,434 @@ +package polaris + +import ( + "context" + "net" + "net/url" + "strconv" + "strings" + "time" + + "github.com/go-kratos/kratos/v3/log" + + "github.com/go-kratos/kratos/v3/registry" + + "github.com/polarismesh/polaris-go/api" + "github.com/polarismesh/polaris-go/pkg/config" + "github.com/polarismesh/polaris-go/pkg/model" +) + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +// _instanceIDSeparator . Instance id Separator. +const _instanceIDSeparator = "-" + +type options struct { + // required, namespace in polaris + Namespace string + + // required, service access token + ServiceToken string + + // optional, protocol in polaris. Default value is nil, it means use protocol config in service + Protocol *string + + // service weight in polaris. Default value is 100, 0 <= weight <= 10000 + Weight int + + // service priority. Default value is 0. The smaller the value, the lower the priority + Priority int + + // To show service is healthy or not. Default value is True . + Healthy bool + + // Heartbeat enable .Not in polaris . Default value is True. + Heartbeat bool + + // To show service is isolate or not. Default value is False . + Isolate bool + + // TTL timeout. if node needs to use heartbeat to report,required. If not set,server will throw ErrorCode-400141 + TTL int + + // optional, Timeout for single query. Default value is global config + // Total is (1+RetryCount) * Timeout + Timeout time.Duration + + // optional, retry count. Default value is global config + RetryCount int +} + +// Option is polaris option. +type Option func(o *options) + +// Registry is polaris registry. +type Registry struct { + opt options + provider api.ProviderAPI + consumer api.ConsumerAPI +} + +// WithNamespace with Namespace option. +func WithNamespace(namespace string) Option { + return func(o *options) { o.Namespace = namespace } +} + +// WithServiceToken with ServiceToken option. +func WithServiceToken(serviceToken string) Option { + return func(o *options) { o.ServiceToken = serviceToken } +} + +// WithProtocol with Protocol option. +func WithProtocol(protocol string) Option { + return func(o *options) { o.Protocol = &protocol } +} + +// WithWeight with Weight option. +func WithWeight(weight int) Option { + return func(o *options) { o.Weight = weight } +} + +// WithHealthy with Healthy option. +func WithHealthy(healthy bool) Option { + return func(o *options) { o.Healthy = healthy } +} + +// WithIsolate with Isolate option. +func WithIsolate(isolate bool) Option { + return func(o *options) { o.Isolate = isolate } +} + +// WithTTL with TTL option. +func WithTTL(TTL int) Option { + return func(o *options) { o.TTL = TTL } +} + +// WithTimeout with Timeout option. +func WithTimeout(timeout time.Duration) Option { + return func(o *options) { o.Timeout = timeout } +} + +// WithRetryCount with RetryCount option. +func WithRetryCount(retryCount int) Option { + return func(o *options) { o.RetryCount = retryCount } +} + +// WithHeartbeat . with Heartbeat option. +func WithHeartbeat(heartbeat bool) Option { + return func(o *options) { o.Heartbeat = heartbeat } +} + +func NewRegistry(provider api.ProviderAPI, consumer api.ConsumerAPI, opts ...Option) (r *Registry) { + op := options{ + Namespace: "default", + ServiceToken: "", + Protocol: nil, + Weight: 0, + Priority: 0, + Healthy: true, + Heartbeat: true, + Isolate: false, + TTL: 0, + Timeout: 0, + RetryCount: 0, + } + for _, option := range opts { + option(&op) + } + return &Registry{ + opt: op, + provider: provider, + consumer: consumer, + } +} + +func NewRegistryWithConfig(conf config.Configuration, opts ...Option) (r *Registry) { + provider, err := api.NewProviderAPIByConfig(conf) + if err != nil { + panic(err) + } + consumer, err := api.NewConsumerAPIByConfig(conf) + if err != nil { + panic(err) + } + return NewRegistry(provider, consumer, opts...) +} + +// Register the registration. +func (r *Registry) Register(_ context.Context, serviceInstance *registry.ServiceInstance) error { + ids := make([]string, 0, len(serviceInstance.Endpoints)) + for _, endpoint := range serviceInstance.Endpoints { + // get url + u, err := url.Parse(endpoint) + if err != nil { + return err + } + + // get host and port + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return err + } + + // port to int + portNum, err := strconv.Atoi(port) + if err != nil { + return err + } + + // medata + var rmd map[string]string + if serviceInstance.Metadata == nil { + rmd = map[string]string{ + "kind": u.Scheme, + "version": serviceInstance.Version, + } + } else { + rmd = make(map[string]string, len(serviceInstance.Metadata)+2) + for k, v := range serviceInstance.Metadata { + rmd[k] = v + } + rmd["kind"] = u.Scheme + rmd["version"] = serviceInstance.Version + } + // Register + service, err := r.provider.Register( + &api.InstanceRegisterRequest{ + InstanceRegisterRequest: model.InstanceRegisterRequest{ + Service: serviceInstance.Name + u.Scheme, + ServiceToken: r.opt.ServiceToken, + Namespace: r.opt.Namespace, + Host: host, + Port: portNum, + Protocol: r.opt.Protocol, + Weight: &r.opt.Weight, + Priority: &r.opt.Priority, + Version: &serviceInstance.Version, + Metadata: rmd, + Healthy: &r.opt.Healthy, + Isolate: &r.opt.Isolate, + TTL: &r.opt.TTL, + Timeout: &r.opt.Timeout, + RetryCount: &r.opt.RetryCount, + }, + }) + if err != nil { + return err + } + instanceID := service.InstanceID + + if r.opt.Heartbeat { + // start heartbeat report + go func() { + ticker := time.NewTicker(time.Second * time.Duration(r.opt.TTL)) + defer ticker.Stop() + + for { + <-ticker.C + + err = r.provider.Heartbeat(&api.InstanceHeartbeatRequest{ + InstanceHeartbeatRequest: model.InstanceHeartbeatRequest{ + Service: serviceInstance.Name + u.Scheme, + Namespace: r.opt.Namespace, + Host: host, + Port: portNum, + ServiceToken: r.opt.ServiceToken, + InstanceID: instanceID, + Timeout: &r.opt.Timeout, + RetryCount: &r.opt.RetryCount, + }, + }) + if err != nil { + log.Error(err.Error()) + continue + } + } + }() + } + + ids = append(ids, instanceID) + } + // need to set InstanceID for Deregister + serviceInstance.ID = strings.Join(ids, _instanceIDSeparator) + return nil +} + +// Deregister the registration. +func (r *Registry) Deregister(_ context.Context, serviceInstance *registry.ServiceInstance) error { + split := strings.Split(serviceInstance.ID, _instanceIDSeparator) + for i, endpoint := range serviceInstance.Endpoints { + // get url + u, err := url.Parse(endpoint) + if err != nil { + return err + } + + // get host and port + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return err + } + + // port to int + portNum, err := strconv.Atoi(port) + if err != nil { + return err + } + // Deregister + err = r.provider.Deregister( + &api.InstanceDeRegisterRequest{ + InstanceDeRegisterRequest: model.InstanceDeRegisterRequest{ + Service: serviceInstance.Name + u.Scheme, + ServiceToken: r.opt.ServiceToken, + Namespace: r.opt.Namespace, + InstanceID: split[i], + Host: host, + Port: portNum, + Timeout: &r.opt.Timeout, + RetryCount: &r.opt.RetryCount, + }, + }, + ) + if err != nil { + return err + } + } + return nil +} + +// GetService return the service instances in memory according to the service name. +func (r *Registry) GetService(_ context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + // get all instances + instancesResponse, err := r.consumer.GetAllInstances(&api.GetAllInstancesRequest{ + GetAllInstancesRequest: model.GetAllInstancesRequest{ + Service: serviceName, + Namespace: r.opt.Namespace, + Timeout: &r.opt.Timeout, + RetryCount: &r.opt.RetryCount, + }, + }) + if err != nil { + return nil, err + } + + serviceInstances := instancesToServiceInstances(instancesResponse.GetInstances()) + + return serviceInstances, nil +} + +// Watch creates a watcher according to the service name. +func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + return newWatcher(ctx, r.opt.Namespace, serviceName, r.consumer) +} + +type Watcher struct { + ServiceName string + Namespace string + Ctx context.Context + Cancel context.CancelFunc + Channel <-chan *model.InstancesResponse + ServiceInstances []*registry.ServiceInstance + watchResponse *model.WatchAllInstancesResponse + first bool +} + +type instancesListener struct { + events chan *model.InstancesResponse +} + +func (l instancesListener) OnInstancesUpdate(resp *model.InstancesResponse) { + select { + case l.events <- resp: + default: + select { + case <-l.events: + default: + } + select { + case l.events <- resp: + default: + } + } +} + +func newWatcher(ctx context.Context, namespace string, serviceName string, consumer api.ConsumerAPI) (*Watcher, error) { + events := make(chan *model.InstancesResponse, 1) + watchResponse, err := consumer.WatchAllInstances(&api.WatchAllInstancesRequest{ + WatchAllInstancesRequest: model.WatchAllInstancesRequest{ + ServiceKey: model.ServiceKey{ + Namespace: namespace, + Service: serviceName, + }, + WatchMode: model.WatchModeNotify, + InstancesListener: instancesListener{events: events}, + }, + }) + if err != nil { + return nil, err + } + + w := &Watcher{ + Namespace: namespace, + ServiceName: serviceName, + first: true, + Channel: events, + ServiceInstances: instancesToServiceInstances(watchResponse.InstancesResponse().GetInstances()), + watchResponse: watchResponse, + } + w.Ctx, w.Cancel = context.WithCancel(ctx) + return w, nil +} + +// Next returns services in the following two cases: +// 1.the first time to watch and the service instance list is not empty. +// 2.any service instance changes found. +// if the above two conditions are not met, it will block until context deadline exceeded or canceled +func (w *Watcher) Next() ([]*registry.ServiceInstance, error) { + if w.first { + w.first = false + return w.ServiceInstances, nil + } + select { + case <-w.Ctx.Done(): + return nil, w.Ctx.Err() + case instances := <-w.Channel: + w.ServiceInstances = instancesToServiceInstances(instances.GetInstances()) + return w.ServiceInstances, nil + } +} + +// Stop close the watcher. +func (w *Watcher) Stop() error { + w.Cancel() + w.watchResponse.CancelWatch() + return nil +} + +func instancesToServiceInstances(instances []model.Instance) []*registry.ServiceInstance { + serviceInstances := make([]*registry.ServiceInstance, 0, len(instances)) + for _, instance := range instances { + if instance.IsHealthy() { + serviceInstances = append(serviceInstances, instanceToServiceInstance(instance)) + } + } + return serviceInstances +} + +func instanceToServiceInstance(instance model.Instance) *registry.ServiceInstance { + metadata := instance.GetMetadata() + // Usually, it won't fail in kratos if register correctly + kind := "" + if k, ok := metadata["kind"]; ok { + kind = k + } + return ®istry.ServiceInstance{ + ID: instance.GetId(), + Name: instance.GetService(), + Version: metadata["version"], + Metadata: metadata, + Endpoints: []string{kind + "://" + net.JoinHostPort(instance.GetHost(), strconv.Itoa(int(instance.GetPort())))}, + } +} diff --git a/contrib/registry/servicecomb/README.md b/contrib/registry/servicecomb/README.md new file mode 100644 index 0000000..c35686b --- /dev/null +++ b/contrib/registry/servicecomb/README.md @@ -0,0 +1,67 @@ +# Servicecomb Registry + +## example + +### server +```go +package main + +import ( + "log" + + "github.com/go-chassis/sc-client" + "github.com/go-kratos/kratos/contrib/registry/servicecomb/v3" + "github.com/go-kratos/kratos/v3" +) + +func main() { + c, err := sc.NewClient(sc.Options{ + Endpoints: []string{"127.0.0.1:30100"}, + }) + if err != nil { + log.Panic(err) + } + r := servicecomb.NewRegistry(c) + app := kratos.New( + kratos.Name("helloServicecomb"), + kratos.Registrar(r), + ) + if err := app.Run(); err != nil { + log.Fatal(err) + } +} +``` + +### client +```go +package main + +import ( + "context" + "log" + + "github.com/go-chassis/sc-client" + "github.com/go-kratos/kratos/contrib/registry/servicecomb/v3" + "github.com/go-kratos/kratos/v3/transport/grpc" +) + +func main() { + c, err := sc.NewClient(sc.Options{ + Endpoints: []string{"127.0.0.1:30100"}, + }) + if err != nil { + log.Panic(err) + } + r := servicecomb.NewRegistry(c) + ctx := context.Background() + conn, err := grpc.NewClient( + ctx, + grpc.WithEndpoint("discovery:///helloServicecomb"), + grpc.WithDiscovery(r), + ) + if err != nil { + return + } + defer conn.Close() +} +``` diff --git a/contrib/registry/servicecomb/go.mod b/contrib/registry/servicecomb/go.mod new file mode 100644 index 0000000..34993d5 --- /dev/null +++ b/contrib/registry/servicecomb/go.mod @@ -0,0 +1,25 @@ +module github.com/go-kratos/kratos/contrib/registry/servicecomb/v3 + +go 1.25.0 + +require ( + github.com/go-chassis/cari v0.9.0 + github.com/go-chassis/sc-client v0.7.0 + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/gofrs/uuid v4.4.0+incompatible +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/go-chassis/foundation v0.4.0 // indirect + github.com/go-chassis/openlog v1.1.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/karlseguin/ccache/v2 v2.0.8 // indirect + github.com/karlseguin/expect v1.0.8 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/stretchr/testify v1.11.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/servicecomb/go.sum b/contrib/registry/servicecomb/go.sum new file mode 100644 index 0000000..93b4085 --- /dev/null +++ b/contrib/registry/servicecomb/go.sum @@ -0,0 +1,83 @@ +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/go-chassis/cari v0.5.1-0.20210823023004-74041d1363c4/go.mod h1:av/19fqwEP4eOC8unL/z67AAbFDwXUCko6SKa4Avrd8= +github.com/go-chassis/cari v0.9.0 h1:skvo2PX8nLyu26CCg7qUMv7yP2DY73GrBW9M5tWj63c= +github.com/go-chassis/cari v0.9.0/go.mod h1:vM13BN0TT505ZKqeJ+hUfzZvfn4nN0vgE6IpBOTWcTc= +github.com/go-chassis/foundation v0.3.0/go.mod h1:2PjwqpVwYEVaAldl5A58a08viH8p27pNeYaiE3ZxOBA= +github.com/go-chassis/foundation v0.4.0 h1:z0xETnSxF+vRXWjoIhOdzt6rywjZ4sB++utEl4YgWEY= +github.com/go-chassis/foundation v0.4.0/go.mod h1:6NsIUaHghTFRGfCBcZN011zl196F6OR5QvD9N+P4oWU= +github.com/go-chassis/openlog v1.1.2/go.mod h1:+eYCADVxWyJkwsFMUBrMxyQlNqW+UUsCxvR2LrYZUaA= +github.com/go-chassis/openlog v1.1.3 h1:XqIOvZ8YPJ9o9lLtLBskQNNWolK5kC6a4Sv7r4s9sZ4= +github.com/go-chassis/openlog v1.1.3/go.mod h1:+eYCADVxWyJkwsFMUBrMxyQlNqW+UUsCxvR2LrYZUaA= +github.com/go-chassis/sc-client v0.7.0 h1:c2LSdbFMuZ3RcbDu7So//kFCzjDCkzdQ0CNKhm8Dy7I= +github.com/go-chassis/sc-client v0.7.0/go.mod h1:DmclCLMhyNpYN42ae0f5AgiF4lTrpG6NyJJgmyAgC+E= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/karlseguin/ccache/v2 v2.0.8 h1:lT38cE//uyf6KcFok0rlgXtGFBWxkI6h/qg4tbFyDnA= +github.com/karlseguin/ccache/v2 v2.0.8/go.mod h1:2BDThcfQMf/c0jnZowt16eW405XIqZPavt+HoYEtcxQ= +github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003/go.mod h1:zNBxMY8P21owkeogJELCLeHIt+voOSduHYTFUbwRAV8= +github.com/karlseguin/expect v1.0.8 h1:Bb0H6IgBWQpadY25UDNkYPDB9ITqK1xnSoZfAq362fw= +github.com/karlseguin/expect v1.0.8/go.mod h1:lXdI8iGiQhmzpnnmU/EGA60vqKs8NbRNFnhhrJGoD5g= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +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/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/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/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= +github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= diff --git a/contrib/registry/servicecomb/registry.go b/contrib/registry/servicecomb/registry.go new file mode 100644 index 0000000..234cc9e --- /dev/null +++ b/contrib/registry/servicecomb/registry.go @@ -0,0 +1,175 @@ +package servicecomb + +import ( + "context" + "encoding/json" + "os" + "time" + + "github.com/go-chassis/cari/discovery" + "github.com/go-chassis/cari/pkg/errsvc" + "github.com/go-chassis/sc-client" + "github.com/gofrs/uuid" + + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/registry" +) + +func init() { + appID = os.Getenv(appIDVar) + if appID == "" { + appID = "default" + } + env = os.Getenv(envVar) +} + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +var ( + curServiceID string + appID string + env string +) + +const ( + appIDKey = "appId" + envKey = "environment" + envVar = "CAS_ENVIRONMENT_ID" + appIDVar = "CAS_APPLICATION_NAME" + frameWorkName = "kratos" + frameWorkVersion = "v2" +) + +type RegistryClient interface { + GetMicroServiceID(appID, microServiceName, version, env string, opts ...sc.CallOption) (string, error) + FindMicroServiceInstances(consumerID, appID, microServiceName, versionRule string, opts ...sc.CallOption) ([]*discovery.MicroServiceInstance, error) + RegisterService(microService *discovery.MicroService) (string, error) + RegisterMicroServiceInstance(microServiceInstance *discovery.MicroServiceInstance) (string, error) + Heartbeat(microServiceID, microServiceInstanceID string) (bool, error) + UnregisterMicroServiceInstance(microServiceID, microServiceInstanceID string) (bool, error) + WatchMicroService(microServiceID string, callback func(*sc.MicroServiceInstanceChangedEvent)) error +} + +// Registry is servicecomb registry. +type Registry struct { + cli RegistryClient +} + +func NewRegistry(client RegistryClient) *Registry { + r := &Registry{ + cli: client, + } + return r +} + +func (r *Registry) GetService(_ context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + instances, err := r.cli.FindMicroServiceInstances("", appID, serviceName, "") + if err != nil { + return nil, err + } + svcInstances := make([]*registry.ServiceInstance, 0, len(instances)) + for _, instance := range instances { + svcInstances = append(svcInstances, ®istry.ServiceInstance{ + ID: instance.InstanceId, + Name: serviceName, + Metadata: instance.Properties, + Endpoints: instance.Endpoints, + Version: instance.ServiceId, + }) + } + return svcInstances, nil +} + +func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + return newWatcher(ctx, r.cli, serviceName) +} + +func (r *Registry) Register(_ context.Context, svcIns *registry.ServiceInstance) error { + fw := &discovery.FrameWork{ + Name: frameWorkName, + Version: frameWorkVersion, + } + ms := &discovery.MicroService{ + ServiceName: svcIns.Name, + AppId: appID, + Version: svcIns.Version, + Environment: env, + Framework: fw, + } + // attempt to register the microservice + sid, err := r.cli.RegisterService(ms) + // if it fails, it may indicate that the service is already registered + if err != nil { + registryException, ok := err.(*sc.RegistryException) + if !ok { + return err + } + var svcErr errsvc.Error + parseErr := json.Unmarshal([]byte(registryException.Message), &svcErr) + if parseErr != nil { + return parseErr + } + // if the error code is not specific to the service already existing, return the current error + if svcErr.Code != discovery.ErrServiceAlreadyExists { + return err + } + sid, err = r.cli.GetMicroServiceID(appID, ms.ServiceName, ms.Version, ms.Environment) + if err != nil { + return err + } + } else { + // save the service ID for the newly registered service + curServiceID = sid + } + if svcIns.ID == "" { + var id uuid.UUID + id, err = uuid.NewV4() + if err != nil { + return err + } + svcIns.ID = id.String() + } + props := map[string]string{ + appIDKey: appID, + envKey: env, + } + _, err = r.cli.RegisterMicroServiceInstance(&discovery.MicroServiceInstance{ + InstanceId: svcIns.ID, + ServiceId: sid, + Endpoints: svcIns.Endpoints, + HostName: svcIns.ID, + Properties: props, + Version: svcIns.Version, + }) + if err != nil { + return err + } + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + <-ticker.C + _, err = r.cli.Heartbeat(sid, svcIns.ID) + if err != nil { + log.Error("failed to send heartbeat", "error", err) + continue + } + } + }() + return nil +} + +func (r *Registry) Deregister(_ context.Context, svcIns *registry.ServiceInstance) error { + sid, err := r.cli.GetMicroServiceID(appID, svcIns.Name, svcIns.Version, env) + if err != nil { + return err + } + _, err = r.cli.UnregisterMicroServiceInstance(sid, svcIns.ID) + if err != nil { + return err + } + return nil +} diff --git a/contrib/registry/servicecomb/registry_test.go b/contrib/registry/servicecomb/registry_test.go new file mode 100644 index 0000000..0e6cee5 --- /dev/null +++ b/contrib/registry/servicecomb/registry_test.go @@ -0,0 +1,158 @@ +package servicecomb + +import ( + "context" + "testing" + + pb "github.com/go-chassis/cari/discovery" + "github.com/go-chassis/sc-client" + "github.com/gofrs/uuid" + + "github.com/go-kratos/kratos/v3/registry" +) + +var r *Registry + +func init() { + r = NewRegistry(&mockClient{}) +} + +type mockClient struct{} + +func (receiver *mockClient) WatchMicroService(_ string, _ func(*sc.MicroServiceInstanceChangedEvent)) error { + return nil +} + +// nolint +func (receiver *mockClient) FindMicroServiceInstances(_, + _, microServiceName, _ string, _ ...sc.CallOption, +) ([]*pb.MicroServiceInstance, error) { + if microServiceName == "KratosServicecomb" { + return []*pb.MicroServiceInstance{{}}, nil + } + return nil, nil +} + +func (receiver *mockClient) RegisterService(_ *pb.MicroService) (string, error) { + return "", nil +} + +func (receiver *mockClient) RegisterMicroServiceInstance(_ *pb.MicroServiceInstance) (string, error) { + return "", nil +} + +func (receiver *mockClient) Heartbeat(_, _ string) (bool, error) { + return true, nil +} + +func (receiver *mockClient) UnregisterMicroServiceInstance(_, _ string) (bool, error) { + return true, nil +} + +func (receiver *mockClient) GetMicroServiceID(_, _, _, _ string, _ ...sc.CallOption) (string, error) { + return "", nil +} + +func TestRegistry(t *testing.T) { + instanceID, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + svc := ®istry.ServiceInstance{ + Name: "KratosServicecomb", + Version: "0.0.1", + Metadata: map[string]string{"app": "kratos"}, + Endpoints: []string{"tcp://127.0.0.1:9000?isSecure=false"}, + ID: instanceID.String(), + } + ctx := context.TODO() + t.Run("Register test, expected: success.", func(t *testing.T) { + err = r.Register(ctx, svc) + if err != nil { + t.Error(err) + } + }) + t.Run("GetService test, expected: success.", func(t *testing.T) { + var insts []*registry.ServiceInstance + insts, err = r.GetService(ctx, svc.Name) + if err != nil { + t.Fatal(err) + } + if len(insts) <= 0 { + t.Errorf("inst len less than 0") + } + }) + t.Run("Deregister test, expected: success.", func(t *testing.T) { + svc.ID = instanceID.String() + err = r.Deregister(ctx, svc) + if err != nil { + t.Error(err) + } + }) +} + +func TestWatcher(t *testing.T) { + instanceID1, err := uuid.NewV4() + if err != nil { + t.Fatal(err) + } + svc1 := ®istry.ServiceInstance{ + Name: "WatcherTest", + Version: "0.0.1", + Metadata: map[string]string{"app": "kratos"}, + Endpoints: []string{"tcp://127.0.0.1:9000?isSecure=false"}, + ID: instanceID1.String(), + } + ctx := context.TODO() + err = r.Register(ctx, svc1) + if err != nil { + t.Fatal(err) + } + w, err := r.Watch(ctx, "WatcherTest") + if err != nil { + t.Fatal(err) + } + if w == nil { + t.Fatal("w is nil") + } + sbWatcher := w.(*Watcher) + t.Run("Watch register event, expected: success", func(t *testing.T) { + go sbWatcher.Put(svc1) + var instances []*registry.ServiceInstance + instances, err = w.Next() + if err != nil { + t.Fatal(err) + } + if len(instances) == 0 { + t.Errorf("instances is empty") + } + if instanceID1.String() != instances[0].ID { + t.Errorf("expected %v, got %v", instanceID1.String(), instances[0].ID) + } + }) + t.Run("Watch deregister event, expected: success", func(t *testing.T) { + // Deregister instance1. + err = r.Deregister(ctx, svc1) + if err != nil { + t.Fatal(err) + } + go sbWatcher.Put(svc1) + var instances []*registry.ServiceInstance + instances, err = w.Next() + if err != nil { + t.Fatal(err) + } + if len(instances) == 0 { + t.Errorf("instances is empty") + } + if instanceID1.String() != instances[0].ID { + t.Errorf("expected %v, got %v", instanceID1.String(), instances[0].ID) + } + }) + t.Run("Stop test, expected: success", func(t *testing.T) { + err = w.Stop() + if err != nil { + t.Error(err) + } + }) +} diff --git a/contrib/registry/servicecomb/watcher.go b/contrib/registry/servicecomb/watcher.go new file mode 100644 index 0000000..8361a46 --- /dev/null +++ b/contrib/registry/servicecomb/watcher.go @@ -0,0 +1,64 @@ +package servicecomb + +import ( + "context" + + "github.com/go-chassis/sc-client" + + "github.com/go-kratos/kratos/v3/registry" +) + +var _ registry.Watcher = (*Watcher)(nil) + +type Watcher struct { + cli RegistryClient + ch chan *registry.ServiceInstance +} + +func newWatcher(_ context.Context, cli RegistryClient, serviceName string) (*Watcher, error) { + // establish dependency relationship between the current service and the target service for discovery + _, err := cli.FindMicroServiceInstances(curServiceID, appID, serviceName, "") + if err != nil { + return nil, err + } + w := &Watcher{ + cli: cli, + ch: make(chan *registry.ServiceInstance), + } + go func() { + watchErr := w.cli.WatchMicroService(curServiceID, func(event *sc.MicroServiceInstanceChangedEvent) { + if event.Key.ServiceName != serviceName { + return + } + svcIns := ®istry.ServiceInstance{ + ID: event.Instance.InstanceId, + Name: event.Key.ServiceName, + Version: event.Key.Version, + Metadata: event.Instance.Properties, + Endpoints: event.Instance.Endpoints, + } + w.Put(svcIns) + }) + if watchErr != nil { + return + } + }() + return w, nil +} + +// Put only for UT +func (w *Watcher) Put(svcIns *registry.ServiceInstance) { + w.ch <- svcIns +} + +func (w *Watcher) Next() ([]*registry.ServiceInstance, error) { + svcInstances := make([]*registry.ServiceInstance, 0, 1) + svcIns := <-w.ch + svcInstances = append(svcInstances, svcIns) + return svcInstances, nil +} + +func (w *Watcher) Stop() error { + close(w.ch) + return nil +} diff --git a/contrib/registry/zookeeper/go.mod b/contrib/registry/zookeeper/go.mod new file mode 100644 index 0000000..4d277de --- /dev/null +++ b/contrib/registry/zookeeper/go.mod @@ -0,0 +1,11 @@ +module github.com/go-kratos/kratos/contrib/registry/zookeeper/v3 + +go 1.25.0 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/go-zookeeper/zk v1.0.4 + golang.org/x/sync v0.20.0 +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/registry/zookeeper/go.sum b/contrib/registry/zookeeper/go.sum new file mode 100644 index 0000000..689cd57 --- /dev/null +++ b/contrib/registry/zookeeper/go.sum @@ -0,0 +1,4 @@ +github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I= +github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= diff --git a/contrib/registry/zookeeper/register.go b/contrib/registry/zookeeper/register.go new file mode 100644 index 0000000..b248eac --- /dev/null +++ b/contrib/registry/zookeeper/register.go @@ -0,0 +1,181 @@ +package zookeeper + +import ( + "context" + "errors" + "path" + "time" + + "github.com/go-zookeeper/zk" + "golang.org/x/sync/singleflight" + + "github.com/go-kratos/kratos/v3/registry" +) + +var ( + _ registry.Registrar = (*Registry)(nil) + _ registry.Discovery = (*Registry)(nil) +) + +// Option is etcd registry option. +type Option func(o *options) + +type options struct { + namespace string + user string + password string +} + +// WithRootPath with registry root path. +func WithRootPath(path string) Option { + return func(o *options) { o.namespace = path } +} + +// WithDigestACL with registry password. +func WithDigestACL(user string, password string) Option { + return func(o *options) { + o.user = user + o.password = password + } +} + +// Registry is consul registry +type Registry struct { + opts *options + conn *zk.Conn + + group singleflight.Group +} + +func New(conn *zk.Conn, opts ...Option) *Registry { + options := &options{ + namespace: "/microservices", + } + for _, o := range opts { + o(options) + } + return &Registry{ + opts: options, + conn: conn, + } +} + +func (r *Registry) Register(_ context.Context, service *registry.ServiceInstance) error { + var ( + data []byte + err error + ) + if err = r.ensureName(r.opts.namespace, []byte(""), 0); err != nil { + return err + } + serviceNamePath := path.Join(r.opts.namespace, service.Name) + if err = r.ensureName(serviceNamePath, []byte(""), 0); err != nil { + return err + } + if data, err = marshal(service); err != nil { + return err + } + servicePath := path.Join(serviceNamePath, service.ID) + if err = r.ensureName(servicePath, data, zk.FlagEphemeral); err != nil { + return err + } + go r.reRegister(servicePath, data) + return nil +} + +// Deregister registry service to zookeeper. +func (r *Registry) Deregister(ctx context.Context, service *registry.ServiceInstance) error { + ch := make(chan error, 1) + servicePath := path.Join(r.opts.namespace, service.Name, service.ID) + go func() { + err := r.conn.Delete(servicePath, -1) + ch <- err + }() + var err error + select { + case <-ctx.Done(): + err = ctx.Err() + case err = <-ch: + } + return err +} + +// GetService get services from zookeeper +func (r *Registry) GetService(_ context.Context, serviceName string) ([]*registry.ServiceInstance, error) { + instances, err, _ := r.group.Do(serviceName, func() (any, error) { + serviceNamePath := path.Join(r.opts.namespace, serviceName) + servicesID, _, err := r.conn.Children(serviceNamePath) + if err != nil { + return nil, err + } + items := make([]*registry.ServiceInstance, 0, len(servicesID)) + for _, service := range servicesID { + servicePath := path.Join(serviceNamePath, service) + serviceInstanceByte, _, err := r.conn.Get(servicePath) + if err != nil { + return nil, err + } + item, err := unmarshal(serviceInstanceByte) + if err != nil { + return nil, err + } + items = append(items, item) + } + return items, nil + }) + if err != nil { + return nil, err + } + return instances.([]*registry.ServiceInstance), nil +} + +func (r *Registry) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + prefix := path.Join(r.opts.namespace, serviceName) + return newWatcher(ctx, prefix, serviceName, r.conn) +} + +// ensureName ensure node exists, if not exist, create and set data +func (r *Registry) ensureName(path string, data []byte, flags int32) error { + exists, stat, err := r.conn.Exists(path) + if err != nil { + return err + } + // ephemeral nodes handling after restart + // fixes a race condition if the server crashes without using CreateProtectedEphemeralSequential() + if flags&zk.FlagEphemeral == zk.FlagEphemeral { + err = r.conn.Delete(path, stat.Version) + if err != nil && !errors.Is(err, zk.ErrNoNode) { + return err + } + exists = false + } + if !exists { + if len(r.opts.user) > 0 && len(r.opts.password) > 0 { + _, err = r.conn.Create(path, data, flags, zk.DigestACL(zk.PermAll, r.opts.user, r.opts.password)) + } else { + _, err = r.conn.Create(path, data, flags, zk.WorldACL(zk.PermAll)) + } + if err != nil { + return err + } + } + return nil +} + +// reRegister re-register data node info when bad connection recovered +func (r *Registry) reRegister(path string, data []byte) { + sessionID := r.conn.SessionID() + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for range ticker.C { + cur := r.conn.SessionID() + // sessionID changed + if cur > 0 && sessionID != cur { + // re-ensureName + if err := r.ensureName(path, data, zk.FlagEphemeral); err != nil { + return + } + sessionID = cur + } + } +} diff --git a/contrib/registry/zookeeper/register_test.go b/contrib/registry/zookeeper/register_test.go new file mode 100644 index 0000000..c11822c --- /dev/null +++ b/contrib/registry/zookeeper/register_test.go @@ -0,0 +1,420 @@ +package zookeeper + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/go-zookeeper/zk" + + "github.com/go-kratos/kratos/v3/registry" +) + +func TestRegistry_GetService(t *testing.T) { + conn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, time.Second*15) + if err != nil { + t.Fatal(err) + return + } + r := New(conn) + + svrHello := ®istry.ServiceInstance{ + ID: "1", + Name: "hello", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + } + + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + serviceName string + } + tests := []struct { + name string + fields fields + args args + want []*registry.ServiceInstance + wantErr bool + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + }{ + { + name: "normal", + preFunc: func(t *testing.T) { + err = r.Register(context.Background(), svrHello) + if err != nil { + t.Error(err) + } + }, + deferFunc: func(t *testing.T) { + err = r.Deregister(context.Background(), svrHello) + if err != nil { + t.Error(err) + } + }, + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + serviceName: svrHello.Name, + }, + want: []*registry.ServiceInstance{svrHello}, + wantErr: false, + }, + { + name: "can't get any", + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + serviceName: "helloxxx", + }, + want: nil, + wantErr: true, + }, + { + name: "conn close", + preFunc: func(*testing.T) { + conn.Close() + }, + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + serviceName: "hello", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc(t) + } + if tt.deferFunc != nil { + defer tt.deferFunc(t) + } + r := tt.fields.registry + got, err := r.GetService(tt.args.ctx, tt.args.serviceName) + if (err != nil) != tt.wantErr { + t.Errorf("GetService() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetService() got = %v", got) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetService() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRegistry_Register(t *testing.T) { + conn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, time.Second*15) + if err != nil { + t.Fatal(err) + return + } + r := New(conn) + + svrHello := ®istry.ServiceInstance{ + ID: "1", + Name: "hello", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + } + + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + service *registry.ServiceInstance + } + tests := []struct { + name string + fields fields + args args + wantErr bool + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + }{ + { + name: "normal", + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + service: svrHello, + }, + wantErr: false, + }, + { + name: "invalid path", + fields: fields{ + registry: New(conn, WithRootPath("invalid")), + }, + args: args{ + ctx: context.Background(), + service: ®istry.ServiceInstance{ + ID: "1", + Name: "hello1", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + }, + }, + wantErr: true, + }, + { + name: "auth", + preFunc: func(t *testing.T) { + err = conn.AddAuth("digest", []byte("test:test")) + if err != nil { + t.Error(err) + } + }, + fields: fields{ + registry: New(conn, WithRootPath("/tt1"), WithDigestACL("test", "test")), + }, + args: args{ + ctx: context.Background(), + service: ®istry.ServiceInstance{ + ID: "1", + Name: "hello2", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc(t) + } + if tt.deferFunc != nil { + defer tt.deferFunc(t) + } + r := tt.fields.registry + if err := r.Register(tt.args.ctx, tt.args.service); (err != nil) != tt.wantErr { + t.Errorf("Register() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestRegistry_Deregister(t *testing.T) { + conn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, time.Second*15) + if err != nil { + t.Fatal(err) + return + } + r := New(conn) + + svrHello := ®istry.ServiceInstance{ + ID: "1", + Name: "hello", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + } + + cancelCtx, cancel := context.WithCancel(context.Background()) + cancel() + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + service *registry.ServiceInstance + } + tests := []struct { + name string + fields fields + args args + wantErr bool + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + }{ + { + name: "normal", + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + service: svrHello, + }, + wantErr: false, + preFunc: func(t *testing.T) { + err = r.Register(context.Background(), svrHello) + if err != nil { + t.Error(err) + } + }, + }, + { + name: "with ctx cancel", + fields: fields{ + registry: r, + }, + args: args{ + ctx: cancelCtx, + service: svrHello, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.fields.registry + if err := r.Deregister(tt.args.ctx, tt.args.service); (err != nil) != tt.wantErr { + t.Errorf("Deregister() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestRegistry_Watch(t *testing.T) { + conn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, time.Second*15) + if err != nil { + t.Fatal(err) + return + } + closeConn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, time.Second*15) + if err != nil { + t.Fatal(err) + return + } + r := New(conn) + + svrHello := ®istry.ServiceInstance{ + ID: "1", + Name: "hello", + Version: "v1.0.0", + Endpoints: []string{"127.0.0.1:8080"}, + } + + cancelCtx, cancel := context.WithCancel(context.Background()) + type fields struct { + registry *Registry + } + type args struct { + ctx context.Context + serviceName string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + want []*registry.ServiceInstance + + preFunc func(t *testing.T) + deferFunc func(t *testing.T) + processFunc func(t *testing.T, w registry.Watcher) + }{ + { + name: "normal", + fields: fields{ + registry: r, + }, + args: args{ + ctx: context.Background(), + serviceName: svrHello.Name, + }, + wantErr: false, + want: []*registry.ServiceInstance{svrHello}, + deferFunc: func(t *testing.T) { + err = r.Deregister(context.Background(), svrHello) + if err != nil { + t.Error(err) + } + }, + processFunc: func(t *testing.T, _ registry.Watcher) { + err = r.Register(context.Background(), svrHello) + if err != nil { + t.Error(err) + } + }, + }, + { + name: "ctx cancel", + fields: fields{ + registry: r, + }, + args: args{ + ctx: cancelCtx, + serviceName: svrHello.Name, + }, + wantErr: true, + want: nil, + processFunc: func(*testing.T, registry.Watcher) { + cancel() + }, + }, + { + name: "disconnect", + fields: fields{ + registry: New(closeConn), + }, + args: args{ + ctx: context.Background(), + serviceName: svrHello.Name, + }, + wantErr: true, + want: nil, + processFunc: func(*testing.T, registry.Watcher) { + closeConn.Close() + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.preFunc != nil { + tt.preFunc(t) + } + if tt.deferFunc != nil { + defer tt.deferFunc(t) + } + r := tt.fields.registry + watcher, err := r.Watch(tt.args.ctx, tt.args.serviceName) + if err != nil { + t.Error(err) + return + } + defer func() { + err = watcher.Stop() + if err != nil { + t.Error(err) + } + }() + _, err = watcher.Next() + if err != nil { + t.Error(err) + return + } + + if tt.processFunc != nil { + tt.processFunc(t, watcher) + } + + want, err := watcher.Next() + if (err != nil) != tt.wantErr { + t.Errorf("Watch() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(want, tt.want) { + t.Errorf("Watch() watcher = %v, want %v", watcher, tt.want) + } + }) + } +} diff --git a/contrib/registry/zookeeper/service.go b/contrib/registry/zookeeper/service.go new file mode 100644 index 0000000..5e10256 --- /dev/null +++ b/contrib/registry/zookeeper/service.go @@ -0,0 +1,16 @@ +package zookeeper + +import ( + "encoding/json" + + "github.com/go-kratos/kratos/v3/registry" +) + +func marshal(si *registry.ServiceInstance) ([]byte, error) { + return json.Marshal(si) +} + +func unmarshal(data []byte) (si *registry.ServiceInstance, err error) { + err = json.Unmarshal(data, &si) + return +} diff --git a/contrib/registry/zookeeper/watcher.go b/contrib/registry/zookeeper/watcher.go new file mode 100644 index 0000000..cdfcf23 --- /dev/null +++ b/contrib/registry/zookeeper/watcher.go @@ -0,0 +1,111 @@ +package zookeeper + +import ( + "context" + "errors" + "path" + "sync/atomic" + + "github.com/go-zookeeper/zk" + + "github.com/go-kratos/kratos/v3/registry" +) + +var _ registry.Watcher = (*watcher)(nil) + +var ErrWatcherStopped = errors.New("watcher stopped") + +type watcher struct { + ctx context.Context + event chan zk.Event + conn *zk.Conn + cancel context.CancelFunc + + first atomic.Bool + // prefix for ZooKeeper paths or keys (used for filtering or identifying watched nodes) + prefix string + // the name of the service being watched in ZooKeeper + serviceName string +} + +func newWatcher(ctx context.Context, prefix, serviceName string, conn *zk.Conn) (*watcher, error) { + w := &watcher{conn: conn, event: make(chan zk.Event, 1), prefix: prefix, serviceName: serviceName} + w.ctx, w.cancel = context.WithCancel(ctx) + go w.watch(w.ctx) + return w, nil +} + +func (w *watcher) watch(ctx context.Context) { + for { + // since a single watch is only valid for one event, we need to loop to continue watching + _, _, ch, err := w.conn.ChildrenW(w.prefix) + if err != nil { + // If the target service node has not been created + if errors.Is(err, zk.ErrNoNode) { + // Add watcher for the node exists + _, _, ch, err = w.conn.ExistsW(w.prefix) + } + if err != nil { + w.event <- zk.Event{Err: err} + continue + } + } + select { + case <-ctx.Done(): + return + case ev := <-ch: + w.event <- ev + } + } +} + +func (w *watcher) Next() ([]*registry.ServiceInstance, error) { + // TODO: multiple calls to Next may lead to inconsistent service instance information + if w.first.CompareAndSwap(false, true) { + return w.getServices() + } + select { + case <-w.ctx.Done(): + return nil, w.ctx.Err() + case e := <-w.event: + if e.State == zk.StateDisconnected { + return nil, ErrWatcherStopped + } + if e.Err != nil { + return nil, e.Err + } + return w.getServices() + } +} + +func (w *watcher) Stop() error { + w.cancel() + return nil +} + +func (w *watcher) getServices() ([]*registry.ServiceInstance, error) { + servicesID, _, err := w.conn.Children(w.prefix) + if err != nil { + return nil, err + } + items := make([]*registry.ServiceInstance, 0, len(servicesID)) + for _, id := range servicesID { + servicePath := path.Join(w.prefix, id) + b, _, err := w.conn.Get(servicePath) + if err != nil { + return nil, err + } + item, err := unmarshal(b) + if err != nil { + return nil, err + } + + // if the service name of the retrieved instance does not match the watcher's service name, skip it + if item.Name != w.serviceName { + continue + } + + items = append(items, item) + } + return items, nil +} diff --git a/contrib/transport/mcp/README.md b/contrib/transport/mcp/README.md new file mode 100644 index 0000000..94e81fa --- /dev/null +++ b/contrib/transport/mcp/README.md @@ -0,0 +1,52 @@ +# MCP Transport + +This module implements the MCP server in Kratos based on mcp-go. + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/go-kratos/kratos/contrib/transport/mcp/v3) + +## Quick start +```go +import( + tm "github.com/go-kratos/kratos/contrib/transport/mcp/v3" + mcp "github.com/mark3labs/mcp-go/mcp" +) + +func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + name, ok := request.Params.Arguments["name"].(string) + if !ok { + return nil, errors.New("name must be a string") + } + return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil +} + +func Health(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health/ready" { + w.WriteHeader(http.StatusOK) + return + } + next.ServeHTTP(w, r) + }) +} + +func main() { + srv := tm.NewServer("kratos-mcp", "v1.0.0", tm.Address(":8000"), tm.Middleware(Health)) + tool := mcp.NewTool("hello_world", + mcp.WithDescription("Say hello to someone"), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name of the person to greet"), + ), + ) + // Add tool handler + srv.AddTool(tool, helloHandler) + // creates a kratos application + app := kratos.New( + kratos.Name("kratos-app"), + kratos.Server(srv), + ) + if err := app.Run(); err != nil { + panic(err) + } +} +``` diff --git a/contrib/transport/mcp/go.mod b/contrib/transport/mcp/go.mod new file mode 100644 index 0000000..d1b509d --- /dev/null +++ b/contrib/transport/mcp/go.mod @@ -0,0 +1,23 @@ +module github.com/go-kratos/kratos/contrib/transport/mcp/v3 + +go 1.25.5 + +require ( + github.com/go-kratos/kratos/v3 v3.0.0 + github.com/mark3labs/mcp-go v0.54.0 +) + +require ( + github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/go-playground/form/v4 v4.3.0 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/go-kratos/kratos/v3 => ../../../ diff --git a/contrib/transport/mcp/go.sum b/contrib/transport/mcp/go.sum new file mode 100644 index 0000000..97bab44 --- /dev/null +++ b/contrib/transport/mcp/go.sum @@ -0,0 +1,43 @@ +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/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +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/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= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mark3labs/mcp-go v0.54.0 h1:PZhQvd+5xrT43cUoiaKn/hDcvLUhcLc1twSEKYPTcTA= +github.com/mark3labs/mcp-go v0.54.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +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/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contrib/transport/mcp/server.go b/contrib/transport/mcp/server.go new file mode 100644 index 0000000..79e0873 --- /dev/null +++ b/contrib/transport/mcp/server.go @@ -0,0 +1,117 @@ +package mcp + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/go-kratos/kratos/v3/transport" + + "github.com/mark3labs/mcp-go/server" +) + +var ( + _ transport.Server = (*Server)(nil) + _ transport.Endpointer = (*Server)(nil) + _ http.Handler = (*Server)(nil) +) + +// MiddlewareFunc is a function that takes an http.Handler and returns an http.Handler. +type MiddlewareFunc func(http.Handler) http.Handler + +// ServerOption is an HTTP server option. +type ServerOption func(*Server) + +// Address with server address. +func Address(addr string) ServerOption { + return func(s *Server) { + s.address = addr + } +} + +// Endpoint with server address. +func Endpoint(endpoint *url.URL) ServerOption { + return func(s *Server) { + s.endpoint = endpoint + } +} + +// Middleware with server middleware. +func Middleware(m MiddlewareFunc) ServerOption { + return func(s *Server) { + s.middleware = m + } +} + +// SrvOptions with server options. +func SrvOptions(opts ...server.ServerOption) ServerOption { + return func(s *Server) { + s.srvOpts = append(s.srvOpts, opts...) + } +} + +// SSEOptions with server SSE options. +func SSEOptions(opts ...server.SSEOption) ServerOption { + return func(s *Server) { + s.sseOpts = append(s.sseOpts, opts...) + } +} + +// Server is a MCP server. +type Server struct { + *server.MCPServer + srv *http.Server + sse *server.SSEServer + middleware MiddlewareFunc + address string + endpoint *url.URL + srvOpts []server.ServerOption + sseOpts []server.SSEOption +} + +// NewServer creates a new MCP server. +func NewServer(name, version string, opts ...ServerOption) *Server { + srv := &Server{ + address: ":8000", + middleware: func(next http.Handler) http.Handler { return next }, + } + for _, o := range opts { + o(srv) + } + srv.MCPServer = server.NewMCPServer(name, version, srv.srvOpts...) + srv.srv = &http.Server{Addr: srv.address, Handler: srv.middleware(srv)} + srv.sse = server.NewSSEServer(srv.MCPServer, append(srv.sseOpts, server.WithHTTPServer(srv.srv))...) + return srv +} + +// ServeHTTP implements the http.Handler interface. +func (s *Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { + s.sse.ServeHTTP(res, req) +} + +// Endpoint return a real address to registry endpoint. +// examples: +// - http://127.0.0.1:8000 +func (s *Server) Endpoint() (*url.URL, error) { + if s.endpoint != nil { + return s.endpoint, nil + } + return url.Parse(fmt.Sprintf("http://%s", s.address)) +} + +// Start start the MCP server. +func (s *Server) Start(_ context.Context) error { + if err := s.srv.ListenAndServe(); err != nil { + if !errors.Is(err, http.ErrServerClosed) { + return err + } + } + return nil +} + +// Stop stop the MCP server. +func (s *Server) Stop(ctx context.Context) error { + return s.sse.Shutdown(ctx) +} diff --git a/contrib/transport/mcp/server_test.go b/contrib/transport/mcp/server_test.go new file mode 100644 index 0000000..4d8293d --- /dev/null +++ b/contrib/transport/mcp/server_test.go @@ -0,0 +1,23 @@ +package mcp + +import ( + "context" + "testing" + "time" +) + +func TestServer(t *testing.T) { + var ( + ctx = context.Background() + srv = NewServer("test", "v1.0.0", Address(":0")) + ) + go func() { + if err := srv.Start(ctx); err != nil { + panic(err) + } + }() + time.Sleep(time.Second) + if err := srv.Stop(ctx); err != nil { + t.Fatal(err) + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/design/kratos-v2.md b/docs/design/kratos-v2.md new file mode 100644 index 0000000..6aa6293 --- /dev/null +++ b/docs/design/kratos-v2.md @@ -0,0 +1,14 @@ +# Kratos v2 Kit Design + +MaoJian + +Last updated: December 25, 2020 + +## Abstract +kratos v1 基础库主要专注在各类功能的细节实现,比如 gRPC 的负载均衡,熔断器等一系列微服务需要的功能。 + +## Background + +## Proposal + +## Implementation diff --git a/docs/images/alipay.png b/docs/images/alipay.png new file mode 100644 index 0000000000000000000000000000000000000000..4497460fcda21843ca288032f3d2b4d24326ea3f GIT binary patch literal 42462 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
PSdQBUa5$M!8`ph}!!x|E{ioNa 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 literal 0 HcmV?d00001 diff --git a/docs/images/wechat.png b/docs/images/wechat.png new file mode 100644 index 0000000000000000000000000000000000000000..3b8e5ecede0e540f11d030eb876003210ac4a6a0 GIT binary patch literal 4268298 zcmeF42cR8Qz4h@4iXaFoHuNciPyOu3Q&D(|iVy67qGAE5M(IVxN^cT*G^r{rG*J)` zRFFU_B?%n@1V{pe011%Z+22_c)^*>xXXebDJLlYU?){C<-Bb7e@84cCVfH$E=F?wV zV}mVL(R5sG{zYy6%ga_-W#9i;WtEq#^0rl8T3bKlZ7cWJ7NaSwvdYT;Yg_pA|89H0 zA)^+&Yp1{e=X!5D@|l->>cby?%^|z*{$HQkefO80bH*93*ldmaU%JtEPS`lgV3k$l zl{PJh5Z+juzg?TJugy#lfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=-9teB zj(nasw8>hkyqH?-YrR;9>9v#z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< zp!*3tR;z+MthFjPms*fvEzb38^9i+?2?7v+00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKw$6^kRP;u3+$$TXIYEuwYB-w!Hb@nK>z{}fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2-G2PN^RR~`)xK=kU=eum)7PV)QOQz5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0z-#@{GyjuU_UjLMXl_OYBS%j8ajHTrVxMt1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009URXbnb%wmE71Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00IzzKz|9GQt)+veGaP?{O$f0&iN3400bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|hzNYUk15q)L+#vmMCY>z0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_0D-CqY}CN2>e*c@@TIl+v8oCt9|%AI0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U@1g6)PU)p0%D4+0Q?00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;-2u!zcRXg?# z1&-$s2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SL5;z#HXV)iUQDfvqA; zz7T)_1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Kn_B<~8W9e90_gE0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009WJh`<>w%9m^* z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AW(+Ds4@q0ECe6`0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*zqL|}T0@+DgcKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2$UhPu*|_63jqi~00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_ z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_ z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_ z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| 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�SI�SI�SI�SI&lIH_ae|w5NK_J@^V2quq@K(ZG32&9y zTY=A#@K*8TD7=;MR`Gj}v=juAi-3IRCGR^=uoY~@2gC=&2gC=&2gC=&2gC=&2gC<5 z)d!OMB`VSdf$T$oF?Obz&Rd}$>pymURNn zQINbKP&Ei->Nk|wE#Lp*1L6bX1L6bX1L6bX1L6bX1L6bX1IhD&s`)|{t%5-HMSw9` zreS!ipMLsD15;L3rq}gCteGd`x{z5|Sg3Be;RekgG3Lq`tUaC!9>jj4jd|ieV#PR0 zOG{(=OudXLlegM`|NSe;g&dT_2J%*vhxRxj6Dj(Kqka4KF&>P4K*~J(S5^C4-Fxr7 z+D_Ac;$o&8YhBsl58C4hvrb@9QIQ^R6E{;Q^A749i&#IZ)KQ5;VHEm^UrtVrs#mXG z%xCQWPQN+)@Wa)ZF=Mnfe3pn~6!v(@@bd-U$KGkMpFU38QD!~^y?XW1{eVud19H9W z;xpH=V@Fr~?05#bhOKVz7?dAyzyVqxc`J;A(^%kLfdlU%$^$mcJEc+DSHFIJwQJWd zZG)42Cbqhg{1F5MfyfY$@4V!F=Lxogt@wcWfcSv;fcSv;fcSv;fcSv;K&JXYWFMU( zNDvSNGy;rEGY!gH9dgJaYQlsGT82}Mn(`2T{_9`=>KgM763b;B#>&ZCAztfrQ1^ZJ z-RBweMr;|f$y>R}WB*e);0RvRKcJ0*P|NeJ{c5d?i@sEF~u3fu&%7dP-zy4a?dh4x~`cc)q z6=K_rWn&HoZ~+ER*kC*mXCFU)Jcr*gWlY|Ww_>dRth3HipMLtO0_N1q6t+O`C!c(x zMvfdAi?ur`rVr-k=IVYp`4z`END95MYsiox3LKK#LA|Jhx@d#8L3B9PLEi@s9H{#C z?W;~XWa8(fBhfO*$GK+B8Vw6Euw%sXsCX;Nn>ll) z_C+_GVH@5pcs_v{u`=-slV|(7b?d5z4I4VX5Acq_y8=4#?tnkL;SBrmtl?ec_P&BV z+`~H!?=~-cY2R_j9jDv)y|Y@iY8CTS*kZSn&)kR+BVwGgeQf1g5D*07LqNXslJ}h_ z*b27d1L6bX1L6bX1L6bX1L6bX1L6aj>I3n8VM;4OKoD?4fN^A|e!P_#dqu1@$Z58* z=NYd>`qfuoX*0o_z#j9&{0rKMI56a3oj<#Na5&^~-hKC7S9y@dIUSEY@`$TEyL>16 z*@vo_Z{=o>-ELgdKE%X{B~#Nsg7Q|TZYO;#qh8Dr$L@)X z7)#0tn^Nzor=F_Tu3cL}FLk1jdconKF8YPGVf8uHH!%>|VlC54-p@b(tT5-5Hrnl@yo)ZnNa3AM8+Ath2m*pYLI`B) zH91rkTM;>`(rPw~@1x=~9PMtcH<`q#V zjGtCE3`apL{mSTbbi;`aXSuycMys+emrjtxOqa zp7QW+qYTPqigL;`c`GLyDR1=X(F*hQ(AJbg9rRz}t<2GoydV$_0`i@gyze~0RXv8p^Ll~co?=fQ^tVFTRF|&GUdD3&pt5rT$Q|)X|Fv``%*pMigQy^J>QD4 zd(5G-uZKrp!gD}BK%LW0J56%|z{-waJl9S><5U)p>_pnY2H;awyj9ToR!&%jEdx&U zeezZqbMsxmJULsFw{pUn^2l4Uy*;J8F!Qa7ii-5>*~v!AllfNs7fez?AoU1j>Nk|w zE#Lp*1L6bX1L6bX1L6bX1L6bX1L6bX1IhD&)PJ3dMnRx@BY-$74yIvvtFZB7r}~Hq zqZ}!DEAj}YUE0Wfe@x!WOD1_MQ=Tb@eI{?kSTt>Nl5HHvD?a;tAMl#*3X`{T8gI%Y zZ$&xwl=8yxR$M2LHbExylqd78>`2SCAdor)Ds#0SI�SI�SI& z#0SI�SI&lIH{Qd|yf%K|m02M}YBKrovkZZ^gC9qT1ie}t+-C$ulD5KBu-73Tc#EGsKhICdr!LySF~-fQ zQ>Ut+`&&8bV;OLT-nw<`DsWObpl^@0r(5~1Zn{YA-cx?Hdd*r@=hV}5oBj4f+w#5F zOg!lW)Zv%1zA3{gk9s`q_e~kl=j1PD8S<26wxwK_Ax-yn0%4;c#(wvK2mBcQZ{51J z9(2EF^PP9z(e>SA)2Hvg`|ffla`{V66ucG32r2WFPy24V=_XZBP@uU!CmZ=}{`AvN z>afEO)8kATW=i`mx#SXscRCKf<6TUZVnIL<2#A1u=OynuPp}ni#RtR(#0SI�SI& z#0SI�SI&GSvqHeo{&`K|l})hyY`mOjFHUk!wMG`n&JG(^iAia5`vX)22;hF=fW$ z&6IKZe*OAsF^nDCWwQL?hadKo=Oi0t)Poc!;>IX{{`u!U^|F6Gdi2osiGisn-aKuh zeY6caXkVj7jbcy`(^!F(p?z2o)si z$_$dH-deS4sg)~NYQ4oJrK-5JOchyaN!f0F7L=6edn;D2R;Sb@RhqJ@dR5L?-HDyq(&x3~9RaRzdn2qz-)_95oL5n!Ygpu>8IE-t+W7 z%Desc+heVre45V^aHgHapVvs~Q#agjLyXsUl1KZ-jvcGWO?k;X@4WM3T(Oh9nDLSk z1O$P=2xRIvl-Mob|KbDU1L6bX1L6bX1L6bX1L6bX1L6b8^MSx0l~Pd<5Cj4tz*r{J zRP$Dhtt0k|c}gQkj#S@#^Nj*$g84p3hYT4KyB<1psQS~N{-k5Tj2)ZtUo#Ir3i2oi zvg{{hw{PEGAzpm*%{S}wuYdik`paMbqL88t=`FY1qJIDT-^XIvu$Mdo@;GVFU3cB3 zzW@Gv_4U_ZSJKCR4I4I0b?w?!>xKM8&s%T3Rn@CkuYygq515$tVcrw%u;an<;lqck zo;`cU`tM{PFfvcf6~T$M&Ts-#;(E)mg^ioBJHM@Z!?TIGkX}0f_P4*OtFF3Az4FQ{p6^-m zRud;qjL9>{40(mj5 zhjQA7*f(s$91r4Q+D2?phbL^o{yaE~EG`K1tMc;lbf3T>g2w@7(2Wygz%geE8|W8K zV+?#z{)a#OK`maqSj*qCWsBOnb!+8w^JcZxI&*N(sJnO)~^fme+(t{2Ntx=(x5z9@AA8m$Z7-Dz(-sTeD`h&ZBIz#Xn7*GDX!o`Q-93#u)A! zPhv&A#~gEv8aHm7?$>H-Z9%(U*aALl;lhRLth3IlA;#mP!En2kDVu`Vz z4!8Sg&#_^OGCV70pD<=Pz*~_!qkR}ByM5rT@QmR|)J82t{s;nsKwt#qJ1=?Pd4jEA zD?T7TAU+^IAU+^IAU+^IAU+^Ikf}Zp_@h!P3Ic*aAOslmWSUCeim`OWWzEx!DI-?T zSh86MWr*cchEr+o@YRc>yso`(ef z20RY9t3iVXslyLHT+4yq)M;E%2jz9^)>T-4ZN-Wenj^y8s|x3$MQZV) zg=&d)E?&4mEnU1wEw%E?mMqp~DCfD(;;<%+9iwX3D(9^Z_|<+2Cv<^FfPM6rqmMpX zVZBQ93*1x%ot6zKTfTg`nm&EH`r{w}sBNXqPW?d{`pT1a*nj9o8O99U9}ZwmALZOD zjt|BRYwMv6+fpV{%0-&)yjADUoppZ`c}9Q>pAnO{LjN%?^gUb?ulw)6U+bl>6I-WL zS9JNq=hJM5s;~!htXNL_INu861lwqnna4e>e@Whoen46F)WtL@76b%=I1$LyZz!=_ zzW>Ds#0SI�SI�SI�SI�SI&lIH_)equ^9K|m02K>#sM986QmTOlUP_$=f4 zj3t}*kmqTykL!3&>Oou1w_SW7=oxC2z&JvsM!F z55|o#Z}L{al6WEwV;^i~>>l&2D4YHP^N`00yBUASnt+I-@7%dlZMXK0!kT^HtS}d8 z*|KGNZ9eR4#ramyfs?!>Z3aFl2S?p zFF#$)o;6d=oH1R^vd$UPr|JCcnKRYg*|XH_nKSgcXu*6nV)!ss`{a|fKkX0PEL&(7 zQsM&M3LG5tV!gnbmaLdwD-XVE?%cV0PSzP`oDt)XXung+K9kEs9r_LPzzVzu4)9j= zQ~E8(1!ID-Lwj;kSh6>P-^#0SI�SI�SI& z#0SI�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 z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IL zKmY**5I_I{1Q0-=gV95I_I{1Q0*~0R#|0009ILKmY** z5I_I{1Q0*~ftC~K-*P3QKnNg!00IagfB*srAbW2UV2q1s}0tg_000IagfB*srAbV*IT2q1s} z0tg_000IagfB*srAbx)xQ<##v=!T1ncv-qpqB3c>vdAbqAbz^+5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q2K+0mXL|tNK=VV@9p^{G?9HC9CEV+%j4FCF}2!_3>m?d~RN{ zvLJu}0tg_000IagfB*srAb 1 { + // post subfield + return populateMapField(fd, v.Mutable(fd).Map(), []string{fieldPath[1]}, values) + } + return fmt.Errorf("invalid path: %q is not a message", fieldName) + } + + v = v.Mutable(fd).Message() + } + if of := fd.ContainingOneof(); of != nil { + if f := v.WhichOneof(of); f != nil { + return fmt.Errorf("field already set for oneof %q", of.FullName().Name()) + } + } + switch { + case fd.IsList(): + return populateRepeatedField(fd, v.Mutable(fd).List(), values) + case fd.IsMap(): + return populateMapField(fd, v.Mutable(fd).Map(), fieldPath, values) + } + if len(values) > 1 { + return fmt.Errorf("too many values for field %q: %s", fd.FullName().Name(), strings.Join(values, ", ")) + } + return populateField(fd, v, values[0]) +} + +func getFieldDescriptor(v protoreflect.Message, fieldName string) protoreflect.FieldDescriptor { + var ( + fields = v.Descriptor().Fields() + fd = getDescriptorByFieldAndName(fields, fieldName) + ) + if fd == nil { + switch { + case v.Descriptor().FullName() == structMessageFullname: + fd = fields.ByNumber(structFieldsFieldNumber) + case len(fieldName) > 2 && strings.HasSuffix(fieldName, "[]"): + fd = getDescriptorByFieldAndName(fields, strings.TrimSuffix(fieldName, "[]")) + default: + // If the type is map, you get the string "map[kratos]", where "map" is a field of proto and "kratos" is a key of map + // Use symbol . for separating fields/structs. (eg. structfield.field) + // ref: https://github.com/go-playground/form + field, _, err := parseURLQueryMapKey(fieldName) + if err != nil { + break + } + fd = getDescriptorByFieldAndName(fields, field) + } + } + return fd +} + +func getDescriptorByFieldAndName(fields protoreflect.FieldDescriptors, fieldName string) protoreflect.FieldDescriptor { + var fd protoreflect.FieldDescriptor + if fd = fields.ByName(protoreflect.Name(fieldName)); fd == nil { + fd = fields.ByJSONName(fieldName) + } + return fd +} + +func populateField(fd protoreflect.FieldDescriptor, v protoreflect.Message, value string) error { + if value == "" { + return nil + } + val, err := parseField(fd, value) + if err != nil { + return fmt.Errorf("parsing field %q: %w", fd.FullName().Name(), err) + } + v.Set(fd, val) + return nil +} + +func populateRepeatedField(fd protoreflect.FieldDescriptor, list protoreflect.List, values []string) error { + for _, value := range values { + v, err := parseField(fd, value) + if err != nil { + return fmt.Errorf("parsing list %q: %w", fd.FullName().Name(), err) + } + list.Append(v) + } + return nil +} + +func populateMapField(fd protoreflect.FieldDescriptor, mp protoreflect.Map, fieldPath []string, values []string) error { + _, keyName, err := parseURLQueryMapKey(strings.Join(fieldPath, fieldSeparator)) + if err != nil { + return err + } + key, err := parseField(fd.MapKey(), keyName) + if err != nil { + return fmt.Errorf("parsing map key %q: %w", fd.FullName().Name(), err) + } + value, err := parseField(fd.MapValue(), values[len(values)-1]) + if err != nil { + return fmt.Errorf("parsing map value %q: %w", fd.FullName().Name(), err) + } + mp.Set(key.MapKey(), value) + return nil +} + +func parseField(fd protoreflect.FieldDescriptor, value string) (protoreflect.Value, error) { + switch fd.Kind() { + case protoreflect.BoolKind: + v, err := strconv.ParseBool(value) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBool(v), nil + case protoreflect.EnumKind: + enum, err := protoregistry.GlobalTypes.FindEnumByName(fd.Enum().FullName()) + switch { + case errors.Is(err, protoregistry.NotFound): + return protoreflect.Value{}, fmt.Errorf("enum %q is not registered", fd.Enum().FullName()) + case err != nil: + return protoreflect.Value{}, fmt.Errorf("failed to look up enum: %w", err) + } + v := enum.Descriptor().Values().ByName(protoreflect.Name(value)) + if v == nil { + i, err := strconv.ParseInt(value, 10, 32) //nolint:mnd + if err != nil { + return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value) + } + v = enum.Descriptor().Values().ByNumber(protoreflect.EnumNumber(i)) + if v == nil { + return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value) + } + } + return protoreflect.ValueOfEnum(v.Number()), nil + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + v, err := strconv.ParseInt(value, 10, 32) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfInt32(int32(v)), nil + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + v, err := strconv.ParseInt(value, 10, 64) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfInt64(v), nil + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + v, err := strconv.ParseUint(value, 10, 32) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfUint32(uint32(v)), nil + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + v, err := strconv.ParseUint(value, 10, 64) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfUint64(v), nil + case protoreflect.FloatKind: + v, err := strconv.ParseFloat(value, 32) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfFloat32(float32(v)), nil + case protoreflect.DoubleKind: + v, err := strconv.ParseFloat(value, 64) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfFloat64(v), nil + case protoreflect.StringKind: + return protoreflect.ValueOfString(value), nil + case protoreflect.BytesKind: + v, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBytes(v), nil + case protoreflect.MessageKind, protoreflect.GroupKind: + return parseMessage(fd.Message(), value) + default: + panic(fmt.Sprintf("unknown field kind: %v", fd.Kind())) + } +} + +func parseMessage(md protoreflect.MessageDescriptor, value string) (protoreflect.Value, error) { + var msg proto.Message + switch md.FullName() { + case "google.protobuf.Timestamp": + if value == nullStr { + break + } + t, err := time.ParseInLocation(time.RFC3339Nano, value, time.Local) + if err != nil { + return protoreflect.Value{}, err + } + msg = timestamppb.New(t) + case "google.protobuf.Duration": + if value == nullStr { + break + } + d, err := time.ParseDuration(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = durationpb.New(d) + case "google.protobuf.DoubleValue": + v, err := strconv.ParseFloat(value, 64) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Double(v) + case "google.protobuf.FloatValue": + v, err := strconv.ParseFloat(value, 32) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Float(float32(v)) + case "google.protobuf.Int64Value": + v, err := strconv.ParseInt(value, 10, 64) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Int64(v) + case "google.protobuf.Int32Value": + v, err := strconv.ParseInt(value, 10, 32) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Int32(int32(v)) + case "google.protobuf.UInt64Value": + v, err := strconv.ParseUint(value, 10, 64) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.UInt64(v) + case "google.protobuf.UInt32Value": + v, err := strconv.ParseUint(value, 10, 32) //nolint:mnd + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.UInt32(uint32(v)) + case "google.protobuf.BoolValue": + v, err := strconv.ParseBool(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = wrapperspb.Bool(v) + case "google.protobuf.StringValue": + msg = wrapperspb.String(value) + case "google.protobuf.BytesValue": + v, err := base64.StdEncoding.DecodeString(value) + if err != nil { + if v, err = base64.URLEncoding.DecodeString(value); err != nil { + return protoreflect.Value{}, err + } + } + msg = wrapperspb.Bytes(v) + case "google.protobuf.FieldMask": + fm := &fieldmaskpb.FieldMask{} + for _, fv := range strings.Split(value, ",") { + fm.Paths = append(fm.Paths, jsonSnakeCase(fv)) + } + msg = fm + case "google.protobuf.Value": + fm, err := structpb.NewValue(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = fm + case "google.protobuf.Struct": + var v structpb.Struct + if err := protojson.Unmarshal([]byte(value), &v); err != nil { + return protoreflect.Value{}, err + } + msg = &v + default: + return protoreflect.Value{}, fmt.Errorf("unsupported message type: %q", string(md.FullName())) + } + return protoreflect.ValueOfMessage(msg.ProtoReflect()), nil +} + +// jsonSnakeCase converts a camelCase identifier to a snake_case identifier, +// according to the protobuf JSON specification. +// references: https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protojson/well_known_types.go#L864 +func jsonSnakeCase(s string) string { + var builder strings.Builder + builder.Grow(len(s)) + + for i := 0; i < len(s); i++ { // proto identifiers are always ASCII + c := s[i] + if isASCIIUpper(c) { + builder.WriteByte('_') + c += 'a' - 'A' // convert to lowercase + } + builder.WriteByte(c) + } + + return builder.String() +} + +func isASCIIUpper(c byte) bool { + return 'A' <= c && c <= 'Z' +} + +// parseURLQueryMapKey parse the url.Values the field name and key name of the value map type key +// for example: convert "map[key]" to "map" and "key" +func parseURLQueryMapKey(key string) (string, string, error) { + startIndex := strings.IndexByte(key, '[') + endIndex := strings.IndexByte(key, ']') + + if startIndex < 0 { + fsCount := strings.Count(key, fieldSeparator) + if fsCount != 1 { + return "", "", errInvalidFormatMapKey + } + + m, k, _ := strings.Cut(key, fieldSeparator) + if m == "" { + return "", "", errInvalidFormatMapKey + } + + return m, k, nil + } + + if startIndex <= 0 || startIndex >= endIndex || len(key) != endIndex+1 { + return "", "", errInvalidFormatMapKey + } + + return key[:startIndex], key[startIndex+1 : endIndex], nil +} diff --git a/encoding/form/proto_decode_test.go b/encoding/form/proto_decode_test.go new file mode 100644 index 0000000..596b41e --- /dev/null +++ b/encoding/form/proto_decode_test.go @@ -0,0 +1,348 @@ +package form + +import ( + "fmt" + "net/url" + "reflect" + "strconv" + "testing" + + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/go-kratos/kratos/v3/internal/testdata/complex" +) + +func TestDecodeValues(t *testing.T) { + form, err := url.ParseQuery("a=19&age=18&b=true&bool=false&byte=MTIz&bytes=MTIz&count=3&d=22.22&double=12.33&duration=" + + "2m0.000000022s&field=1%2C2&float=12.34&id=2233&int32=32&int64=64&numberOne=2233&price=11.23&sex=woman&strings=3344&" + + "strings=5566&string=go-kratos×tamp=1970-01-01T00%3A00%3A20.000000002Z&uint32=32&uint64=64&very_simple.component=5566") + if err != nil { + t.Fatal(err) + } + + comp := &complex.Complex{} + err = DecodeValues(comp, form) + if err != nil { + t.Fatal(err) + } + if comp.Id != int64(2233) { + t.Errorf("want %v, got %v", int64(2233), comp.Id) + } + if comp.NoOne != "2233" { + t.Errorf("want %v, got %v", "2233", comp.NoOne) + } + if comp.Simple == nil { + t.Fatalf("want %v, got %v", nil, comp.Simple) + } + if comp.Simple.Component != "5566" { + t.Errorf("want %v, got %v", "5566", comp.Simple.Component) + } + if len(comp.Strings) != 2 { + t.Fatalf("want %v, got %v", 2, len(comp.Strings)) + } + if comp.Strings[0] != "3344" { + t.Errorf("want %v, got %v", "3344", comp.Strings[0]) + } + if comp.Strings[1] != "5566" { + t.Errorf("want %v, got %v", "5566", comp.Strings[1]) + } +} + +func TestGetFieldDescriptor(t *testing.T) { + comp := &complex.Complex{} + + field := getFieldDescriptor(comp.ProtoReflect(), "id") + if field.Kind() != protoreflect.Int64Kind { + t.Errorf("want: %d, got: %d", protoreflect.Int64Kind, field.Kind()) + } + + field = getFieldDescriptor(comp.ProtoReflect(), "strings") + if field.Kind() != protoreflect.StringKind { + t.Errorf("want: %d, got: %d", protoreflect.StringKind, field.Kind()) + } +} + +func TestPopulateRepeatedField(t *testing.T) { + query, err := url.ParseQuery("strings=3344&strings=5566") + if err != nil { + t.Fatal(err) + } + comp := &complex.Complex{} + field := getFieldDescriptor(comp.ProtoReflect(), "strings") + + err = populateRepeatedField(field, comp.ProtoReflect().Mutable(field).List(), query["strings"]) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual([]string{"3344", "5566"}, comp.GetStrings()) { + t.Errorf("want: %v, got: %v", []string{"3344", "5566"}, comp.GetStrings()) + } +} + +func TestPopulateMapField(t *testing.T) { + query, err := url.ParseQuery("map%5Bkratos%5D=https://go-kratos.dev/") + if err != nil { + t.Fatal(err) + } + comp := &complex.Complex{} + field := getFieldDescriptor(comp.ProtoReflect(), "map") + // Fill the comp map field with the url query values + err = populateMapField(field, comp.ProtoReflect().Mutable(field).Map(), []string{"map[kratos]"}, query["map[kratos]"]) + if err != nil { + t.Fatal(err) + } + // Get the comp map field value + if query["map[kratos]"][0] != comp.Map["kratos"] { + t.Errorf("want: %s, got: %s", query["map[kratos]"], comp.Map["kratos"]) + } +} + +func TestPopulateMapSepField(t *testing.T) { + query, err := url.ParseQuery("map.name=kratos") + if err != nil { + t.Fatal(err) + } + comp := &complex.Complex{} + field := getFieldDescriptor(comp.ProtoReflect(), "map") + // Fill the comp map field with the url query values + err = populateMapField(field, comp.ProtoReflect().Mutable(field).Map(), []string{"map.name"}, query["map.name"]) + if err != nil { + t.Fatal(err) + } + // Get the comp map field value + if query["map.name"][0] != comp.Map["name"] { + t.Errorf("want: %s, got: %s", query, comp.Map) + } +} + +func TestParseField(t *testing.T) { + tests := []struct { + name string + fieldName string + protoReflectKind protoreflect.Kind + value string + targetProtoReflectValue protoreflect.Value + targetErr error + }{ + { + name: "BoolKind", + fieldName: "b", + protoReflectKind: protoreflect.BoolKind, + value: "true", + targetProtoReflectValue: protoreflect.ValueOfBool(true), + targetErr: nil, + }, + { + name: "BoolKind", + fieldName: "b", + protoReflectKind: protoreflect.BoolKind, + value: "a", + targetProtoReflectValue: protoreflect.Value{}, + targetErr: &strconv.NumError{Func: "ParseBool", Num: "a", Err: strconv.ErrSyntax}, + }, + { + name: "EnumKind", + fieldName: "sex", + protoReflectKind: protoreflect.EnumKind, + value: "1", + targetProtoReflectValue: protoreflect.ValueOfEnum(1), + targetErr: nil, + }, + { + name: "EnumKind", + fieldName: "sex", + protoReflectKind: protoreflect.EnumKind, + value: "2", + targetProtoReflectValue: protoreflect.Value{}, + targetErr: fmt.Errorf("%q is not a valid value", "2"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + comp := &complex.Complex{} + field := getFieldDescriptor(comp.ProtoReflect(), test.fieldName) + if test.protoReflectKind != field.Kind() { + t.Fatalf("want: %d, got: %d", test.protoReflectKind, field.Kind()) + } + val, err := parseField(field, test.value) + if !reflect.DeepEqual(test.targetErr, err) { + t.Fatalf("want: %s, got: %s", test.targetErr, err) + } + if !reflect.DeepEqual(test.targetProtoReflectValue, val) { + t.Errorf("want: %s, got: %s", test.targetProtoReflectValue, val) + } + }) + } +} + +func TestJsonSnakeCase(t *testing.T) { + tests := []struct { + camelCase string + snakeCase string + }{ + { + "userId", "user_id", + }, + { + "user", "user", + }, + { + "userIdAndUsername", "user_id_and_username", + }, + { + "", "", + }, + } + for _, test := range tests { + t.Run(test.camelCase, func(t *testing.T) { + snake := jsonSnakeCase(test.camelCase) + if snake != test.snakeCase { + t.Errorf("want: %s, got: %s", test.snakeCase, snake) + } + }) + } +} + +func TestIsASCIIUpper(t *testing.T) { + tests := []struct { + b byte + upper bool + }{ + { + 'A', true, + }, + { + 'a', false, + }, + { + ',', false, + }, + { + '1', false, + }, + { + ' ', false, + }, + } + for _, test := range tests { + t.Run(string(test.b), func(t *testing.T) { + upper := isASCIIUpper(test.b) + if test.upper != upper { + t.Errorf("'%s' is not ascii upper", string(test.b)) + } + }) + } +} + +func TestParseURLQueryMapKey(t *testing.T) { + tests := []struct { + fieldName string + field string + fieldKey string + err error + }{ + { + fieldName: "map[kratos]", field: "map", fieldKey: "kratos", err: nil, + }, + { + fieldName: "map[]", field: "map", fieldKey: "", err: nil, + }, + { + fieldName: "", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "[[]", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "map[kratos]=", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "[kratos]", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "map", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "map[", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "]kratos[", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "[kratos", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "kratos]", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "map.kratos", field: "map", fieldKey: "kratos", err: nil, + }, + { + fieldName: "map.", field: "map", fieldKey: "", err: nil, + }, + { + fieldName: ".kratos", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + { + fieldName: "map.kratos.v2", field: "", fieldKey: "", err: errInvalidFormatMapKey, + }, + } + for _, test := range tests { + t.Run(test.fieldName, func(t *testing.T) { + fieldName, fieldKey, err := parseURLQueryMapKey(test.fieldName) + if test.err != err { + t.Fatalf("want: %s, got: %s", test.err, err) + } + if test.field != fieldName { + t.Errorf("want: %s, got: %s", test.field, fieldName) + } + if test.fieldKey != fieldKey { + t.Errorf("want: %s, got: %s", test.fieldKey, fieldKey) + } + }) + } +} + +func BenchmarkParseURLQueryMapKey(b *testing.B) { + testCases := []struct { + testName string + fieldName string + wantedField string + wantedFieldKey string + wantedErr error + }{ + { + testName: "with bracket", + fieldName: "kratos[version]", + wantedField: "kratos", + wantedFieldKey: "version", + wantedErr: nil, + }, + { + testName: "with point", + fieldName: "kratos.version", + wantedField: "kratos", + wantedFieldKey: "version", + wantedErr: nil, + }, + } + + for _, testCase := range testCases { + b.Run(testCase.testName, func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + field, fieldKey, err := parseURLQueryMapKey(testCase.fieldName) + if testCase.wantedErr != err { + b.Fatalf("want: %s, got: %s", testCase.wantedErr, err) + } + if testCase.wantedField != field { + b.Errorf("want: %s, got: %s", testCase.wantedField, field) + } + if testCase.wantedFieldKey != fieldKey { + b.Errorf("want: %s, got: %s", testCase.wantedFieldKey, fieldKey) + } + } + }) + } +} diff --git a/encoding/form/proto_encode.go b/encoding/form/proto_encode.go new file mode 100644 index 0000000..901fc85 --- /dev/null +++ b/encoding/form/proto_encode.go @@ -0,0 +1,218 @@ +package form + +import ( + "encoding/base64" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/fieldmaskpb" +) + +// EncodeValues encode a message into url values. +func EncodeValues(msg any) (url.Values, error) { + if msg == nil || (reflect.ValueOf(msg).Kind() == reflect.Pointer && reflect.ValueOf(msg).IsNil()) { + return url.Values{}, nil + } + if v, ok := msg.(proto.Message); ok { + u := make(url.Values) + err := encodeByField(u, "", v.ProtoReflect()) + return u, err + } + return encoder.Encode(msg) +} + +func encodeByField(u url.Values, path string, m protoreflect.Message) (finalErr error) { + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + var ( + key string + newPath string + ) + if fd.HasJSONName() { + key = fd.JSONName() + } else { + key = fd.TextName() + } + if path == "" { + newPath = key + } else { + newPath = path + "." + key + } + if of := fd.ContainingOneof(); of != nil { + if f := m.WhichOneof(of); f != nil && f != fd { + return true + } + } + switch { + case fd.IsList(): + if v.List().Len() > 0 { + list, err := encodeRepeatedField(fd, v.List()) + if err != nil { + finalErr = err + } + for _, item := range list { + u.Add(newPath, item) + } + } + case fd.IsMap(): + if v.Map().Len() > 0 { + m, err := encodeMapField(fd, v.Map()) + if err != nil { + finalErr = err + } + for k, value := range m { + u.Set(newPath+"["+k+"]", value) + } + } + case (fd.Kind() == protoreflect.MessageKind) || (fd.Kind() == protoreflect.GroupKind): + value, err := encodeMessage(fd.Message(), v) + if err == nil { + u.Set(newPath, value) + return true + } + if err = encodeByField(u, newPath, v.Message()); err != nil { + finalErr = err + } + default: + value, err := EncodeField(fd, v) + if err != nil { + finalErr = err + } + u.Set(newPath, value) + } + return true + }) + return +} + +func encodeRepeatedField(fieldDescriptor protoreflect.FieldDescriptor, list protoreflect.List) ([]string, error) { + var values []string + for i := 0; i < list.Len(); i++ { + value, err := EncodeField(fieldDescriptor, list.Get(i)) + if err != nil { + return nil, err + } + values = append(values, value) + } + return values, nil +} + +func encodeMapField(fieldDescriptor protoreflect.FieldDescriptor, mp protoreflect.Map) (map[string]string, error) { + m := make(map[string]string) + mp.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { + key, err := EncodeField(fieldDescriptor.MapKey(), k.Value()) + if err != nil { + return false + } + value, err := EncodeField(fieldDescriptor.MapValue(), v) + if err != nil { + return false + } + m[key] = value + return true + }) + + return m, nil +} + +// EncodeField encode proto message filed +func EncodeField(fieldDescriptor protoreflect.FieldDescriptor, value protoreflect.Value) (string, error) { + switch fieldDescriptor.Kind() { + case protoreflect.BoolKind: + return strconv.FormatBool(value.Bool()), nil + case protoreflect.EnumKind: + if fieldDescriptor.Enum().FullName() == "google.protobuf.NullValue" { + return nullStr, nil + } + desc := fieldDescriptor.Enum().Values().ByNumber(value.Enum()) + return string(desc.Name()), nil + case protoreflect.BytesKind: + return base64.URLEncoding.EncodeToString(value.Bytes()), nil + case protoreflect.MessageKind, protoreflect.GroupKind: + return encodeMessage(fieldDescriptor.Message(), value) + default: + return value.String(), nil + } +} + +// encodeMessage marshals the fields in the given protoreflect.Message. +// If the typeURL is non-empty, then a synthetic "@type" field is injected +// containing the URL as the value. +func encodeMessage(msgDescriptor protoreflect.MessageDescriptor, value protoreflect.Value) (string, error) { + switch msgDescriptor.FullName() { + case timestampMessageFullname: + return marshalTimestamp(value.Message()) + case durationMessageFullname: + return marshalDuration(value.Message()) + case bytesMessageFullname: + return marshalBytes(value.Message()) + case "google.protobuf.DoubleValue", "google.protobuf.FloatValue", "google.protobuf.Int64Value", "google.protobuf.Int32Value", + "google.protobuf.UInt64Value", "google.protobuf.UInt32Value", "google.protobuf.BoolValue", "google.protobuf.StringValue": + fd := msgDescriptor.Fields() + v := value.Message().Get(fd.ByName("value")) + return fmt.Sprint(v.Interface()), nil + case fieldMaskFullName: + m, ok := value.Message().Interface().(*fieldmaskpb.FieldMask) + if !ok || m == nil { + return "", nil + } + for i, v := range m.Paths { + m.Paths[i] = jsonCamelCase(v) + } + return strings.Join(m.Paths, ","), nil + default: + return "", fmt.Errorf("unsupported message type: %q", string(msgDescriptor.FullName())) + } +} + +// EncodeFieldMask return field mask name=paths +func EncodeFieldMask(m protoreflect.Message) (query string) { + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + if fd.Kind() == protoreflect.MessageKind { + if msg := fd.Message(); msg.FullName() == fieldMaskFullName { + value, err := encodeMessage(msg, v) + if err != nil { + return false + } + if fd.HasJSONName() { + query = fd.JSONName() + "=" + value + } else { + query = fd.TextName() + "=" + value + } + return false + } + } + return true + }) + return +} + +// jsonCamelCase converts a snake_case identifier to a camelCase identifier, +// according to the protobuf JSON specification. +// references: https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protojson/well_known_types.go#L842 +func jsonCamelCase(s string) string { + var builder strings.Builder + builder.Grow(len(s)) + + wasUnderscore := false + for i := 0; i < len(s); i++ { // proto identifiers are always ASCIIS + c := s[i] + if c != '_' { + if wasUnderscore && isASCIILower(c) { + c -= 'a' - 'A' // convert to uppercase + } + builder.WriteByte(c) + } + wasUnderscore = c == '_' + } + + return builder.String() +} + +func isASCIILower(c byte) bool { + return 'a' <= c && c <= 'z' +} diff --git a/encoding/form/proto_encode_test.go b/encoding/form/proto_encode_test.go new file mode 100644 index 0000000..ccf4ef5 --- /dev/null +++ b/encoding/form/proto_encode_test.go @@ -0,0 +1,112 @@ +package form + +import ( + "testing" + "time" + + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/fieldmaskpb" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/go-kratos/kratos/v3/internal/testdata/complex" +) + +func TestEncodeValues(t *testing.T) { + in := &complex.Complex{ + Id: 2233, + NoOne: "2233", + Simple: &complex.Simple{Component: "5566"}, + Strings: []string{"3344", "5566"}, + B: true, + Sex: complex.Sex_woman, + Age: 18, + A: 19, + Count: 3, + Price: 11.23, + D: 22.22, + Byte: []byte("123"), + Map: map[string]string{"kratos": "https://go-kratos.dev/", "kratos_start": "https://go-kratos.dev/docs/getting-started/start/"}, + MapInt64Key: map[int64]string{1: "kratos", 2: "go-zero"}, + + Timestamp: timestamppb.New(time.Date(1970, 1, 1, 0, 0, 20, 2, time.Local)), + Duration: &durationpb.Duration{Seconds: 120, Nanos: 22}, + Field: &fieldmaskpb.FieldMask{Paths: []string{"1", "2"}}, + Double: &wrapperspb.DoubleValue{Value: 12.33}, + Float: &wrapperspb.FloatValue{Value: 12.34}, + Int64: &wrapperspb.Int64Value{Value: 64}, + Int32: &wrapperspb.Int32Value{Value: 32}, + Uint64: &wrapperspb.UInt64Value{Value: 64}, + Uint32: &wrapperspb.UInt32Value{Value: 32}, + Bool: &wrapperspb.BoolValue{Value: false}, + String_: &wrapperspb.StringValue{Value: "go-kratos"}, + Bytes: &wrapperspb.BytesValue{Value: []byte("123")}, + } + query, err := EncodeValues(in) + if err != nil { + t.Fatal(err) + } + want := "a=19&age=18&b=true&bool=false&byte=MTIz&bytes=MTIz&count=3&d=22.22&double=12.33&duration=2m0.000000022s&field=1%2C2&float=12.34&id=2233&int32=32&int64=64&map%5Bkratos%5D=https%3A%2F%2Fgo-kratos.dev%2F&map%5Bkratos_start%5D=https%3A%2F%2Fgo-kratos.dev%2Fdocs%2Fgetting-started%2Fstart%2F&map_int64_key%5B1%5D=kratos&map_int64_key%5B2%5D=go-zero&numberOne=2233&price=11.23&sex=woman&string=go-kratos&strings=3344&strings=5566×tamp=1970-01-01T00%3A00%3A20.000000002Z&uint32=32&uint64=64&very_simple.component=5566" // nolint:lll + if got := query.Encode(); want != got { + t.Errorf("\nwant: %s, \ngot: %s", want, got) + } +} + +func TestJsonCamelCase(t *testing.T) { + tests := []struct { + camelCase string + snakeCase string + }{ + { + "userId", "user_id", + }, + { + "user", "user", + }, + { + "userIdAndUsername", "user_id_and_username", + }, + { + "", "", + }, + } + for _, test := range tests { + t.Run(test.snakeCase, func(t *testing.T) { + camel := jsonCamelCase(test.snakeCase) + if camel != test.camelCase { + t.Errorf("want: %s, got: %s", test.camelCase, camel) + } + }) + } +} + +func TestIsASCIILower(t *testing.T) { + tests := []struct { + b byte + lower bool + }{ + { + 'A', false, + }, + { + 'a', true, + }, + { + ',', false, + }, + { + '1', false, + }, + { + ' ', false, + }, + } + for _, test := range tests { + t.Run(string(test.b), func(t *testing.T) { + lower := isASCIILower(test.b) + if test.lower != lower { + t.Errorf("'%s' is not ascii lower", string(test.b)) + } + }) + } +} diff --git a/encoding/form/well_known_types.go b/encoding/form/well_known_types.go new file mode 100644 index 0000000..4690bdd --- /dev/null +++ b/encoding/form/well_known_types.go @@ -0,0 +1,94 @@ +package form + +import ( + "encoding/base64" + "fmt" + "math" + "strings" + "time" + + "google.golang.org/protobuf/reflect/protoreflect" +) + +const ( + // timestamp + timestampMessageFullname protoreflect.FullName = "google.protobuf.Timestamp" + maxTimestampSeconds = 253402300799 + minTimestampSeconds = -6213559680013 + timestampSecondsFieldNumber protoreflect.FieldNumber = 1 + timestampNanosFieldNumber protoreflect.FieldNumber = 2 + + // duration + durationMessageFullname protoreflect.FullName = "google.protobuf.Duration" + secondsInNanos = 999999999 + durationSecondsFieldNumber protoreflect.FieldNumber = 1 + durationNanosFieldNumber protoreflect.FieldNumber = 2 + + // bytes + bytesMessageFullname protoreflect.FullName = "google.protobuf.BytesValue" + bytesValueFieldNumber protoreflect.FieldNumber = 1 + + // google.protobuf.Struct. + structMessageFullname protoreflect.FullName = "google.protobuf.Struct" + structFieldsFieldNumber protoreflect.FieldNumber = 1 + + fieldMaskFullName protoreflect.FullName = "google.protobuf.FieldMask" +) + +func marshalTimestamp(m protoreflect.Message) (string, error) { + fds := m.Descriptor().Fields() + fdSeconds := fds.ByNumber(timestampSecondsFieldNumber) + fdNanos := fds.ByNumber(timestampNanosFieldNumber) + + secsVal := m.Get(fdSeconds) + nanosVal := m.Get(fdNanos) + secs := secsVal.Int() + nanos := nanosVal.Int() + if secs < minTimestampSeconds || secs > maxTimestampSeconds { + return "", fmt.Errorf("%s: seconds out of range %v", timestampMessageFullname, secs) + } + if nanos < 0 || nanos > secondsInNanos { + return "", fmt.Errorf("%s: nanos out of range %v", timestampMessageFullname, nanos) + } + // Uses RFC 3339, where generated output will be Z-normalized and uses 0, 3, + // 6 or 9 fractional digits. + t := time.Unix(secs, nanos).Local() + x := t.Format("2006-01-02T15:04:05.000000000") + x = strings.TrimSuffix(x, "000") + x = strings.TrimSuffix(x, "000") + x = strings.TrimSuffix(x, ".000") + return x + "Z", nil +} + +func marshalDuration(m protoreflect.Message) (string, error) { + fds := m.Descriptor().Fields() + fdSeconds := fds.ByNumber(durationSecondsFieldNumber) + fdNanos := fds.ByNumber(durationNanosFieldNumber) + + secsVal := m.Get(fdSeconds) + nanosVal := m.Get(fdNanos) + secs := secsVal.Int() + nanos := nanosVal.Int() + d := time.Duration(secs) * time.Second + overflow := d/time.Second != time.Duration(secs) + d += time.Duration(nanos) * time.Nanosecond + overflow = overflow || (secs < 0 && nanos < 0 && d > 0) + overflow = overflow || (secs > 0 && nanos > 0 && d < 0) + if overflow { + switch { + case secs < 0: + return time.Duration(math.MinInt64).String(), nil + case secs > 0: + return time.Duration(math.MaxInt64).String(), nil + } + } + return d.String(), nil +} + +func marshalBytes(m protoreflect.Message) (string, error) { + fds := m.Descriptor().Fields() + fdBytes := fds.ByNumber(bytesValueFieldNumber) + bytesVal := m.Get(fdBytes) + val := bytesVal.Bytes() + return base64.StdEncoding.EncodeToString(val), nil +} diff --git a/encoding/form/well_known_types_test.go b/encoding/form/well_known_types_test.go new file mode 100644 index 0000000..ce5e970 --- /dev/null +++ b/encoding/form/well_known_types_test.go @@ -0,0 +1,95 @@ +package form + +import ( + "encoding/base64" + "testing" + "time" + + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func TestMarshalTimeStamp(t *testing.T) { + tests := []struct { + input *timestamppb.Timestamp + expect string + }{ + { + input: timestamppb.New(time.Date(2022, 1, 2, 3, 4, 5, 6, time.Local)), + expect: "2022-01-02T03:04:05.000000006Z", + }, + { + input: timestamppb.New(time.Date(2022, 13, 1, 13, 61, 61, 100, time.Local)), + expect: "2023-01-01T14:02:01.000000100Z", + }, + } + for _, v := range tests { + got, err := marshalTimestamp(v.input.ProtoReflect()) + if err != nil { + t.Fatal(err) + } + if want := v.expect; got != want { + t.Errorf("expect %v, got %v", want, got) + } + } +} + +func TestMarshalDuration(t *testing.T) { + tests := []struct { + input *durationpb.Duration + expect string + }{ + { + input: durationpb.New(time.Duration(1<<63 - 1)), + expect: "2562047h47m16.854775807s", + }, + { + input: durationpb.New(time.Duration(-1 << 63)), + expect: "-2562047h47m16.854775808s", + }, + { + input: durationpb.New(100 * time.Second), + expect: "1m40s", + }, + { + input: durationpb.New(-100 * time.Second), + expect: "-1m40s", + }, + } + for _, v := range tests { + got, err := marshalDuration(v.input.ProtoReflect()) + if err != nil { + t.Fatal(err) + } + if want := v.expect; got != want { + t.Errorf("expect %s, got %s", want, got) + } + } +} + +func TestMarshalBytes(t *testing.T) { + tests := []struct { + input protoreflect.Message + expect string + }{ + { + input: wrapperspb.Bytes([]byte("abc123!?$*&()'-=@~")).ProtoReflect(), + expect: base64.StdEncoding.EncodeToString([]byte("abc123!?$*&()'-=@~")), + }, + { + input: wrapperspb.Bytes([]byte("kratos")).ProtoReflect(), + expect: base64.StdEncoding.EncodeToString([]byte("kratos")), + }, + } + for _, v := range tests { + got, err := marshalBytes(v.input) + if err != nil { + t.Fatal(err) + } + if want := v.expect; got != want { + t.Errorf("expect %v, got %v", want, got) + } + } +} diff --git a/encoding/json/json.go b/encoding/json/json.go new file mode 100644 index 0000000..054d25e --- /dev/null +++ b/encoding/json/json.go @@ -0,0 +1,42 @@ +package json + +import ( + "encoding/json" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Name is the name registered for the json codec. +const Name = "json" + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with json. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + switch m := v.(type) { + case json.Marshaler: + return m.MarshalJSON() + default: + return json.Marshal(m) + } +} + +func (codec) Unmarshal(data []byte, v any) error { + if len(data) == 0 { + return nil + } + switch m := v.(type) { + case json.Unmarshaler: + return m.UnmarshalJSON(data) + default: + return json.Unmarshal(data, m) + } +} + +func (codec) Name() string { + return Name +} diff --git a/encoding/json/json_test.go b/encoding/json/json_test.go new file mode 100644 index 0000000..d3e0c44 --- /dev/null +++ b/encoding/json/json_test.go @@ -0,0 +1,142 @@ +package json + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + testData "github.com/go-kratos/kratos/v3/internal/testdata/encoding" +) + +type testEmbed struct { + Level1a int `json:"a"` + Level1b int `json:"b"` + Level1c int `json:"c"` +} + +type testMessage struct { + Field1 string `json:"a"` + Field2 string `json:"b"` + Field3 string `json:"c"` + Embed *testEmbed `json:"embed,omitempty"` +} + +type mock struct { + value int +} + +const ( + Unknown = iota + Gopher + Zebra +) + +func (a *mock) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + switch strings.ToLower(s) { + default: + a.value = Unknown + case "gopher": + a.value = Gopher + case "zebra": + a.value = Zebra + } + + return nil +} + +func (a *mock) MarshalJSON() ([]byte, error) { + var s string + switch a.value { + default: + s = "unknown" + case Gopher: + s = "gopher" + case Zebra: + s = "zebra" + } + + return json.Marshal(s) +} + +func TestJSON_Marshal(t *testing.T) { + tests := []struct { + input any + expect string + }{ + { + input: &testMessage{}, + expect: `{"a":"","b":"","c":""}`, + }, + { + input: &testMessage{Field1: "a", Field2: "b", Field3: "c"}, + expect: `{"a":"a","b":"b","c":"c"}`, + }, + { + input: &testData.TestModel{Id: 1, Name: "go-kratos", Hobby: []string{"1", "2"}}, + expect: `{"id":1,"name":"go-kratos","hobby":["1","2"]}`, + }, + { + input: &mock{value: Gopher}, + expect: `"gopher"`, + }, + } + for _, v := range tests { + data, err := (codec{}).Marshal(v.input) + if err != nil { + t.Errorf("marshal(%#v): %s", v.input, err) + } + if got, want := string(data), v.expect; strings.ReplaceAll(got, " ", "") != want { + if strings.Contains(want, "\n") { + t.Errorf("marshal(%#v):\nHAVE:\n%s\nWANT:\n%s", v.input, got, want) + } else { + t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) + } + } + } +} + +func TestJSON_Unmarshal(t *testing.T) { + p := testMessage{} + p2 := &testData.TestModel{} + p4 := &mock{} + tests := []struct { + input string + expect any + }{ + { + input: `{"a":"","b":"","c":""}`, + expect: &testMessage{}, + }, + { + input: `{"a":"a","b":"b","c":"c"}`, + expect: &p, + }, + { + input: `{"id":1,"name":"go-kratos","hobby":["1","2"]}`, + expect: &p2, + }, + { + input: `"zebra"`, + expect: p4, + }, + } + for _, v := range tests { + want := []byte(v.input) + err := (codec{}).Unmarshal(want, v.expect) + if err != nil { + t.Errorf("marshal(%#v): %s", v.input, err) + } + got, err := codec{}.Marshal(v.expect) + if err != nil { + t.Errorf("marshal(%#v): %s", v.input, err) + } + if !reflect.DeepEqual(strings.ReplaceAll(string(got), " ", ""), strings.ReplaceAll(string(want), " ", "")) { + t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) + } + } +} diff --git a/encoding/proto/proto.go b/encoding/proto/proto.go new file mode 100644 index 0000000..1a63417 --- /dev/null +++ b/encoding/proto/proto.go @@ -0,0 +1,51 @@ +// Package proto defines the protobuf codec. Importing this package will +// register the codec. +package proto + +import ( + "errors" + "reflect" + + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Name is the name registered for the proto compressor. +const Name = "proto" + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with protobuf. It is the default codec for Transport. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + return proto.Marshal(v.(proto.Message)) +} + +func (codec) Unmarshal(data []byte, v any) error { + pm, err := getProtoMessage(v) + if err != nil { + return err + } + return proto.Unmarshal(data, pm) +} + +func (codec) Name() string { + return Name +} + +func getProtoMessage(v any) (proto.Message, error) { + if msg, ok := v.(proto.Message); ok { + return msg, nil + } + val := reflect.ValueOf(v) + if val.Kind() != reflect.Pointer { + return nil, errors.New("not proto message") + } + + val = val.Elem() + return getProtoMessage(val.Interface()) +} diff --git a/encoding/proto/proto_test.go b/encoding/proto/proto_test.go new file mode 100644 index 0000000..fda1dcd --- /dev/null +++ b/encoding/proto/proto_test.go @@ -0,0 +1,104 @@ +package proto + +import ( + "reflect" + "testing" + + testData "github.com/go-kratos/kratos/v3/internal/testdata/encoding" +) + +func TestName(t *testing.T) { + c := new(codec) + if !reflect.DeepEqual(c.Name(), "proto") { + t.Errorf("no expect float_key value: %v, but got: %v", c.Name(), "proto") + } +} + +func TestCodec(t *testing.T) { + c := new(codec) + + model := testData.TestModel{ + Id: 1, + Name: "kratos", + Hobby: []string{"study", "eat", "play"}, + } + + m, err := c.Marshal(&model) + if err != nil { + t.Errorf("Marshal() should be nil, but got %s", err) + } + + var res testData.TestModel + + err = c.Unmarshal(m, &res) + if err != nil { + t.Errorf("Unmarshal() should be nil, but got %s", err) + } + if !reflect.DeepEqual(res.Id, model.Id) { + t.Errorf("ID should be %d, but got %d", res.Id, model.Id) + } + if !reflect.DeepEqual(res.Name, model.Name) { + t.Errorf("Name should be %s, but got %s", res.Name, model.Name) + } + if !reflect.DeepEqual(res.Hobby, model.Hobby) { + t.Errorf("Hobby should be %s, but got %s", res.Hobby, model.Hobby) + } +} + +func TestCodec2(t *testing.T) { + c := new(codec) + + model := testData.TestModel{ + Id: 1, + Name: "kratos", + Hobby: []string{"study", "eat", "play"}, + } + + m, err := c.Marshal(&model) + if err != nil { + t.Errorf("Marshal() should be nil, but got %s", err) + } + + var res testData.TestModel + rp := &res + + err = c.Unmarshal(m, &rp) + if err != nil { + t.Errorf("Unmarshal() should be nil, but got %s", err) + } + if !reflect.DeepEqual(res.Id, model.Id) { + t.Errorf("ID should be %d, but got %d", res.Id, model.Id) + } + if !reflect.DeepEqual(res.Name, model.Name) { + t.Errorf("Name should be %s, but got %s", res.Name, model.Name) + } + if !reflect.DeepEqual(res.Hobby, model.Hobby) { + t.Errorf("Hobby should be %s, but got %s", res.Hobby, model.Hobby) + } +} + +func Test_getProtoMessage(t *testing.T) { + p := &testData.TestModel{Id: 1} + type args struct { + v any + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "test1", args: args{v: &testData.TestModel{}}, wantErr: false}, + {name: "test2", args: args{v: testData.TestModel{}}, wantErr: true}, + {name: "test3", args: args{v: &p}, wantErr: false}, + {name: "test4", args: args{v: 1}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := getProtoMessage(tt.args.v) + if (err != nil) != tt.wantErr { + t.Errorf("getProtoMessage() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/encoding/protojson/protojson.go b/encoding/protojson/protojson.go new file mode 100644 index 0000000..db43b87 --- /dev/null +++ b/encoding/protojson/protojson.go @@ -0,0 +1,54 @@ +package protojson + +import ( + "fmt" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Name is the name registered for the protojson codec. +const Name = "protojson" + +var ( + // MarshalOptions is a configurable JSON format marshaller. + MarshalOptions = protojson.MarshalOptions{ + EmitUnpopulated: true, + } + // UnmarshalOptions is a configurable JSON format parser. + UnmarshalOptions = protojson.UnmarshalOptions{ + DiscardUnknown: true, + } +) + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with protojson. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + m, ok := v.(proto.Message) + if !ok { + return nil, fmt.Errorf("failed to marshal, message is %T, want proto.Message", v) + } + return MarshalOptions.Marshal(m) +} + +func (codec) Unmarshal(data []byte, v any) error { + if len(data) == 0 { + return nil + } + m, ok := v.(proto.Message) + if !ok { + return fmt.Errorf("failed to unmarshal, message is %T, want proto.Message", v) + } + return UnmarshalOptions.Unmarshal(data, m) +} + +func (codec) Name() string { + return Name +} diff --git a/encoding/protojson/protojson_test.go b/encoding/protojson/protojson_test.go new file mode 100644 index 0000000..b85368a --- /dev/null +++ b/encoding/protojson/protojson_test.go @@ -0,0 +1,58 @@ +package protojson + +import ( + "reflect" + "strings" + "testing" + + testData "github.com/go-kratos/kratos/v3/internal/testdata/encoding" +) + +func TestName(t *testing.T) { + c := new(codec) + if !reflect.DeepEqual(c.Name(), "protojson") { + t.Errorf("expected %v, got %v", "protojson", c.Name()) + } +} + +func TestCodec(t *testing.T) { + c := new(codec) + model := testData.TestModel{ + Id: 1, + Name: "go-kratos", + Hobby: []string{"1", "2"}, + } + + data, err := c.Marshal(&model) + if err != nil { + t.Fatalf("Marshal() error = %v", err) + } + if got, want := strings.ReplaceAll(string(data), " ", ""), `{"id":"1","name":"go-kratos","hobby":["1","2"],"attrs":{}}`; got != want { + t.Errorf("Marshal() = %s, want %s", got, want) + } + + var out testData.TestModel + if err := c.Unmarshal([]byte(`{"id":"1","name":"go-kratos","hobby":["1","2"],"unknown":"discarded"}`), &out); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + if !reflect.DeepEqual(out.Id, model.Id) { + t.Errorf("Id = %d, want %d", out.Id, model.Id) + } + if !reflect.DeepEqual(out.Name, model.Name) { + t.Errorf("Name = %s, want %s", out.Name, model.Name) + } + if !reflect.DeepEqual(out.Hobby, model.Hobby) { + t.Errorf("Hobby = %v, want %v", out.Hobby, model.Hobby) + } +} + +func TestCodecRejectsNonProtoMessage(t *testing.T) { + c := new(codec) + + if _, err := c.Marshal(struct{}{}); err == nil { + t.Fatal("Marshal() error = nil, want error") + } + if err := c.Unmarshal([]byte("{}"), &struct{}{}); err == nil { + t.Fatal("Unmarshal() error = nil, want error") + } +} diff --git a/encoding/xml/xml.go b/encoding/xml/xml.go new file mode 100644 index 0000000..c5bd542 --- /dev/null +++ b/encoding/xml/xml.go @@ -0,0 +1,29 @@ +package xml + +import ( + "encoding/xml" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Name is the name registered for the xml codec. +const Name = "xml" + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with xml. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + return xml.Marshal(v) +} + +func (codec) Unmarshal(data []byte, v any) error { + return xml.Unmarshal(data, v) +} + +func (codec) Name() string { + return Name +} diff --git a/encoding/xml/xml_test.go b/encoding/xml/xml_test.go new file mode 100644 index 0000000..c2c21af --- /dev/null +++ b/encoding/xml/xml_test.go @@ -0,0 +1,117 @@ +package xml + +import ( + "reflect" + "strings" + "testing" +) + +type Plain struct { + V any +} + +type NestedOrder struct { + XMLName struct{} `xml:"result"` + Field1 string `xml:"parent>c"` + Field2 string `xml:"parent>b"` + Field3 string `xml:"parent>a"` +} + +func TestCodec_Marshal(t *testing.T) { + tests := []struct { + Value any + ExpectXML string + }{ + // Test value types + {Value: &Plain{true}, ExpectXML: `true`}, + {Value: &Plain{false}, ExpectXML: `false`}, + {Value: &Plain{42}, ExpectXML: `42`}, + { + Value: &NestedOrder{Field1: "C", Field2: "B", Field3: "A"}, + ExpectXML: `` + + `` + + `C` + + `B` + + `A` + + `` + + ``, + }, + } + for _, tt := range tests { + data, err := (codec{}).Marshal(tt.Value) + if err != nil { + t.Errorf("marshal(%#v): %s", tt.Value, err) + } + if got, want := string(data), tt.ExpectXML; got != want { + if strings.Contains(want, "\n") { + t.Errorf("marshal(%#v):\nHAVE:\n%s\nWANT:\n%s", tt.Value, got, want) + } else { + t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", tt.Value, got, want) + } + } + } +} + +func TestCodec_Unmarshal(t *testing.T) { + tests := []struct { + want any + InputXML string + }{ + { + want: &NestedOrder{Field1: "C", Field2: "B", Field3: "A"}, + InputXML: `` + + `` + + `C` + + `B` + + `A` + + `` + + ``, + }, + } + + for _, tt := range tests { + vt := reflect.TypeOf(tt.want) + dest := reflect.New(vt.Elem()).Interface() + data := []byte(tt.InputXML) + err := (codec{}).Unmarshal(data, dest) + if err != nil { + t.Errorf("unmarshal(%#v, %#v): %s", tt.InputXML, dest, err) + } + if got, want := dest, tt.want; !reflect.DeepEqual(got, want) { + t.Errorf("unmarshal(%q):\nhave %#v\nwant %#v", tt.InputXML, got, want) + } + } +} + +func TestCodec_NilUnmarshal(t *testing.T) { + tests := []struct { + want any + InputXML string + }{ + { + want: &NestedOrder{Field1: "C", Field2: "B", Field3: "A"}, + InputXML: `` + + `` + + `C` + + `B` + + `A` + + `` + + ``, + }, + } + + for _, tt := range tests { + s := struct { + A string `xml:"a"` + B *NestedOrder + }{A: "a"} + data := []byte(tt.InputXML) + err := (codec{}).Unmarshal(data, &s.B) + if err != nil { + t.Errorf("unmarshal(%#v, %#v): %s", tt.InputXML, s.B, err) + } + if got, want := s.B, tt.want; !reflect.DeepEqual(got, want) { + t.Errorf("unmarshal(%q):\nhave %#v\nwant %#v", tt.InputXML, got, want) + } + } +} diff --git a/encoding/yaml/yaml.go b/encoding/yaml/yaml.go new file mode 100644 index 0000000..4cbbc4f --- /dev/null +++ b/encoding/yaml/yaml.go @@ -0,0 +1,29 @@ +package yaml + +import ( + "gopkg.in/yaml.v3" + + "github.com/go-kratos/kratos/v3/encoding" +) + +// Name is the name registered for the yaml codec. +const Name = "yaml" + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with yaml. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + return yaml.Marshal(v) +} + +func (codec) Unmarshal(data []byte, v any) error { + return yaml.Unmarshal(data, v) +} + +func (codec) Name() string { + return Name +} diff --git a/encoding/yaml/yaml_test.go b/encoding/yaml/yaml_test.go new file mode 100644 index 0000000..0cb7527 --- /dev/null +++ b/encoding/yaml/yaml_test.go @@ -0,0 +1,104 @@ +package yaml + +import ( + "math" + "reflect" + "testing" +) + +func TestCodec_Unmarshal(t *testing.T) { + tests := []struct { + data string + value any + }{ + { + "", + (*struct{})(nil), + }, + { + "{}", &struct{}{}, + }, + { + "v: hi", + map[string]string{"v": "hi"}, + }, + { + "v: hi", map[string]any{"v": "hi"}, + }, + { + "v: true", + map[string]string{"v": "true"}, + }, + { + "v: true", + map[string]any{"v": true}, + }, + { + "v: 10", + map[string]any{"v": 10}, + }, + { + "v: 0b10", + map[string]any{"v": 2}, + }, + { + "v: 0xA", + map[string]any{"v": 10}, + }, + { + "v: 4294967296", + map[string]int64{"v": 4294967296}, + }, + { + "v: 0.1", + map[string]any{"v": 0.1}, + }, + { + "v: .1", + map[string]any{"v": 0.1}, + }, + { + "v: .Inf", + map[string]any{"v": math.Inf(+1)}, + }, + { + "v: -.Inf", + map[string]any{"v": math.Inf(-1)}, + }, + { + "v: -10", + map[string]any{"v": -10}, + }, + { + "v: -.1", + map[string]any{"v": -0.1}, + }, + } + for _, tt := range tests { + v := reflect.ValueOf(tt.value).Type() + value := reflect.New(v) + err := (codec{}).Unmarshal([]byte(tt.data), value.Interface()) + if err != nil { + t.Fatalf("(codec{}).Unmarshal should not return err") + } + } + spec := struct { + A string + B map[string]any + }{A: "a"} + err := (codec{}).Unmarshal([]byte("v: hi"), &spec.B) + if err != nil { + t.Fatalf("(codec{}).Unmarshal should not return err") + } +} + +func TestCodec_Marshal(t *testing.T) { + value := map[string]string{"v": "hi"} + got, err := (codec{}).Marshal(value) + if err != nil { + t.Fatalf("should not return err") + } + if string(got) != "v: hi\n" { + t.Fatalf("want \"v: hi\n\" return \"%s\"", string(got)) + } +} diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..d68ea15 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,152 @@ +package errors + +import ( + "errors" + "fmt" + + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/status" + + httpstatus "github.com/go-kratos/kratos/v3/transport/http/status" +) + +const ( + // UnknownCode is unknown code for error info. + UnknownCode = 500 + // UnknownReason is unknown reason for error info. + UnknownReason = "" + // SupportPackageIsVersion1 this constant should not be referenced by any other code. + SupportPackageIsVersion1 = true +) + +// Error is a status error. +type Error struct { + Status + cause error +} + +func (e *Error) Error() string { + return fmt.Sprintf("error: code = %d reason = %s message = %s metadata = %v cause = %v", e.Code, e.Reason, e.Message, e.Metadata, e.cause) +} + +// Unwrap provides compatibility for Go 1.13 error chains. +func (e *Error) Unwrap() error { return e.cause } + +// Is matches each error in the chain with the target value. +func (e *Error) Is(err error) bool { + if se := new(Error); errors.As(err, &se) { + return se.Code == e.Code && se.Reason == e.Reason + } + return false +} + +// WithCause with the underlying cause of the error. +func (e *Error) WithCause(cause error) *Error { + err := Clone(e) + err.cause = cause + return err +} + +// WithMetadata with an MD formed by the mapping of key, value. +func (e *Error) WithMetadata(md map[string]string) *Error { + err := Clone(e) + err.Metadata = md + return err +} + +// GRPCStatus returns the Status represented by se. +func (e *Error) GRPCStatus() *status.Status { + s, _ := status.New(httpstatus.ToGRPCCode(int(e.Code)), e.Message). + WithDetails(&errdetails.ErrorInfo{ + Reason: e.Reason, + Metadata: e.Metadata, + }) + return s +} + +// New returns an error object for the code, message. +func New(code int, reason, message string) *Error { + return &Error{ + Status: Status{ + Code: int32(code), + Message: message, + Reason: reason, + }, + } +} + +// Newf New(code fmt.Sprintf(format, a...)) +func Newf(code int, reason, format string, a ...any) *Error { + return New(code, reason, fmt.Sprintf(format, a...)) +} + +// Errorf returns an error object for the code, message and error info. +func Errorf(code int, reason, format string, a ...any) error { + return New(code, reason, fmt.Sprintf(format, a...)) +} + +// Code returns the http code for an error. +// It supports wrapped errors. +func Code(err error) int { + if err == nil { + return 200 //nolint:mnd + } + return int(FromError(err).Code) +} + +// Reason returns the reason for a particular error. +// It supports wrapped errors. +func Reason(err error) string { + if err == nil { + return UnknownReason + } + return FromError(err).Reason +} + +// Clone deep clone error to a new error. +func Clone(err *Error) *Error { + if err == nil { + return nil + } + metadata := make(map[string]string, len(err.Metadata)) + for k, v := range err.Metadata { + metadata[k] = v + } + return &Error{ + cause: err.cause, + Status: Status{ + Code: err.Code, + Reason: err.Reason, + Message: err.Message, + Metadata: metadata, + }, + } +} + +// FromError try to convert an error to *Error. +// It supports wrapped errors. +func FromError(err error) *Error { + if err == nil { + return nil + } + if se := new(Error); errors.As(err, &se) { + return se + } + gs, ok := status.FromError(err) + if !ok { + return New(UnknownCode, UnknownReason, err.Error()) + } + ret := New( + httpstatus.FromGRPCCode(gs.Code()), + UnknownReason, + gs.Message(), + ) + for _, detail := range gs.Details() { + switch d := detail.(type) { + case *errdetails.ErrorInfo: + ret.Reason = d.Reason + return ret.WithMetadata(d.Metadata) + } + } + return ret +} diff --git a/errors/errors.pb.go b/errors/errors.pb.go new file mode 100644 index 0000000..8aa5bab --- /dev/null +++ b/errors/errors.pb.go @@ -0,0 +1,194 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: errors/errors.proto + +package errors + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + descriptorpb "google.golang.org/protobuf/types/descriptorpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Status struct { + state protoimpl.MessageState `protogen:"open.v1"` + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Status) Reset() { + *x = Status{} + mi := &file_errors_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_errors_errors_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_errors_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *Status) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Status) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *Status) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Status) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +var file_errors_errors_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*descriptorpb.EnumOptions)(nil), + ExtensionType: (*int32)(nil), + Field: 1108, + Name: "errors.default_code", + Tag: "varint,1108,opt,name=default_code", + Filename: "errors/errors.proto", + }, + { + ExtendedType: (*descriptorpb.EnumValueOptions)(nil), + ExtensionType: (*int32)(nil), + Field: 1109, + Name: "errors.code", + Tag: "varint,1109,opt,name=code", + Filename: "errors/errors.proto", + }, +} + +// Extension fields to descriptorpb.EnumOptions. +var ( + // optional int32 default_code = 1108; + E_DefaultCode = &file_errors_errors_proto_extTypes[0] +) + +// Extension fields to descriptorpb.EnumValueOptions. +var ( + // optional int32 code = 1109; + E_Code = &file_errors_errors_proto_extTypes[1] +) + +var File_errors_errors_proto protoreflect.FileDescriptor + +const file_errors_errors_proto_rawDesc = "" + + "\n" + + "\x13errors/errors.proto\x12\x06errors\x1a google/protobuf/descriptor.proto\"\xc5\x01\n" + + "\x06Status\x12\x12\n" + + "\x04code\x18\x01 \x01(\x05R\x04code\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\x128\n" + + "\bmetadata\x18\x04 \x03(\v2\x1c.errors.Status.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01:@\n" + + "\fdefault_code\x12\x1c.google.protobuf.EnumOptions\x18\xd4\b \x01(\x05R\vdefaultCode:6\n" + + "\x04code\x12!.google.protobuf.EnumValueOptions\x18\xd5\b \x01(\x05R\x04codeBY\n" + + "\x18com.github.kratos.errorsP\x01Z,github.com/go-kratos/kratos/v3/errors;errors\xa2\x02\fKratosErrorsb\x06proto3" + +var ( + file_errors_errors_proto_rawDescOnce sync.Once + file_errors_errors_proto_rawDescData []byte +) + +func file_errors_errors_proto_rawDescGZIP() []byte { + file_errors_errors_proto_rawDescOnce.Do(func() { + file_errors_errors_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_errors_errors_proto_rawDesc), len(file_errors_errors_proto_rawDesc))) + }) + return file_errors_errors_proto_rawDescData +} + +var file_errors_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_errors_errors_proto_goTypes = []any{ + (*Status)(nil), // 0: errors.Status + nil, // 1: errors.Status.MetadataEntry + (*descriptorpb.EnumOptions)(nil), // 2: google.protobuf.EnumOptions + (*descriptorpb.EnumValueOptions)(nil), // 3: google.protobuf.EnumValueOptions +} +var file_errors_errors_proto_depIdxs = []int32{ + 1, // 0: errors.Status.metadata:type_name -> errors.Status.MetadataEntry + 2, // 1: errors.default_code:extendee -> google.protobuf.EnumOptions + 3, // 2: errors.code:extendee -> google.protobuf.EnumValueOptions + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 1, // [1:3] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_errors_errors_proto_init() } +func file_errors_errors_proto_init() { + if File_errors_errors_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_errors_errors_proto_rawDesc), len(file_errors_errors_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 2, + NumServices: 0, + }, + GoTypes: file_errors_errors_proto_goTypes, + DependencyIndexes: file_errors_errors_proto_depIdxs, + MessageInfos: file_errors_errors_proto_msgTypes, + ExtensionInfos: file_errors_errors_proto_extTypes, + }.Build() + File_errors_errors_proto = out.File + file_errors_errors_proto_goTypes = nil + file_errors_errors_proto_depIdxs = nil +} diff --git a/errors/errors.proto b/errors/errors.proto new file mode 100644 index 0000000..4860953 --- /dev/null +++ b/errors/errors.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package errors; + +option go_package = "github.com/go-kratos/kratos/v3/errors;errors"; +option java_multiple_files = true; +option java_package = "com.github.kratos.errors"; +option objc_class_prefix = "KratosErrors"; + +import "google/protobuf/descriptor.proto"; + +message Status { + int32 code = 1; + string reason = 2; + string message = 3; + map metadata = 4; +}; + +extend google.protobuf.EnumOptions { + int32 default_code = 1108; +} + +extend google.protobuf.EnumValueOptions { + int32 code = 1109; +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100644 index 0000000..c5f4d42 --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,149 @@ +package errors + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type TestError struct{ message string } + +func (e *TestError) Error() string { return e.message } + +func TestErrors(t *testing.T) { + var base *Error + err := Newf(http.StatusBadRequest, "reason", "message") + err2 := Newf(http.StatusBadRequest, "reason", "message") + err3 := err.WithMetadata(map[string]string{ + "foo": "bar", + }) + werr := fmt.Errorf("wrap %w", err) + + if errors.Is(err, new(Error)) { + t.Errorf("should not be equal: %v", err) + } + if !errors.Is(werr, err) { + t.Errorf("should be equal: %v", err) + } + if !errors.Is(werr, err2) { + t.Errorf("should be equal: %v", err) + } + + if !errors.As(err, &base) { + t.Errorf("should be matches: %v", err) + } + if !IsBadRequest(err) { + t.Errorf("should be matches: %v", err) + } + + if reason := Reason(err); reason != err3.Reason { + t.Errorf("got %s want: %s", reason, err) + } + + if err3.Metadata["foo"] != "bar" { + t.Error("not expected metadata") + } + + gs := err.GRPCStatus() + se := FromError(gs.Err()) + if se.Reason != "reason" { + t.Errorf("got %+v want %+v", se, err) + } + + gs2 := status.New(codes.InvalidArgument, "bad request") + se2 := FromError(gs2.Err()) + // codes.InvalidArgument should convert to http.StatusBadRequest + if se2.Code != http.StatusBadRequest { + t.Errorf("convert code err, got %d want %d", UnknownCode, http.StatusBadRequest) + } + if FromError(nil) != nil { + t.Errorf("FromError(nil) should be nil") + } + e := FromError(errors.New("test")) + if !reflect.DeepEqual(e.Code, int32(UnknownCode)) { + t.Errorf("no expect value: %v, but got: %v", e.Code, int32(UnknownCode)) + } +} + +func TestIs(t *testing.T) { + tests := []struct { + name string + e *Error + err error + want bool + }{ + { + name: "true", + e: New(404, "test", ""), + err: New(http.StatusNotFound, "test", ""), + want: true, + }, + { + name: "false", + e: New(0, "test", ""), + err: errors.New("test"), + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if ok := tt.e.Is(tt.err); ok != tt.want { + t.Errorf("Error.Error() = %v, want %v", ok, tt.want) + } + }) + } +} + +func TestCause(t *testing.T) { + testError := &TestError{message: "test"} + err := BadRequest("foo", "bar").WithCause(testError) + if !errors.Is(err, testError) { + t.Fatalf("want %v but got %v", testError, err) + } + if te := new(TestError); errors.As(err, &te) { + if te.message != testError.message { + t.Fatalf("want %s but got %s", testError.message, te.message) + } + } +} + +func TestOther(t *testing.T) { + err := Errorf(10001, "test code 10001", "message") + // Code + if !reflect.DeepEqual(Code(nil), 200) { + t.Errorf("Code(nil) = %v, want %v", Code(nil), 200) + } + if !reflect.DeepEqual(Code(errors.New("test")), UnknownCode) { + t.Errorf(`Code(errors.New("test")) = %v, want %v`, Code(nil), 200) + } + if !reflect.DeepEqual(Code(err), 10001) { + t.Errorf(`Code(err) = %v, want %v`, Code(err), 10001) + } + // Reason + if !reflect.DeepEqual(Reason(nil), UnknownReason) { + t.Errorf(`Reason(nil) = %v, want %v`, Reason(nil), UnknownReason) + } + if !reflect.DeepEqual(Reason(errors.New("test")), UnknownReason) { + t.Errorf(`Reason(errors.New("test")) = %v, want %v`, Reason(nil), UnknownReason) + } + if !reflect.DeepEqual(Reason(err), "test code 10001") { + t.Errorf(`Reason(err) = %v, want %v`, Reason(err), "test code 10001") + } + // Clone + err400 := Newf(http.StatusBadRequest, "BAD_REQUEST", "param invalid") + err400.Metadata = map[string]string{ + "key1": "val1", + "key2": "val2", + } + if cerr := Clone(err400); cerr == nil || cerr.Error() != err400.Error() { + t.Errorf("Clone(err) = %v, want %v", Clone(err400), err400) + } + if cerr := Clone(nil); cerr != nil { + t.Errorf("Clone(nil) = %v, want %v", Clone(err400), err400) + } +} diff --git a/errors/types.go b/errors/types.go new file mode 100644 index 0000000..6afb66b --- /dev/null +++ b/errors/types.go @@ -0,0 +1,112 @@ +// nolint:mnd +package errors + +// BadRequest new BadRequest error that is mapped to a 400 response. +func BadRequest(reason, message string) *Error { + return New(400, reason, message) +} + +// IsBadRequest determines if err is an error which indicates a BadRequest error. +// It supports wrapped errors. +func IsBadRequest(err error) bool { + return Code(err) == 400 +} + +// Unauthorized new Unauthorized error that is mapped to a 401 response. +func Unauthorized(reason, message string) *Error { + return New(401, reason, message) +} + +// IsUnauthorized determines if err is an error which indicates an Unauthorized error. +// It supports wrapped errors. +func IsUnauthorized(err error) bool { + return Code(err) == 401 +} + +// Forbidden new Forbidden error that is mapped to a 403 response. +func Forbidden(reason, message string) *Error { + return New(403, reason, message) +} + +// IsForbidden determines if err is an error which indicates a Forbidden error. +// It supports wrapped errors. +func IsForbidden(err error) bool { + return Code(err) == 403 +} + +// NotFound new NotFound error that is mapped to a 404 response. +func NotFound(reason, message string) *Error { + return New(404, reason, message) +} + +// IsNotFound determines if err is an error which indicates an NotFound error. +// It supports wrapped errors. +func IsNotFound(err error) bool { + return Code(err) == 404 +} + +// Conflict new Conflict error that is mapped to a 409 response. +func Conflict(reason, message string) *Error { + return New(409, reason, message) +} + +// IsConflict determines if err is an error which indicates a Conflict error. +// It supports wrapped errors. +func IsConflict(err error) bool { + return Code(err) == 409 +} + +// TooManyRequests new TooManyRequests error that is mapped to an HTTP 429 response. +func TooManyRequests(reason, message string) *Error { + return New(429, reason, message) +} + +// IsTooManyRequests determines if err is an error which indicates a TooManyRequests error. +// It supports wrapped errors. +func IsTooManyRequests(err error) bool { + return Code(err) == 429 +} + +// ClientClosed new ClientClosed error that is mapped to an HTTP 499 response. +func ClientClosed(reason, message string) *Error { + return New(499, reason, message) +} + +// IsClientClosed determines if err is an error which indicates a IsClientClosed error. +// It supports wrapped errors. +func IsClientClosed(err error) bool { + return Code(err) == 499 +} + +// InternalServer new InternalServer error that is mapped to a 500 response. +func InternalServer(reason, message string) *Error { + return New(500, reason, message) +} + +// IsInternalServer determines if err is an error which indicates an Internal error. +// It supports wrapped errors. +func IsInternalServer(err error) bool { + return Code(err) == 500 +} + +// ServiceUnavailable new ServiceUnavailable error that is mapped to an HTTP 503 response. +func ServiceUnavailable(reason, message string) *Error { + return New(503, reason, message) +} + +// IsServiceUnavailable determines if err is an error which indicates an Unavailable error. +// It supports wrapped errors. +func IsServiceUnavailable(err error) bool { + return Code(err) == 503 +} + +// GatewayTimeout new GatewayTimeout error that is mapped to an HTTP 504 response. +func GatewayTimeout(reason, message string) *Error { + return New(504, reason, message) +} + +// IsGatewayTimeout determines if err is an error which indicates a GatewayTimeout error. +// It supports wrapped errors. +func IsGatewayTimeout(err error) bool { + return Code(err) == 504 +} diff --git a/errors/types_test.go b/errors/types_test.go new file mode 100644 index 0000000..e45c52a --- /dev/null +++ b/errors/types_test.go @@ -0,0 +1,38 @@ +package errors + +import ( + "testing" +) + +func TestTypes(t *testing.T) { + var ( + input = []error{ + BadRequest("reason_400", "message_400"), + Unauthorized("reason_401", "message_401"), + Forbidden("reason_403", "message_403"), + NotFound("reason_404", "message_404"), + Conflict("reason_409", "message_409"), + InternalServer("reason_500", "message_500"), + ServiceUnavailable("reason_503", "message_503"), + GatewayTimeout("reason_504", "message_504"), + ClientClosed("reason_499", "message_499"), + } + output = []func(error) bool{ + IsBadRequest, + IsUnauthorized, + IsForbidden, + IsNotFound, + IsConflict, + IsInternalServer, + IsServiceUnavailable, + IsGatewayTimeout, + IsClientClosed, + } + ) + + for i, in := range input { + if !output[i](in) { + t.Errorf("not expect: %v", in) + } + } +} diff --git a/errors/wrap.go b/errors/wrap.go new file mode 100644 index 0000000..bd9d125 --- /dev/null +++ b/errors/wrap.go @@ -0,0 +1,68 @@ +package errors + +import ( + stderrors "errors" +) + +// ErrUnsupported indicates that a requested operation cannot be performed, +// because it is unsupported. For example, a call to os.Link when using a +// file system that does not support hard links. +// +// Functions and methods should not return this error but should instead +// return an error including appropriate context that satisfies +// +// errors.Is(err, errors.ErrUnsupported) +// +// either by directly wrapping ErrUnsupported or by implementing an Is method. +// +// Functions and methods should document the cases in which an error +// wrapping this will be returned. +var ErrUnsupported = stderrors.ErrUnsupported + +// Is reports whether any error in err's chain matches target. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +func Is(err, target error) bool { return stderrors.Is(err, target) } + +// As finds the first error in err's chain that matches target, and if so, sets +// target to that error value and returns true. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error matches target if the error's concrete value is assignable to the value +// pointed to by target, or if the error has a method As(interface{}) bool such that +// As(target) returns true. In the latter case, the As method is responsible for +// setting target. +// +// As will panic if target is not a non-nil pointer to either a type that implements +// error, or to any interface type. As returns false if err is nil. +func As(err error, target any) bool { return stderrors.As(err, target) } + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + return stderrors.Unwrap(err) +} + +// Join returns an error that wraps the given errors. The returned error has a +// method Unwrap() []error that returns the given errors in order. +// +// Join returns nil if errs contains no non-nil error values. If errs contains a +// single non-nil error value, Join returns that error. If errs contains multiple +// non-nil error values, Join returns an error that formats as the concatenation +// of the format of the non-nil error values, separated by "; ". The returned +// error's Unwrap method returns a slice of the non-nil error values. +// +// Join is designed for use in situations where multiple errors may be returned, +// such as when processing a list of items and collecting errors from each item. +// It allows you to combine those errors into a single error value that can be +// returned to the caller. +func Join(errs ...error) error { + return stderrors.Join(errs...) +} diff --git a/errors/wrap_test.go b/errors/wrap_test.go new file mode 100644 index 0000000..5a2e76e --- /dev/null +++ b/errors/wrap_test.go @@ -0,0 +1,27 @@ +package errors + +import ( + "fmt" + "testing" +) + +type mockErr struct{} + +func (*mockErr) Error() string { + return "mock error" +} + +func TestWarp(t *testing.T) { + var err error = &mockErr{} + err2 := fmt.Errorf("wrap %w", err) + if err != Unwrap(err2) { + t.Errorf("got %v want: %v", err, Unwrap(err2)) + } + if !Is(err2, err) { + t.Errorf("Is(err2, err) got %v want: %v", Is(err2, err), true) + } + err3 := &mockErr{} + if !As(err2, &err3) { + t.Errorf("As(err2, &err3) got %v want: %v", As(err2, &err3), true) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b13d5e9 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/go-kratos/kratos/v3 + +go 1.25.0 + +require ( + github.com/fsnotify/fsnotify v1.10.1 + github.com/go-playground/form/v4 v4.3.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 + golang.org/x/sync v0.20.0 + google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + cel.dev/expr v0.25.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4961780 --- /dev/null +++ b/go.sum @@ -0,0 +1,80 @@ +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc= +google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= +google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/.lintcheck_failures b/hack/.lintcheck_failures new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/hack/.lintcheck_failures @@ -0,0 +1 @@ + diff --git a/hack/.test_ignored_files b/hack/.test_ignored_files new file mode 100644 index 0000000..c33d3f4 --- /dev/null +++ b/hack/.test_ignored_files @@ -0,0 +1,8 @@ +./examples +./cmd/protoc-gen-go-errors +./cmd/protoc-gen-go-http +./cmd/kratos +./contrib/config/kubernetes +./contrib/registry/kubernetes +./contrib/registry/zookeeper +./contrib/registry/eureka diff --git a/hack/resolve-modules.sh b/hack/resolve-modules.sh new file mode 100755 index 0000000..84101c9 --- /dev/null +++ b/hack/resolve-modules.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# This is used by the linter action. +# Recursively finds all directories with a go.mod file and creates +# a GitHub Actions JSON output option. + +set -o errexit + +echo "Resolving modules in $(pwd)" + +KRATOS_HOME=$( + cd "$(dirname "${BASH_SOURCE[0]}")" && + cd .. && + pwd +) + +source "${KRATOS_HOME}/hack/util.sh" + +FAILURE_FILE=${KRATOS_HOME}/hack/.lintcheck_failures + +all_modules=$(util::find_modules) +failing_modules=() +while IFS='' read -r line; do failing_modules+=("$line"); done < <(cat "$FAILURE_FILE") + +echo "Ignored failing modules:" +echo "${failing_modules[*]}" +echo + +PATHS="" + +for mod in $all_modules; do + util::array_contains "$mod" "${failing_modules[*]}" && in_failing=$? || in_failing=$? + if [[ "$in_failing" -ne "0" ]]; then + PATHS+=$(printf '{"workdir":"%s"},' ${mod}) + fi +done + +echo "::set-output name=matrix::{\"include\":[${PATHS%?}]}" diff --git a/hack/tools.sh b/hack/tools.sh new file mode 100755 index 0000000..b0faef7 --- /dev/null +++ b/hack/tools.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# This is a tools shell script +# used by Makefile commands + +set -o errexit +set -o nounset +set -o pipefail + +GO111MODULE=on +KRATOS_HOME=$( + cd "$(dirname "${BASH_SOURCE[0]}")" && + cd .. && + pwd +) + +source "${KRATOS_HOME}/hack/util.sh" + +LINTER=${KRATOS_HOME}/bin/golangci-lint +LINTER_CONFIG=${KRATOS_HOME}/.golangci.yml +FAILURE_FILE=${KRATOS_HOME}/hack/.lintcheck_failures +IGNORED_FILE=${KRATOS_HOME}/hack/.test_ignored_files + +all_modules=$(util::find_modules) +failing_modules=() +while IFS='' read -r line; do failing_modules+=("$line"); done < <(cat "$FAILURE_FILE") +ignored_modules=() +while IFS='' read -r line; do ignored_modules+=("$line"); done < <(cat "$IGNORED_FILE") + +# functions +# lint all mod +function lint() { + for mod in $all_modules; do + local in_failing + util::array_contains "$mod" "${failing_modules[*]}" && in_failing=$? || in_failing=$? + if [[ "$in_failing" -ne "0" ]]; then + pushd "$mod" >/dev/null && + echo "golangci lint $(sed -n 1p go.mod | cut -d ' ' -f2)" && + eval "${LINTER} run --timeout=5m --config=${LINTER_CONFIG}" + popd >/dev/null || exit + fi + done +} + +# test all mod +function test() { + for mod in $all_modules; do + local in_failing + util::array_contains "$mod" "${ignored_modules[*]}" && in_failing=$? || in_failing=$? + if [[ "$in_failing" -ne "0" ]]; then + pushd "$mod" >/dev/null && + echo "go test $(sed -n 1p go.mod | cut -d ' ' -f2)" && + go test -race ./... + popd >/dev/null || exit + fi + done +} + +function test_coverage() { + echo "" >coverage.out + local base + base=$(pwd) + for mod in $all_modules; do + local in_failing + util::array_contains "$mod" "${ignored_modules[*]}" && in_failing=$? || in_failing=$? + if [[ "$in_failing" -ne "0" ]]; then + pushd "$mod" >/dev/null && + echo "go test $(sed -n 1p go.mod | cut -d ' ' -f2)" && + go test -race -coverprofile=profile.out -covermode=atomic ./... + if [ -f profile.out ]; then + cat profile.out >>"${base}/coverage.out" + rm profile.out + fi + popd >/dev/null || exit + fi + done +} + +# try to fix all mod with golangci-lint +function fix() { + for mod in $all_modules; do + local in_failing + util::array_contains "$mod" "${failing_modules[*]}" && in_failing=$? || in_failing=$? + if [[ "$in_failing" -ne "0" ]]; then + pushd "$mod" >/dev/null && + echo "golangci fix $(sed -n 1p go.mod | cut -d ' ' -f2)" && + eval "${LINTER} run -v --fix --timeout=5m --config=${LINTER_CONFIG}" + popd >/dev/null || exit + fi + done +} + +function tidy() { + for mod in $all_modules; do + pushd "$mod" >/dev/null && + echo "go mod tidy $(sed -n 1p go.mod | cut -d ' ' -f2)" && + go mod tidy + popd >/dev/null || exit + done +} + +function help() { + echo "use: lint, test, test_coverage, fix, tidy" +} + +case $1 in +lint) + lint + ;; +test) + test + ;; +test_coverage) + test_coverage + ;; +tidy) + tidy + ;; +fix) + fix + ;; +*) + help + ;; +esac diff --git a/hack/util.sh b/hack/util.sh new file mode 100755 index 0000000..0cbe289 --- /dev/null +++ b/hack/util.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# This is a common util functions shell script + +# arguments: target, item1, item2, item3, ... +# returns 0 if target is in the given items, 1 otherwise. +function util::array_contains() { + local target="$1" + shift + local items="$*" + for item in ${items[*]}; do + if [[ "${item}" == "${target}" ]]; then + return 0 + fi + done + return 1 +} + +# find all go mod path +# returns an array contains mod path +function util::find_modules() { + find . -not \( \ + \( \ + -path './output' \ + -o -path './.git' \ + -o -path '*/third_party/*' \ + -o -path '*/vendor/*' \ + \) -prune \ + \) -name 'go.mod' -print0 | xargs -0 -I {} dirname {} +} diff --git a/internal/README.md b/internal/README.md new file mode 100644 index 0000000..a57dcfa --- /dev/null +++ b/internal/README.md @@ -0,0 +1 @@ +# internal diff --git a/internal/circuitbreaker/circuitbreaker.go b/internal/circuitbreaker/circuitbreaker.go new file mode 100644 index 0000000..b2b8846 --- /dev/null +++ b/internal/circuitbreaker/circuitbreaker.go @@ -0,0 +1,13 @@ +package circuitbreaker + +import "errors" + +// ErrNotAllowed is returned when the circuit breaker is open. +var ErrNotAllowed = errors.New("circuitbreaker: not allowed for circuit open") + +// CircuitBreaker is a circuit breaker. +type CircuitBreaker interface { + Allow() error + MarkSuccess() + MarkFailed() +} diff --git a/internal/circuitbreaker/sre.go b/internal/circuitbreaker/sre.go new file mode 100644 index 0000000..baf65e0 --- /dev/null +++ b/internal/circuitbreaker/sre.go @@ -0,0 +1,184 @@ +package circuitbreaker + +import ( + "math" + "math/rand" + "sync" + "sync/atomic" + "time" +) + +const ( + // StateOpen rejects requests according to the calculated drop ratio. + StateOpen int32 = iota + // StateClosed allows requests while the rolling failure ratio is healthy. + StateClosed +) + +// Option configures the SRE circuit breaker. +type Option func(*options) + +type options struct { + failureRatio float64 + request int64 + bucket int + window time.Duration +} + +// WithFailureRatio sets the failure ratio threshold that starts rejection. +func WithFailureRatio(ratio float64) Option { + return func(o *options) { + o.failureRatio = ratio + } +} + +// WithRequest sets the minimum request count before rejection starts. +func WithRequest(r int64) Option { + return func(o *options) { + o.request = r + } +} + +// WithWindow sets the rolling statistical window. +func WithWindow(d time.Duration) Option { + return func(o *options) { + o.window = d + } +} + +// WithBucket sets the number of buckets in the rolling window. +func WithBucket(b int) Option { + return func(o *options) { + o.bucket = b + } +} + +// Breaker is an SRE-style circuit breaker. +type Breaker struct { + stat *rollingCounter + + random func() float64 + + k float64 + request int64 + state int32 +} + +// NewBreaker returns an SRE circuit breaker. +func NewBreaker(opts ...Option) CircuitBreaker { + opt := options{ + failureRatio: 0.5, + request: 20, + bucket: 10, + window: 3 * time.Second, + } + for _, o := range opts { + o(&opt) + } + if opt.failureRatio < 0 || opt.failureRatio >= 1 { + opt.failureRatio = 0.5 + } + if opt.request < 1 { + opt.request = 1 + } + if opt.bucket < 1 { + opt.bucket = 1 + } + if opt.window <= 0 { + opt.window = 3 * time.Second + } + bucketDuration := opt.window / time.Duration(opt.bucket) + if bucketDuration <= 0 { + bucketDuration = opt.window + } + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + var randMu sync.Mutex + return &Breaker{ + stat: newRollingCounter(opt.bucket, bucketDuration), + random: func() float64 { randMu.Lock(); defer randMu.Unlock(); return rnd.Float64() }, + request: opt.request, + k: 1 / (1 - opt.failureRatio), + state: StateClosed, + } +} + +// Allow reports whether the request can pass the breaker. +func (b *Breaker) Allow() error { + successes, total := b.stat.summary() + requests := b.k * float64(successes) + if total < b.request || float64(total) < requests { + atomic.CompareAndSwapInt32(&b.state, StateOpen, StateClosed) + return nil + } + atomic.CompareAndSwapInt32(&b.state, StateClosed, StateOpen) + dropRatio := math.Max(0, (float64(total)-requests)/float64(total+1)) + if b.random() < dropRatio { + return ErrNotAllowed + } + return nil +} + +// MarkSuccess records a successful request. +func (b *Breaker) MarkSuccess() { + b.stat.add(1) +} + +// MarkFailed records a failed request. +func (b *Breaker) MarkFailed() { + b.stat.add(0) +} + +type rollingCounter struct { + mu sync.Mutex + buckets []counterBucket + bucketDuration time.Duration +} + +type counterBucket struct { + slot int64 + success int64 + total int64 +} + +func newRollingCounter(size int, bucketDuration time.Duration) *rollingCounter { + return &rollingCounter{ + buckets: make([]counterBucket, size), + bucketDuration: bucketDuration, + } +} + +func (r *rollingCounter) add(success int64) { + slot := r.currentSlot() + offset := int(slot % int64(len(r.buckets))) + + r.mu.Lock() + defer r.mu.Unlock() + bucket := &r.buckets[offset] + if bucket.slot != slot { + bucket.slot = slot + bucket.success = 0 + bucket.total = 0 + } + bucket.success += success + bucket.total++ +} + +func (r *rollingCounter) summary() (success int64, total int64) { + slot := r.currentSlot() + size := int64(len(r.buckets)) + + r.mu.Lock() + defer r.mu.Unlock() + for _, bucket := range r.buckets { + if bucket.total == 0 || slot-bucket.slot >= size || bucket.slot > slot { + continue + } + success += bucket.success + total += bucket.total + } + return success, total +} + +func (r *rollingCounter) currentSlot() int64 { + return time.Now().UnixNano() / int64(r.bucketDuration) +} diff --git a/internal/circuitbreaker/sre_test.go b/internal/circuitbreaker/sre_test.go new file mode 100644 index 0000000..6f536d3 --- /dev/null +++ b/internal/circuitbreaker/sre_test.go @@ -0,0 +1,72 @@ +package circuitbreaker + +import ( + "errors" + "testing" + "time" +) + +func TestBreakerAllowsBeforeRequestThreshold(t *testing.T) { + breaker := NewBreaker(WithRequest(3), WithWindow(time.Second), WithBucket(2)) + breaker.MarkFailed() + breaker.MarkFailed() + + if err := breaker.Allow(); err != nil { + t.Fatalf("Allow() error = %v, want nil", err) + } +} + +func TestBreakerRejectsAfterFailures(t *testing.T) { + breaker := NewBreaker(WithRequest(1), WithWindow(time.Second), WithBucket(2)).(*Breaker) + breaker.random = func() float64 { return 0 } + breaker.MarkFailed() + + if err := breaker.Allow(); !errors.Is(err, ErrNotAllowed) { + t.Fatalf("Allow() error = %v, want %v", err, ErrNotAllowed) + } +} + +func TestBreakerDefaultRequestThreshold(t *testing.T) { + breaker := NewBreaker(WithWindow(time.Second), WithBucket(2)).(*Breaker) + breaker.random = func() float64 { return 0 } + + for range 19 { + breaker.MarkFailed() + } + if err := breaker.Allow(); err != nil { + t.Fatalf("Allow() error = %v, want nil before default request threshold", err) + } + + breaker.MarkFailed() + if err := breaker.Allow(); !errors.Is(err, ErrNotAllowed) { + t.Fatalf("Allow() error = %v, want %v at default request threshold", err, ErrNotAllowed) + } +} + +func TestBreakerDefaultFailureRatio(t *testing.T) { + breaker := NewBreaker(WithRequest(20), WithWindow(time.Second), WithBucket(2)).(*Breaker) + breaker.random = func() float64 { return 0 } + + for range 10 { + breaker.MarkSuccess() + breaker.MarkFailed() + } + if err := breaker.Allow(); err != nil { + t.Fatalf("Allow() error = %v, want nil at default failure ratio threshold", err) + } + + breaker.MarkFailed() + if err := breaker.Allow(); !errors.Is(err, ErrNotAllowed) { + t.Fatalf("Allow() error = %v, want %v above default failure ratio threshold", err, ErrNotAllowed) + } +} + +func TestBreakerClosesAfterSuccesses(t *testing.T) { + breaker := NewBreaker(WithRequest(1), WithFailureRatio(0.5), WithWindow(time.Second), WithBucket(2)) + breaker.MarkSuccess() + breaker.MarkSuccess() + + if err := breaker.Allow(); err != nil { + t.Fatalf("Allow() error = %v, want nil", err) + } +} diff --git a/internal/context/context.go b/internal/context/context.go new file mode 100644 index 0000000..7c5392c --- /dev/null +++ b/internal/context/context.go @@ -0,0 +1,115 @@ +package context + +import ( + "context" + "sync" + "sync/atomic" + "time" +) + +type mergeCtx struct { + parent1, parent2 context.Context + + done chan struct{} + doneMark atomic.Bool + doneOnce sync.Once + doneErr error + + cancelCh chan struct{} + cancelOnce sync.Once +} + +// Merge merges two contexts into one. +func Merge(parent1, parent2 context.Context) (context.Context, context.CancelFunc) { + mc := &mergeCtx{ + parent1: parent1, + parent2: parent2, + done: make(chan struct{}), + cancelCh: make(chan struct{}), + } + select { + case <-parent1.Done(): + _ = mc.finish(parent1.Err()) + case <-parent2.Done(): + _ = mc.finish(parent2.Err()) + default: + go mc.wait() + } + return mc, mc.cancel +} + +func (mc *mergeCtx) finish(err error) error { + mc.doneOnce.Do(func() { + mc.doneErr = err + mc.doneMark.Store(true) + close(mc.done) + }) + return mc.doneErr +} + +func (mc *mergeCtx) wait() { + var err error + select { + case <-mc.parent1.Done(): + err = mc.parent1.Err() + case <-mc.parent2.Done(): + err = mc.parent2.Err() + case <-mc.cancelCh: + err = context.Canceled + } + _ = mc.finish(err) +} + +func (mc *mergeCtx) cancel() { + mc.cancelOnce.Do(func() { + close(mc.cancelCh) + }) +} + +// Done implements context.Context. +func (mc *mergeCtx) Done() <-chan struct{} { + return mc.done +} + +// Err implements context.Context. +func (mc *mergeCtx) Err() error { + if mc.doneMark.Load() { + return mc.doneErr + } + var err error + select { + case <-mc.parent1.Done(): + err = mc.parent1.Err() + case <-mc.parent2.Done(): + err = mc.parent2.Err() + case <-mc.cancelCh: + err = context.Canceled + default: + return nil + } + return mc.finish(err) +} + +// Deadline implements context.Context. +func (mc *mergeCtx) Deadline() (time.Time, bool) { + d1, ok1 := mc.parent1.Deadline() + d2, ok2 := mc.parent2.Deadline() + switch { + case !ok1: + return d2, ok2 + case !ok2: + return d1, ok1 + case d1.Before(d2): + return d1, true + default: + return d2, true + } +} + +// Value implements context.Context. +func (mc *mergeCtx) Value(key any) any { + if v := mc.parent1.Value(key); v != nil { + return v + } + return mc.parent2.Value(key) +} diff --git a/internal/context/context_test.go b/internal/context/context_test.go new file mode 100644 index 0000000..ca40ac1 --- /dev/null +++ b/internal/context/context_test.go @@ -0,0 +1,280 @@ +package context + +import ( + "context" + "errors" + "reflect" + "testing" + "time" +) + +func TestContext(t *testing.T) { + type ctxKey1 struct{} + type ctxKey2 struct{} + ctx1 := context.WithValue(context.Background(), ctxKey1{}, "https://github.com/go-kratos/") + ctx2 := context.WithValue(context.Background(), ctxKey2{}, "https://go-kratos.dev/") + + ctx, cancel := Merge(ctx1, ctx2) + defer cancel() + + got := ctx.Value(ctxKey1{}) + value1, ok := got.(string) + if !ok { + t.Errorf("expect %v, got %v", true, ok) + } + if !reflect.DeepEqual(value1, "https://github.com/go-kratos/") { + t.Errorf("expect %v, got %v", "https://github.com/go-kratos/", value1) + } + + got2 := ctx.Value(ctxKey2{}) + value2, ok := got2.(string) + if !ok { + t.Errorf("expect %v, got %v", true, ok) + } + if !reflect.DeepEqual("https://go-kratos.dev/", value2) { + t.Errorf("expect %v, got %v", "https://go-kratos.dev/", value2) + } + + t.Log(value1) + t.Log(value2) +} + +func TestMerge(t *testing.T) { + type ctxKey1 struct{} + type ctxKey2 struct{} + ctx, cancel := context.WithCancel(context.Background()) + cancel() + ctx1 := context.WithValue(context.Background(), ctxKey1{}, "https://github.com/go-kratos/") + ctx2 := context.WithValue(ctx, ctxKey2{}, "https://go-kratos.dev/") + + ctx, cancel = Merge(ctx1, ctx2) + defer cancel() + + got := ctx.Value(ctxKey1{}) + value1, ok := got.(string) + if !ok { + t.Errorf("expect %v, got %v", true, ok) + } + if !reflect.DeepEqual(value1, "https://github.com/go-kratos/") { + t.Errorf("expect %v, got %v", "https://github.com/go-kratos/", value1) + } + + got2 := ctx.Value(ctxKey2{}) + value2, ok := got2.(string) + if !ok { + t.Errorf("expect %v, got %v", true, ok) + } + if !reflect.DeepEqual(value2, "https://go-kratos.dev/") { + t.Errorf("expect %v, got %v", " https://go-kratos.dev/", value2) + } + + t.Log(ctx) +} + +func TestErr(t *testing.T) { + ctx1, cancel := context.WithTimeout(context.Background(), time.Microsecond) + defer cancel() + time.Sleep(time.Millisecond) + + ctx, cancel := Merge(ctx1, context.Background()) + defer cancel() + if !errors.Is(ctx.Err(), context.DeadlineExceeded) { + t.Errorf("expect %v, got %v", context.DeadlineExceeded, ctx.Err()) + } +} + +func TestDone(t *testing.T) { + ctx1, cancel := context.WithCancel(context.Background()) + defer cancel() + + ctx, cancel := Merge(ctx1, context.Background()) + go func() { + time.Sleep(time.Millisecond * 50) + cancel() + }() + + if <-ctx.Done() != struct{}{} { + t.Errorf("expect %v, got %v", struct{}{}, <-ctx.Done()) + } +} + +func TestFinish(t *testing.T) { + mc := &mergeCtx{ + parent1: context.Background(), + parent2: context.Background(), + done: make(chan struct{}), + cancelCh: make(chan struct{}), + } + err := mc.finish(context.DeadlineExceeded) + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("expect %v, got %v", context.DeadlineExceeded, err) + } + if done := mc.doneMark.Load(); done != true { + t.Errorf("expect %v, got %v", true, done) + } + if <-mc.done != struct{}{} { + t.Errorf("expect %v, got %v", struct{}{}, <-mc.done) + } +} + +func TestWait(t *testing.T) { + ctx1, cancel := context.WithCancel(context.Background()) + + mc := &mergeCtx{ + parent1: ctx1, + parent2: context.Background(), + done: make(chan struct{}), + cancelCh: make(chan struct{}), + } + go func() { + time.Sleep(time.Millisecond * 50) + cancel() + }() + + mc.wait() + t.Log(mc.doneErr) + if !errors.Is(mc.doneErr, context.Canceled) { + t.Errorf("expect %v, got %v", context.Canceled, mc.doneErr) + } + + ctx2, cancel2 := context.WithCancel(context.Background()) + + mc = &mergeCtx{ + parent1: ctx2, + parent2: context.Background(), + done: make(chan struct{}), + cancelCh: make(chan struct{}), + } + go func() { + time.Sleep(time.Millisecond * 50) + cancel2() + }() + + mc.wait() + t.Log(mc.doneErr) + if !errors.Is(mc.doneErr, context.Canceled) { + t.Errorf("expect %v, got %v", context.Canceled, mc.doneErr) + } +} + +func TestCancel(t *testing.T) { + mc := &mergeCtx{ + parent1: context.Background(), + parent2: context.Background(), + done: make(chan struct{}), + cancelCh: make(chan struct{}), + } + mc.cancel() + if <-mc.cancelCh != struct{}{} { + t.Errorf("expect %v, got %v", struct{}{}, <-mc.cancelCh) + } +} + +func Test_mergeCtx_Deadline(t *testing.T) { + type fields struct { + parent1Timeout time.Time + parent2Timeout time.Time + } + tests := []struct { + name string + fields fields + want1 bool + }{ + { + name: "parent1 not deadline", + fields: fields{time.Time{}, time.Now().Add(time.Second * 100)}, + want1: true, + }, + { + name: "parent2 not deadline", + fields: fields{time.Now().Add(time.Second * 100), time.Time{}}, + want1: true, + }, + { + name: " parent1 parent2 not deadline", + fields: fields{time.Time{}, time.Time{}}, + want1: false, + }, + { + name: " parent1 < parent2", + fields: fields{time.Now().Add(time.Second * 100), time.Now().Add(time.Second * 200)}, + want1: true, + }, + { + name: " parent1 > parent2", + fields: fields{time.Now().Add(time.Second * 100), time.Now().Add(time.Second * 50)}, + want1: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var parent1, parent2 context.Context + var cancel1, cancel2 context.CancelFunc + if reflect.DeepEqual(tt.fields.parent1Timeout, time.Time{}) { + parent1 = context.Background() + } else { + parent1, cancel1 = context.WithDeadline(context.Background(), tt.fields.parent1Timeout) + defer cancel1() + } + if reflect.DeepEqual(tt.fields.parent2Timeout, time.Time{}) { + parent2 = context.Background() + } else { + parent2, cancel2 = context.WithDeadline(context.Background(), tt.fields.parent2Timeout) + defer cancel2() + } + + mc := &mergeCtx{ + parent1: parent1, + parent2: parent2, + } + got, got1 := mc.Deadline() + t.Log(got) + if got1 != tt.want1 { + t.Errorf("Deadline() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func Test_Err2(t *testing.T) { + ctx1, cancel := context.WithCancel(context.Background()) + defer cancel() + time.Sleep(time.Millisecond) + + ctx, cancel := Merge(ctx1, context.Background()) + defer cancel() + + if ctx.Err() != nil { + t.Errorf("expect %v, got %v", nil, ctx.Err()) + } + + ctx1, cancel1 := context.WithCancel(context.Background()) + time.Sleep(time.Millisecond) + + ctx, cancel = Merge(ctx1, context.Background()) + defer cancel() + + cancel1() + + if !errors.Is(ctx.Err(), context.Canceled) { + t.Errorf("expect %v, got %v", context.Canceled, ctx.Err()) + } + + ctx1, cancel1 = context.WithCancel(context.Background()) + time.Sleep(time.Millisecond) + + ctx, cancel = Merge(context.Background(), ctx1) + defer cancel() + + cancel1() + + if !errors.Is(ctx.Err(), context.Canceled) { + t.Errorf("expect %v, got %v", context.Canceled, ctx.Err()) + } + + ctx, cancel = Merge(context.Background(), context.Background()) + cancel() + if !errors.Is(ctx.Err(), context.Canceled) { + t.Errorf("expect %v, got %v", context.Canceled, ctx.Err()) + } +} diff --git a/internal/endpoint/endpoint.go b/internal/endpoint/endpoint.go new file mode 100644 index 0000000..160b694 --- /dev/null +++ b/internal/endpoint/endpoint.go @@ -0,0 +1,34 @@ +package endpoint + +import ( + "net/url" +) + +// NewEndpoint new an Endpoint URL. +func NewEndpoint(scheme, host string) *url.URL { + return &url.URL{Scheme: scheme, Host: host} +} + +// ParseEndpoint parses an Endpoint URL. +func ParseEndpoint(endpoints []string, scheme string) (string, error) { + for _, e := range endpoints { + u, err := url.Parse(e) + if err != nil { + return "", err + } + + if u.Scheme == scheme { + return u.Host, nil + } + } + return "", nil +} + +// Scheme is the scheme of endpoint url. +// examples: scheme="http",isSecure=true get "https" +func Scheme(scheme string, isSecure bool) string { + if isSecure { + return scheme + "s" + } + return scheme +} diff --git a/internal/endpoint/endpoint_test.go b/internal/endpoint/endpoint_test.go new file mode 100644 index 0000000..c02878f --- /dev/null +++ b/internal/endpoint/endpoint_test.go @@ -0,0 +1,126 @@ +package endpoint + +import ( + "net/url" + "reflect" + "testing" +) + +func TestNewEndpoint(t *testing.T) { + type args struct { + scheme string + host string + } + tests := []struct { + name string + args args + want *url.URL + }{ + { + name: "https://github.com/go-kratos/kratos/", + args: args{"https", "github.com/go-kratos/kratos/"}, + want: &url.URL{Scheme: "https", Host: "github.com/go-kratos/kratos/"}, + }, + { + name: "https://go-kratos.dev/", + args: args{"https", "go-kratos.dev/"}, + want: &url.URL{Scheme: "https", Host: "go-kratos.dev/"}, + }, + { + name: "https://www.google.com/", + args: args{"https", "www.google.com/"}, + want: &url.URL{Scheme: "https", Host: "www.google.com/"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewEndpoint(tt.args.scheme, tt.args.host); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewEndpoint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseEndpoint(t *testing.T) { + type args struct { + endpoints []string + scheme string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "kratos", + args: args{endpoints: []string{"https://github.com/go-kratos/kratos"}, scheme: "https"}, + want: "github.com", + wantErr: false, + }, + { + name: "test", + args: args{endpoints: []string{"http://go-kratos.dev/"}, scheme: "https"}, + want: "", + wantErr: false, + }, + { + name: "localhost:8080", + args: args{endpoints: []string{"grpcs://localhost:8080/"}, scheme: "grpcs"}, + want: "localhost:8080", + wantErr: false, + }, + { + name: "localhost:8081", + args: args{endpoints: []string{"grpcs://localhost:8080/"}, scheme: "grpc"}, + want: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseEndpoint(tt.args.endpoints, tt.args.scheme) + if (err != nil) != tt.wantErr { + t.Errorf("ParseEndpoint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseEndpoint() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSchema(t *testing.T) { + tests := []struct { + schema string + secure bool + want string + }{ + { + schema: "http", + secure: true, + want: "https", + }, + { + schema: "http", + secure: false, + want: "http", + }, + { + schema: "grpc", + secure: true, + want: "grpcs", + }, + { + schema: "grpc", + secure: false, + want: "grpc", + }, + } + for _, tt := range tests { + if got := Scheme(tt.schema, tt.secure); got != tt.want { + t.Errorf("Schema() = %v, want %v", got, tt.want) + } + } +} diff --git a/internal/group/example_test.go b/internal/group/example_test.go new file mode 100644 index 0000000..edab426 --- /dev/null +++ b/internal/group/example_test.go @@ -0,0 +1,42 @@ +package group + +import "fmt" + +type Counter struct { + Value int +} + +func (c *Counter) Incr() { + c.Value++ +} + +func ExampleGroup_Get() { + group := NewGroup(func() any { + fmt.Println("Only Once") + return &Counter{} + }) + + // Create a new Counter + group.Get("pass").(*Counter).Incr() + + // Get the created Counter again. + group.Get("pass").(*Counter).Incr() + // Output: + // Only Once +} + +func ExampleGroup_Reset() { + group := NewGroup(func() any { + return &Counter{} + }) + + // Reset the new function and clear all created objects. + group.Reset(func() any { + fmt.Println("reset") + return &Counter{} + }) + + // Create a new Counter + group.Get("pass").(*Counter).Incr() + // Output:reset +} diff --git a/internal/group/group.go b/internal/group/group.go new file mode 100644 index 0000000..23fae95 --- /dev/null +++ b/internal/group/group.go @@ -0,0 +1,67 @@ +// Package group provides a sample lazy load container. +// The group only creating a new object not until the object is needed by user. +// And it will cache all the objects to reduce the creation of object. +package group + +import "sync" + +// Factory is a function that creates an object of type T. +type Factory[T any] func() T + +// Group is a lazy load container. +type Group[T any] struct { + factory func() T + vals map[string]T + sync.RWMutex +} + +// NewGroup news a group container. +func NewGroup[T any](factory Factory[T]) *Group[T] { + if factory == nil { + panic("container.group: can't assign a nil to the new function") + } + return &Group[T]{ + factory: factory, + vals: make(map[string]T), + } +} + +// Get gets the object by the given key. +func (g *Group[T]) Get(key string) T { + g.RLock() + v, ok := g.vals[key] + if ok { + g.RUnlock() + return v + } + g.RUnlock() + + // slow path for group don`t have specified key value + g.Lock() + defer g.Unlock() + v, ok = g.vals[key] + if ok { + return v + } + v = g.factory() + g.vals[key] = v + return v +} + +// Reset resets the new function and deletes all existing objects. +func (g *Group[T]) Reset(factory Factory[T]) { + if factory == nil { + panic("container.group: can't assign a nil to the new function") + } + g.Lock() + g.factory = factory + g.Unlock() + g.Clear() +} + +// Clear deletes all objects. +func (g *Group[T]) Clear() { + g.Lock() + g.vals = make(map[string]T) + g.Unlock() +} diff --git a/internal/group/group_test.go b/internal/group/group_test.go new file mode 100644 index 0000000..9de4ffb --- /dev/null +++ b/internal/group/group_test.go @@ -0,0 +1,79 @@ +package group + +import ( + "reflect" + "testing" +) + +func TestGroupGet(t *testing.T) { + count := 0 + g := NewGroup[int](func() int { + count++ + return count + }) + v := g.Get("key_0") + if !reflect.DeepEqual(v, 1) { + t.Errorf("expect 1, actual %v", v) + } + + v = g.Get("key_1") + if !reflect.DeepEqual(v, 2) { + t.Errorf("expect 2, actual %v", v) + } + + v = g.Get("key_0") + if !reflect.DeepEqual(v, 1) { + t.Errorf("expect 1, actual %v", v) + } + if !reflect.DeepEqual(count, 2) { + t.Errorf("expect count 2, actual %v", count) + } +} + +func TestGroupReset(t *testing.T) { + g := NewGroup(func() int { + return 1 + }) + g.Get("key") + call := false + g.Reset(func() int { + call = true + return 1 + }) + + length := 0 + for range g.vals { + length++ + } + if !reflect.DeepEqual(length, 0) { + t.Errorf("expect length 0, actual %v", length) + } + + g.Get("key") + if !reflect.DeepEqual(call, true) { + t.Errorf("expect call true, actual %v", call) + } +} + +func TestGroupClear(t *testing.T) { + g := NewGroup(func() int { + return 1 + }) + g.Get("key") + length := 0 + for range g.vals { + length++ + } + if !reflect.DeepEqual(length, 1) { + t.Errorf("expect length 1, actual %v", length) + } + + g.Clear() + length = 0 + for range g.vals { + length++ + } + if !reflect.DeepEqual(length, 0) { + t.Errorf("expect length 0, actual %v", length) + } +} diff --git a/internal/host/host.go b/internal/host/host.go new file mode 100644 index 0000000..c527089 --- /dev/null +++ b/internal/host/host.go @@ -0,0 +1,91 @@ +package host + +import ( + "fmt" + "net" + "strconv" +) + +// ExtractHostPort from address +func ExtractHostPort(addr string) (host string, port uint64, err error) { + var ports string + host, ports, err = net.SplitHostPort(addr) + if err != nil { + return + } + port, err = strconv.ParseUint(ports, 10, 16) //nolint:mnd + return +} + +func isValidIP(addr string) bool { + ip := net.ParseIP(addr) + return ip.IsGlobalUnicast() && !ip.IsInterfaceLocalMulticast() +} + +// Port return a real port. +func Port(lis net.Listener) (int, bool) { + if addr, ok := lis.Addr().(*net.TCPAddr); ok { + return addr.Port, true + } + return 0, false +} + +// Extract returns a private addr and port. +func Extract(hostPort string, lis net.Listener) (string, error) { + addr, port, err := net.SplitHostPort(hostPort) + if err != nil && lis == nil { + return "", err + } + if lis != nil { + p, ok := Port(lis) + if !ok { + return "", fmt.Errorf("failed to extract port: %v", lis.Addr()) + } + port = strconv.Itoa(p) + } + if len(addr) > 0 && (addr != "0.0.0.0" && addr != "[::]" && addr != "::") { + return net.JoinHostPort(addr, port), nil + } + ifaces, err := net.Interfaces() + if err != nil { + return "", err + } + var ( + minIndex = 0 + ips = make([]net.IP, 0, 1) + ) + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + if iface.Index >= minIndex && len(ips) != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, rawAddr := range addrs { + var ip net.IP + switch addr := rawAddr.(type) { + case *net.IPAddr: + ip = addr.IP + case *net.IPNet: + ip = addr.IP + default: + continue + } + if isValidIP(ip.String()) { + minIndex = iface.Index + ips = append(ips, ip) + if ip.To4() != nil { + break + } + } + } + } + if len(ips) != 0 { + return net.JoinHostPort(ips[len(ips)-1].String(), port), nil + } + return "", nil +} diff --git a/internal/host/host_test.go b/internal/host/host_test.go new file mode 100644 index 0000000..8551875 --- /dev/null +++ b/internal/host/host_test.go @@ -0,0 +1,149 @@ +package host + +import ( + "net" + "reflect" + "testing" +) + +func TestValidIP(t *testing.T) { + tests := []struct { + addr string + expect bool + }{ + {"127.0.0.1", false}, + {"255.255.255.255", false}, + {"0.0.0.0", false}, + {"localhost", false}, + {"10.1.0.1", true}, + {"172.16.0.1", true}, + {"192.168.1.1", true}, + {"8.8.8.8", true}, + {"1.1.1.1", true}, + {"9.255.255.255", true}, + {"10.0.0.0", true}, + {"10.255.255.255", true}, + {"11.0.0.0", true}, + {"172.15.255.255", true}, + {"172.16.0.0", true}, + {"172.16.255.255", true}, + {"172.23.18.255", true}, + {"172.31.255.255", true}, + {"172.31.0.0", true}, + {"172.32.0.0", true}, + {"192.167.255.255", true}, + {"192.168.0.0", true}, + {"192.168.255.255", true}, + {"192.169.0.0", true}, + {"fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true}, + {"fc00::", true}, + {"fcff:1200:0:44::", true}, + {"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true}, + {"fe00::", true}, + } + for _, test := range tests { + t.Run(test.addr, func(t *testing.T) { + res := isValidIP(test.addr) + if res != test.expect { + t.Fatalf("expected %t got %t", test.expect, res) + } + }) + } +} + +func TestExtract(t *testing.T) { + tests := []struct { + addr string + expect string + }{ + {"127.0.0.1:80", "127.0.0.1:80"}, + {"10.0.0.1:80", "10.0.0.1:80"}, + {"172.16.0.1:80", "172.16.0.1:80"}, + {"192.168.1.1:80", "192.168.1.1:80"}, + {"0.0.0.0:80", ""}, + {"[::]:80", ""}, + {":80", ""}, + } + for _, test := range tests { + t.Run(test.addr, func(t *testing.T) { + res, err := Extract(test.addr, nil) + if err != nil { + t.Fatal(err) + } + if res != test.expect && (test.expect == "" && test.addr == test.expect) { + t.Fatalf("expected %s got %s", test.expect, res) + } + }) + } + lis, err := net.Listen("tcp", ":12345") + if err != nil { + t.Errorf("expected: %v got %v", nil, err) + } + res, err := Extract("", lis) + if err != nil { + t.Errorf("expected: %v got %v", nil, err) + } + expect, err := Extract(lis.Addr().String(), nil) + if err != nil { + t.Errorf("expected: %v got %v", nil, err) + } + if !reflect.DeepEqual(res, expect) { + t.Errorf("expected %s got %s", expect, res) + } +} + +func TestExtract2(t *testing.T) { + addr := "localhost:9001" + lis, err := net.Listen("tcp", addr) + if err != nil { + t.Errorf("expected: %v got %v", nil, err) + } + res, err := Extract(addr, lis) + if err != nil { + t.Errorf("expected: %v got %v", nil, err) + } + if !reflect.DeepEqual(res, "localhost:9001") { + t.Errorf("expected %s got %s", "localhost:9001", res) + } +} + +func TestPort(t *testing.T) { + lis, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal(err) + } + port, ok := Port(lis) + if !ok || port == 0 { + t.Fatalf("expected: %s got %d", lis.Addr().String(), port) + } +} + +func TestExtractHostPort(t *testing.T) { + host, port, err := ExtractHostPort("127.0.0.1:8000") + if err != nil { + t.Fatalf("expected: %v got %v", nil, err) + } + t.Logf("host port: %s, %d", host, port) + + host, port, err = ExtractHostPort("www.bilibili.com:80") + if err != nil { + t.Fatalf("expected: %v got %v", nil, err) + } + t.Logf("host port: %s, %d", host, port) + + host, port, err = ExtractHostPort("consul://2/33") + if err == nil { + t.Fatalf("expected: not nil got %v", nil) + } + t.Logf("host port: %s, %d", host, port) +} + +func TestIpIsUp(t *testing.T) { + interfaces, err := net.Interfaces() + if err != nil { + t.Fail() + } + for i := range interfaces { + println(interfaces[i].Name, interfaces[i].Flags&net.FlagUp) + } +} diff --git a/internal/httputil/http.go b/internal/httputil/http.go new file mode 100644 index 0000000..b89b931 --- /dev/null +++ b/internal/httputil/http.go @@ -0,0 +1,34 @@ +package httputil + +import ( + "strings" +) + +const ( + baseContentType = "application" +) + +// ContentType returns the content-type with base prefix. +func ContentType(subtype string) string { + return baseContentType + "/" + subtype +} + +// ContentSubtype returns the content-subtype for the given content-type. The +// given content-type must be a valid content-type that starts with +// but no content-subtype will be returned. +// according rfc7231. +// contentType is assumed to be lowercase already. +func ContentSubtype(contentType string) string { + left := strings.Index(contentType, "/") + if left == -1 { + return "" + } + right := strings.Index(contentType, ";") + if right == -1 { + right = len(contentType) + } + if right < left { + return "" + } + return contentType[left+1 : right] +} diff --git a/internal/httputil/http_test.go b/internal/httputil/http_test.go new file mode 100644 index 0000000..da252a9 --- /dev/null +++ b/internal/httputil/http_test.go @@ -0,0 +1,47 @@ +package httputil + +import ( + "testing" +) + +func TestContentSubtype(t *testing.T) { + tests := []struct { + contentType string + want string + }{ + {"text/html; charset=utf-8", "html"}, + {"multipart/form-data; boundary=something", "form-data"}, + {"application/json; charset=utf-8", "json"}, + {"application/json", "json"}, + {"application/xml", "xml"}, + {"text/xml", "xml"}, + {";text/xml", ""}, + {"application", ""}, + } + for _, test := range tests { + t.Run(test.contentType, func(t *testing.T) { + got := ContentSubtype(test.contentType) + if got != test.want { + t.Fatalf("want %v got %v", test.want, got) + } + }) + } +} + +func TestContentType(t *testing.T) { + tests := []struct { + name string + subtype string + want string + }{ + {"kratos", "kratos", "application/kratos"}, + {"json", "json", "application/json"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContentType(tt.subtype); got != tt.want { + t.Errorf("ContentType() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/matcher/middleware.go b/internal/matcher/middleware.go new file mode 100644 index 0000000..70eee58 --- /dev/null +++ b/internal/matcher/middleware.go @@ -0,0 +1,62 @@ +package matcher + +import ( + "sort" + "strings" + + "github.com/go-kratos/kratos/v3/middleware" +) + +// Matcher is a middleware matcher. +type Matcher interface { + Use(ms ...middleware.Middleware) + Add(selector string, ms ...middleware.Middleware) + Match(operation string) []middleware.Middleware +} + +// New new a middleware matcher. +func New() Matcher { + return &matcher{ + matches: make(map[string][]middleware.Middleware), + } +} + +type matcher struct { + prefix []string + defaults []middleware.Middleware + matches map[string][]middleware.Middleware +} + +func (m *matcher) Use(ms ...middleware.Middleware) { + m.defaults = ms +} + +func (m *matcher) Add(selector string, ms ...middleware.Middleware) { + if strings.HasSuffix(selector, "*") { + selector = strings.TrimSuffix(selector, "*") + m.prefix = append(m.prefix, selector) + // sort the prefix: + // - /foo/bar + // - /foo + sort.Slice(m.prefix, func(i, j int) bool { + return m.prefix[i] > m.prefix[j] + }) + } + m.matches[selector] = ms +} + +func (m *matcher) Match(operation string) []middleware.Middleware { + ms := make([]middleware.Middleware, 0, len(m.defaults)) + if len(m.defaults) > 0 { + ms = append(ms, m.defaults...) + } + if next, ok := m.matches[operation]; ok { + return append(ms, next...) + } + for _, prefix := range m.prefix { + if strings.HasPrefix(operation, prefix) { + return append(ms, m.matches[prefix]...) + } + } + return ms +} diff --git a/internal/matcher/middleware_test.go b/internal/matcher/middleware_test.go new file mode 100644 index 0000000..b122c9c --- /dev/null +++ b/internal/matcher/middleware_test.go @@ -0,0 +1,62 @@ +package matcher + +import ( + "context" + "testing" + + "github.com/go-kratos/kratos/v3/middleware" +) + +func logging(module string) middleware.Middleware { + return func(middleware.Handler) middleware.Handler { + return func(context.Context, any) (reply any, err error) { + return module, nil + } + } +} + +func equal(ms []middleware.Middleware, modules ...string) bool { + if len(ms) == 0 { + return false + } + for i, m := range ms { + x, _ := m(nil)(nil, nil) + if x != modules[i] { + return false + } + } + return true +} + +func TestMatcher(t *testing.T) { + m := New() + m.Use(logging("logging")) + m.Add("*", logging("*")) + m.Add("/foo/*", logging("foo/*")) + m.Add("/foo/bar/*", logging("foo/bar/*")) + m.Add("/foo/bar", logging("foo/bar")) + + if ms := m.Match("/"); len(ms) != 2 { + t.Fatal("not equal") + } else if !equal(ms, "logging", "*") { + t.Fatal("not equal") + } + + if ms := m.Match("/foo/xxx"); len(ms) != 2 { + t.Fatal("not equal") + } else if !equal(ms, "logging", "foo/*") { + t.Fatal("not equal") + } + + if ms := m.Match("/foo/bar"); len(ms) != 2 { + t.Fatal("not equal") + } else if !equal(ms, "logging", "foo/bar") { + t.Fatal("not equal") + } + + if ms := m.Match("/foo/bar/x"); len(ms) != 2 { + t.Fatal("not equal") + } else if !equal(ms, "logging", "foo/bar/*") { + t.Fatal("not equal") + } +} diff --git a/internal/ratelimit/bbr.go b/internal/ratelimit/bbr.go new file mode 100644 index 0000000..55336c6 --- /dev/null +++ b/internal/ratelimit/bbr.go @@ -0,0 +1,359 @@ +package ratelimit + +import ( + "math" + "runtime" + "runtime/metrics" + "sync" + "sync/atomic" + "time" +) + +var ( + gCPU int64 + decay = 0.95 +) + +type ( + cpuGetter func() int64 + + // Option configures a BBR limiter. + Option func(*options) +) + +func init() { + go cpuproc() +} + +func cpuproc() { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + sampler := newCPUSampler() + for range ticker.C { + curCPU := sampler.usage() + if curCPU < 0 { + continue + } + curCPU = minInt64(curCPU, 1000) + prevCPU := atomic.LoadInt64(&gCPU) + cpu := int64(float64(prevCPU)*decay + float64(curCPU)*(1.0-decay)) + atomic.StoreInt64(&gCPU, cpu) + } +} + +type cpuSampler struct { + mu sync.Mutex + samples []metrics.Sample + prevUsed float64 + prevTotal float64 + ready bool +} + +func newCPUSampler() *cpuSampler { + return &cpuSampler{ + samples: []metrics.Sample{ + {Name: "/cpu/classes/total:cpu-seconds"}, + {Name: "/cpu/classes/idle:cpu-seconds"}, + }, + } +} + +func (s *cpuSampler) usage() int64 { + s.mu.Lock() + defer s.mu.Unlock() + + metrics.Read(s.samples) + total := s.samples[0].Value.Float64() + idle := s.samples[1].Value.Float64() + used := total - idle + if !s.ready { + s.prevUsed = used + s.prevTotal = total + s.ready = true + return 0 + } + usedDelta := used - s.prevUsed + totalDelta := total - s.prevTotal + s.prevUsed = used + s.prevTotal = total + if totalDelta <= 0 || usedDelta < 0 { + return 0 + } + return int64((usedDelta / totalDelta) * 1000) +} + +func minInt64(l, r int64) int64 { + if l < r { + return l + } + return r +} + +// Stat contains a BBR metrics snapshot. +type Stat struct { + CPU int64 + InFlight int64 + MaxInFlight int64 + MinRt int64 + MaxPass int64 +} + +type counterCache struct { + val int64 + time time.Time +} + +type options struct { + Window time.Duration + Bucket int + CPUThreshold int64 + CPUQuota float64 +} + +// WithWindow sets the rolling window duration. +func WithWindow(d time.Duration) Option { + return func(o *options) { + o.Window = d + } +} + +// WithBucket sets the rolling window bucket count. +func WithBucket(b int) Option { + return func(o *options) { + o.Bucket = b + } +} + +// WithCPUThreshold sets the CPU threshold, scaled from 0 to 1000. +func WithCPUThreshold(threshold int64) Option { + return func(o *options) { + o.CPUThreshold = threshold + } +} + +// WithCPUQuota sets the real CPU quota if it differs from GOMAXPROCS. +func WithCPUQuota(quota float64) Option { + return func(o *options) { + o.CPUQuota = quota + } +} + +// BBR implements a BBR-like adaptive limiter. +type BBR struct { + cpu cpuGetter + passStat *rollingCounter + rtStat *rollingCounter + inFlight int64 + bucketPerSecond int64 + bucketDuration time.Duration + + prevDropTime atomic.Value + maxPASSCache atomic.Value + minRtCache atomic.Value + + opts options +} + +// NewLimiter returns a BBR limiter. +func NewLimiter(opts ...Option) *BBR { + opt := options{ + Window: 10 * time.Second, + Bucket: 100, + CPUThreshold: 800, + } + for _, o := range opts { + o(&opt) + } + if opt.Window <= 0 { + opt.Window = 10 * time.Second + } + if opt.Bucket < 1 { + opt.Bucket = 1 + } + + bucketDuration := opt.Window / time.Duration(opt.Bucket) + if bucketDuration <= 0 { + bucketDuration = opt.Window + } + + limiter := &BBR{ + opts: opt, + passStat: newRollingCounter(opt.Bucket, bucketDuration), + rtStat: newRollingCounter(opt.Bucket, bucketDuration), + bucketDuration: bucketDuration, + bucketPerSecond: int64(time.Second / bucketDuration), + cpu: func() int64 { return atomic.LoadInt64(&gCPU) }, + } + if limiter.bucketPerSecond < 1 { + limiter.bucketPerSecond = 1 + } + if opt.CPUQuota != 0 { + limiter.cpu = func() int64 { + return int64(float64(atomic.LoadInt64(&gCPU)) * float64(runtime.GOMAXPROCS(0)) / opt.CPUQuota) + } + } + return limiter +} + +func (l *BBR) maxPASS() int64 { + passCache := l.maxPASSCache.Load() + if passCache != nil { + ps := passCache.(*counterCache) + if l.timespan(ps.time) < 1 { + return ps.val + } + } + rawMaxPass := int64(l.passStat.reduce(func(bucket counterBucket) float64 { + return float64(bucket.count) + }, math.Max, 1)) + l.maxPASSCache.Store(&counterCache{ + val: rawMaxPass, + time: time.Now(), + }) + return rawMaxPass +} + +func (l *BBR) timespan(lastTime time.Time) int { + v := int(time.Since(lastTime) / l.bucketDuration) + if v > -1 { + return v + } + return l.opts.Bucket +} + +func (l *BBR) minRT() int64 { + rtCache := l.minRtCache.Load() + if rtCache != nil { + rc := rtCache.(*counterCache) + if l.timespan(rc.time) < 1 { + return rc.val + } + } + rawRT := l.rtStat.reduce(func(bucket counterBucket) float64 { + if bucket.count == 0 { + return math.MaxFloat64 + } + return bucket.sum / float64(bucket.count) + }, math.Min, math.MaxFloat64) + rawMinRT := int64(1) + if rawRT > 0 && rawRT != math.MaxFloat64 { + rawMinRT = int64(math.Ceil(rawRT)) + } + l.minRtCache.Store(&counterCache{ + val: rawMinRT, + time: time.Now(), + }) + return rawMinRT +} + +func (l *BBR) maxInFlight() int64 { + return int64(math.Floor(float64(l.maxPASS()*l.minRT()*l.bucketPerSecond)/1000.0) + 0.5) +} + +func (l *BBR) shouldDrop() bool { + now := time.Duration(time.Now().UnixNano()) + if l.cpu() < l.opts.CPUThreshold { + prevDropTime, _ := l.prevDropTime.Load().(time.Duration) + if prevDropTime == 0 { + return false + } + if now-prevDropTime <= time.Second { + inFlight := atomic.LoadInt64(&l.inFlight) + return inFlight > 1 && inFlight > l.maxInFlight() + } + l.prevDropTime.Store(time.Duration(0)) + return false + } + inFlight := atomic.LoadInt64(&l.inFlight) + drop := inFlight > 1 && inFlight > l.maxInFlight() + if drop { + prevDrop, _ := l.prevDropTime.Load().(time.Duration) + if prevDrop != 0 { + return true + } + l.prevDropTime.Store(now) + } + return drop +} + +// Stat returns a metrics snapshot. +func (l *BBR) Stat() Stat { + return Stat{ + CPU: l.cpu(), + MinRt: l.minRT(), + MaxPass: l.maxPASS(), + MaxInFlight: l.maxInFlight(), + InFlight: atomic.LoadInt64(&l.inFlight), + } +} + +// Allow checks whether a request is allowed. +func (l *BBR) Allow() (DoneFunc, error) { + if l.shouldDrop() { + return nil, ErrLimitExceed + } + atomic.AddInt64(&l.inFlight, 1) + start := time.Now() + return func(DoneInfo) { + if rt := math.Ceil(float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond)); rt > 0 { + l.rtStat.add(rt) + } + atomic.AddInt64(&l.inFlight, -1) + l.passStat.add(1) + }, nil +} + +type rollingCounter struct { + mu sync.Mutex + buckets []counterBucket + bucketDuration time.Duration +} + +type counterBucket struct { + slot int64 + sum float64 + count int64 +} + +func newRollingCounter(size int, bucketDuration time.Duration) *rollingCounter { + return &rollingCounter{ + buckets: make([]counterBucket, size), + bucketDuration: bucketDuration, + } +} + +func (r *rollingCounter) add(value float64) { + slot := r.currentSlot() + offset := int(slot % int64(len(r.buckets))) + + r.mu.Lock() + defer r.mu.Unlock() + bucket := &r.buckets[offset] + if bucket.slot != slot { + bucket.slot = slot + bucket.sum = 0 + bucket.count = 0 + } + bucket.sum += value + bucket.count++ +} + +func (r *rollingCounter) reduce(value func(counterBucket) float64, aggregate func(float64, float64) float64, fallback float64) float64 { + slot := r.currentSlot() + size := int64(len(r.buckets)) + result := fallback + + r.mu.Lock() + defer r.mu.Unlock() + for _, bucket := range r.buckets { + if bucket.count == 0 || bucket.slot == slot || slot-bucket.slot >= size || bucket.slot > slot { + continue + } + result = aggregate(result, value(bucket)) + } + return result +} + +func (r *rollingCounter) currentSlot() int64 { + return time.Now().UnixNano() / int64(r.bucketDuration) +} diff --git a/internal/ratelimit/bbr_test.go b/internal/ratelimit/bbr_test.go new file mode 100644 index 0000000..a9bb0f3 --- /dev/null +++ b/internal/ratelimit/bbr_test.go @@ -0,0 +1,44 @@ +package ratelimit + +import ( + "errors" + "testing" + "time" +) + +func TestBBRAllowRecordsDone(t *testing.T) { + limiter := NewLimiter(WithWindow(time.Second), WithBucket(10)) + done, err := limiter.Allow() + if err != nil { + t.Fatalf("Allow() error = %v, want nil", err) + } + done(DoneInfo{}) + + stat := limiter.Stat() + if stat.InFlight != 0 { + t.Fatalf("InFlight = %d, want 0", stat.InFlight) + } + if stat.MaxPass == 0 { + t.Fatalf("MaxPass = %d, want > 0", stat.MaxPass) + } +} + +func TestBBRDropsWhenCPUThresholdExceeded(t *testing.T) { + limiter := NewLimiter(WithWindow(time.Second), WithBucket(10), WithCPUThreshold(-1)) + limiter.cpu = func() int64 { return 1000 } + + done1, err := limiter.Allow() + if err != nil { + t.Fatalf("first Allow() error = %v, want nil", err) + } + defer done1(DoneInfo{}) + done2, err := limiter.Allow() + if err != nil { + t.Fatalf("second Allow() error = %v, want nil", err) + } + defer done2(DoneInfo{}) + + if _, err = limiter.Allow(); !errors.Is(err, ErrLimitExceed) { + t.Fatalf("third Allow() error = %v, want %v", err, ErrLimitExceed) + } +} diff --git a/internal/ratelimit/ratelimit.go b/internal/ratelimit/ratelimit.go new file mode 100644 index 0000000..51409bb --- /dev/null +++ b/internal/ratelimit/ratelimit.go @@ -0,0 +1,19 @@ +package ratelimit + +import "errors" + +// ErrLimitExceed is returned when the rate limiter rejects a request. +var ErrLimitExceed = errors.New("rate limit exceeded") + +// DoneFunc records request completion. +type DoneFunc func(DoneInfo) + +// DoneInfo contains request completion metadata. +type DoneInfo struct { + Err error +} + +// Limiter is a rate limiter. +type Limiter interface { + Allow() (DoneFunc, error) +} diff --git a/internal/subset/subset.go b/internal/subset/subset.go new file mode 100644 index 0000000..9aed3d7 --- /dev/null +++ b/internal/subset/subset.go @@ -0,0 +1,122 @@ +package subset + +import ( + "errors" + "hash/fnv" + "sort" + "strconv" +) + +type member interface { + String() string +} + +var errEmptyCircle = errors.New("empty circle") + +// Subset returns a deterministic subset for the given select key. +func Subset[M member](selectKey string, inss []M, num int) []M { + if num <= 0 || len(inss) <= num { + return inss + } + + circle := newConsistent[M]() + circle.set(inss) + out, err := circle.getN(selectKey, num) + if err != nil { + return inss + } + return out +} + +type consistent[M member] struct { + circle map[uint32]M + members map[string]bool + sortedHashes []uint32 +} + +func newConsistent[M member]() *consistent[M] { + return &consistent[M]{ + circle: make(map[uint32]M), + members: make(map[string]bool), + } +} + +func (c *consistent[M]) set(inss []M) { + for _, ins := range inss { + if c.members[ins.String()] { + continue + } + c.add(ins) + } +} + +func (c *consistent[M]) add(ins M) { + for i := 0; i < 160; i++ { + c.circle[hashKey(strconv.Itoa(i)+ins.String())] = ins + } + c.members[ins.String()] = true + c.updateSortedHashes() +} + +func (c *consistent[M]) getN(key string, n int) ([]M, error) { + if len(c.circle) == 0 { + return nil, errEmptyCircle + } + if len(c.members) < n { + n = len(c.members) + } + offset := c.search(hashKey(key)) + out := make([]M, 0, n) + for i := offset; ; i++ { + if i >= len(c.sortedHashes) { + i = 0 + } + ins := c.circle[c.sortedHashes[i]] + if !contains(out, ins) { + out = append(out, ins) + if len(out) == n { + return out, nil + } + } + if i == offset-1 || (offset == 0 && i == len(c.sortedHashes)-1) { + break + } + } + return out, nil +} + +func (c *consistent[M]) search(key uint32) int { + i := sort.Search(len(c.sortedHashes), func(i int) bool { + return c.sortedHashes[i] > key + }) + if i >= len(c.sortedHashes) { + i = 0 + } + return i +} + +func (c *consistent[M]) updateSortedHashes() { + hashes := c.sortedHashes[:0] + for key := range c.circle { + hashes = append(hashes, key) + } + sort.Slice(hashes, func(i, j int) bool { + return hashes[i] < hashes[j] + }) + c.sortedHashes = hashes +} + +func contains[M member](inss []M, ins M) bool { + for _, item := range inss { + if item.String() == ins.String() { + return true + } + } + return false +} + +func hashKey(key string) uint32 { + h := fnv.New32a() + _, _ = h.Write([]byte(key)) + return h.Sum32() +} diff --git a/internal/subset/subset_test.go b/internal/subset/subset_test.go new file mode 100644 index 0000000..f4ae389 --- /dev/null +++ b/internal/subset/subset_test.go @@ -0,0 +1,30 @@ +package subset + +import ( + "reflect" + "testing" +) + +type testMember string + +func (m testMember) String() string { return string(m) } + +func TestSubsetKeepsSmallInput(t *testing.T) { + in := []testMember{"a", "b"} + got := Subset("key", in, 2) + if !reflect.DeepEqual(got, in) { + t.Fatalf("Subset() = %v, want %v", got, in) + } +} + +func TestSubsetIsDeterministic(t *testing.T) { + in := []testMember{"a", "b", "c", "d"} + got := Subset("key", in, 2) + again := Subset("key", in, 2) + if !reflect.DeepEqual(got, again) { + t.Fatalf("Subset() = %v, want %v", got, again) + } + if len(got) != 2 { + t.Fatalf("len(Subset()) = %d, want 2", len(got)) + } +} diff --git a/internal/testdata/binding/generate.go b/internal/testdata/binding/generate.go new file mode 100644 index 0000000..7abe42f --- /dev/null +++ b/internal/testdata/binding/generate.go @@ -0,0 +1,3 @@ +package binding + +//go:generate protoc -I . --go_out=paths=source_relative:. ./test.proto diff --git a/internal/testdata/binding/test.pb.go b/internal/testdata/binding/test.pb.go new file mode 100644 index 0000000..a0f9091 --- /dev/null +++ b/internal/testdata/binding/test.pb.go @@ -0,0 +1,291 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v3.17.3 +// source: test.proto + +package binding + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The request message containing the user's name. +type HelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Sub *Sub `protobuf:"bytes,2,opt,name=sub,proto3" json:"sub,omitempty"` + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,3,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + OptInt32 *int32 `protobuf:"varint,4,opt,name=opt_int32,json=optInt32,proto3,oneof" json:"opt_int32,omitempty"` + OptInt64 *int64 `protobuf:"varint,5,opt,name=opt_int64,json=optInt64,proto3,oneof" json:"opt_int64,omitempty"` + OptString *string `protobuf:"bytes,6,opt,name=opt_string,json=optString,proto3,oneof" json:"opt_string,omitempty"` + SubField *Sub `protobuf:"bytes,7,opt,name=subField,proto3" json:"subField,omitempty"` + TestRepeated []string `protobuf:"bytes,8,rep,name=test_repeated,proto3" json:"test_repeated,omitempty"` +} + +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. +func (*HelloRequest) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{0} +} + +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *HelloRequest) GetSub() *Sub { + if x != nil { + return x.Sub + } + return nil +} + +func (x *HelloRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +func (x *HelloRequest) GetOptInt32() int32 { + if x != nil && x.OptInt32 != nil { + return *x.OptInt32 + } + return 0 +} + +func (x *HelloRequest) GetOptInt64() int64 { + if x != nil && x.OptInt64 != nil { + return *x.OptInt64 + } + return 0 +} + +func (x *HelloRequest) GetOptString() string { + if x != nil && x.OptString != nil { + return *x.OptString + } + return "" +} + +func (x *HelloRequest) GetSubField() *Sub { + if x != nil { + return x.SubField + } + return nil +} + +func (x *HelloRequest) GetTestRepeated() []string { + if x != nil { + return x.TestRepeated + } + return nil +} + +type Sub struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,json=naming,proto3" json:"name,omitempty"` +} + +func (x *Sub) Reset() { + *x = Sub{} + if protoimpl.UnsafeEnabled { + mi := &file_test_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Sub) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Sub) ProtoMessage() {} + +func (x *Sub) ProtoReflect() protoreflect.Message { + mi := &file_test_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Sub.ProtoReflect.Descriptor instead. +func (*Sub) Descriptor() ([]byte, []int) { + return file_test_proto_rawDescGZIP(), []int{1} +} + +func (x *Sub) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_test_proto protoreflect.FileDescriptor + +var file_test_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x62, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, + 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe2, 0x02, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x03, + 0x73, 0x75, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x62, 0x69, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, 0x62, 0x52, 0x03, 0x73, 0x75, 0x62, 0x12, 0x3b, 0x0a, 0x0b, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0a, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x12, 0x20, 0x0a, 0x09, 0x6f, 0x70, 0x74, + 0x5f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, + 0x6f, 0x70, 0x74, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x6f, + 0x70, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x48, 0x01, + 0x52, 0x08, 0x6f, 0x70, 0x74, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, + 0x0a, 0x6f, 0x70, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x02, 0x52, 0x09, 0x6f, 0x70, 0x74, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x88, 0x01, + 0x01, 0x12, 0x28, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x75, + 0x62, 0x52, 0x08, 0x73, 0x75, 0x62, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x74, + 0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0d, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6f, 0x70, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x42, + 0x0c, 0x0a, 0x0a, 0x5f, 0x6f, 0x70, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x42, 0x0d, 0x0a, + 0x0b, 0x5f, 0x6f, 0x70, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x1b, 0x0a, 0x03, + 0x53, 0x75, 0x62, 0x12, 0x14, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x6e, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, + 0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, + 0x72, 0x74, 0x2f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_test_proto_rawDescOnce sync.Once + file_test_proto_rawDescData = file_test_proto_rawDesc +) + +func file_test_proto_rawDescGZIP() []byte { + file_test_proto_rawDescOnce.Do(func() { + file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData) + }) + return file_test_proto_rawDescData +} + +var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_test_proto_goTypes = []interface{}{ + (*HelloRequest)(nil), // 0: binding.HelloRequest + (*Sub)(nil), // 1: binding.Sub + (*fieldmaskpb.FieldMask)(nil), // 2: google.protobuf.FieldMask +} +var file_test_proto_depIdxs = []int32{ + 1, // 0: binding.HelloRequest.sub:type_name -> binding.Sub + 2, // 1: binding.HelloRequest.update_mask:type_name -> google.protobuf.FieldMask + 1, // 2: binding.HelloRequest.subField:type_name -> binding.Sub + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_test_proto_init() } +func file_test_proto_init() { + if File_test_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Sub); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_test_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_test_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_test_proto_goTypes, + DependencyIndexes: file_test_proto_depIdxs, + MessageInfos: file_test_proto_msgTypes, + }.Build() + File_test_proto = out.File + file_test_proto_rawDesc = nil + file_test_proto_goTypes = nil + file_test_proto_depIdxs = nil +} diff --git a/internal/testdata/binding/test.proto b/internal/testdata/binding/test.proto new file mode 100644 index 0000000..c55fc4d --- /dev/null +++ b/internal/testdata/binding/test.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package binding; + +import "google/protobuf/field_mask.proto"; + +option go_package = "github.com/go-kratos/kratos/transport/binding"; + +// The request message containing the user's name. +message HelloRequest { + string name = 1; + Sub sub = 2; + google.protobuf.FieldMask update_mask = 3; + optional int32 opt_int32 = 4; + optional int64 opt_int64 = 5; + optional string opt_string = 6; + Sub subField = 7; + repeated string test_repeated = 8 [json_name = "test_repeated"]; +} + +message Sub{ + string name = 1 [json_name = "naming"]; +} diff --git a/internal/testdata/complex/complex.pb.go b/internal/testdata/complex/complex.pb.go new file mode 100644 index 0000000..d58afab --- /dev/null +++ b/internal/testdata/complex/complex.pb.go @@ -0,0 +1,586 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v5.29.3 +// source: complex.proto + +package complex + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Sex int32 + +const ( + Sex_man Sex = 0 + Sex_woman Sex = 1 +) + +// Enum value maps for Sex. +var ( + Sex_name = map[int32]string{ + 0: "man", + 1: "woman", + } + Sex_value = map[string]int32{ + "man": 0, + "woman": 1, + } +) + +func (x Sex) Enum() *Sex { + p := new(Sex) + *p = x + return p +} + +func (x Sex) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Sex) Descriptor() protoreflect.EnumDescriptor { + return file_complex_proto_enumTypes[0].Descriptor() +} + +func (Sex) Type() protoreflect.EnumType { + return &file_complex_proto_enumTypes[0] +} + +func (x Sex) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Sex.Descriptor instead. +func (Sex) EnumDescriptor() ([]byte, []int) { + return file_complex_proto_rawDescGZIP(), []int{0} +} + +// SimpleMessage represents a simple message sent to the Echo service. +type Complex struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Id represents the message identifier. + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + NoOne string `protobuf:"bytes,2,opt,name=no_one,json=numberOne,proto3" json:"no_one,omitempty"` + Simple *Simple `protobuf:"bytes,3,opt,name=simple,json=very_simple,proto3" json:"simple,omitempty"` + Strings []string `protobuf:"bytes,4,rep,name=strings,proto3" json:"strings,omitempty"` + Simples []*Simple `protobuf:"bytes,27,rep,name=simples,proto3" json:"simples,omitempty"` + B bool `protobuf:"varint,5,opt,name=b,proto3" json:"b,omitempty"` + Sex Sex `protobuf:"varint,6,opt,name=sex,proto3,enum=testproto.Sex" json:"sex,omitempty"` + Age int32 `protobuf:"varint,7,opt,name=age,proto3" json:"age,omitempty"` + A uint32 `protobuf:"varint,8,opt,name=a,proto3" json:"a,omitempty"` + Count uint64 `protobuf:"varint,9,opt,name=count,proto3" json:"count,omitempty"` + Price float32 `protobuf:"fixed32,10,opt,name=price,proto3" json:"price,omitempty"` + D float64 `protobuf:"fixed64,11,opt,name=d,proto3" json:"d,omitempty"` + Byte []byte `protobuf:"bytes,12,opt,name=byte,proto3" json:"byte,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Duration *durationpb.Duration `protobuf:"bytes,14,opt,name=duration,proto3" json:"duration,omitempty"` + Field *fieldmaskpb.FieldMask `protobuf:"bytes,15,opt,name=field,proto3" json:"field,omitempty"` + Double *wrapperspb.DoubleValue `protobuf:"bytes,16,opt,name=double,proto3" json:"double,omitempty"` + Float *wrapperspb.FloatValue `protobuf:"bytes,17,opt,name=float,proto3" json:"float,omitempty"` + Int64 *wrapperspb.Int64Value `protobuf:"bytes,18,opt,name=int64,proto3" json:"int64,omitempty"` + Int32 *wrapperspb.Int32Value `protobuf:"bytes,19,opt,name=int32,proto3" json:"int32,omitempty"` + Uint64 *wrapperspb.UInt64Value `protobuf:"bytes,20,opt,name=uint64,proto3" json:"uint64,omitempty"` + Uint32 *wrapperspb.UInt32Value `protobuf:"bytes,21,opt,name=uint32,proto3" json:"uint32,omitempty"` + Bool *wrapperspb.BoolValue `protobuf:"bytes,22,opt,name=bool,proto3" json:"bool,omitempty"` + String_ *wrapperspb.StringValue `protobuf:"bytes,23,opt,name=string,proto3" json:"string,omitempty"` + Bytes *wrapperspb.BytesValue `protobuf:"bytes,24,opt,name=bytes,proto3" json:"bytes,omitempty"` + Map map[string]string `protobuf:"bytes,25,rep,name=map,proto3" json:"map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + MapInt64Key map[int64]string `protobuf:"bytes,26,rep,name=map_int64_key,proto3" json:"map_int64_key,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *Complex) Reset() { + *x = Complex{} + if protoimpl.UnsafeEnabled { + mi := &file_complex_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Complex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Complex) ProtoMessage() {} + +func (x *Complex) ProtoReflect() protoreflect.Message { + mi := &file_complex_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Complex.ProtoReflect.Descriptor instead. +func (*Complex) Descriptor() ([]byte, []int) { + return file_complex_proto_rawDescGZIP(), []int{0} +} + +func (x *Complex) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Complex) GetNoOne() string { + if x != nil { + return x.NoOne + } + return "" +} + +func (x *Complex) GetSimple() *Simple { + if x != nil { + return x.Simple + } + return nil +} + +func (x *Complex) GetStrings() []string { + if x != nil { + return x.Strings + } + return nil +} + +func (x *Complex) GetSimples() []*Simple { + if x != nil { + return x.Simples + } + return nil +} + +func (x *Complex) GetB() bool { + if x != nil { + return x.B + } + return false +} + +func (x *Complex) GetSex() Sex { + if x != nil { + return x.Sex + } + return Sex_man +} + +func (x *Complex) GetAge() int32 { + if x != nil { + return x.Age + } + return 0 +} + +func (x *Complex) GetA() uint32 { + if x != nil { + return x.A + } + return 0 +} + +func (x *Complex) GetCount() uint64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *Complex) GetPrice() float32 { + if x != nil { + return x.Price + } + return 0 +} + +func (x *Complex) GetD() float64 { + if x != nil { + return x.D + } + return 0 +} + +func (x *Complex) GetByte() []byte { + if x != nil { + return x.Byte + } + return nil +} + +func (x *Complex) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *Complex) GetDuration() *durationpb.Duration { + if x != nil { + return x.Duration + } + return nil +} + +func (x *Complex) GetField() *fieldmaskpb.FieldMask { + if x != nil { + return x.Field + } + return nil +} + +func (x *Complex) GetDouble() *wrapperspb.DoubleValue { + if x != nil { + return x.Double + } + return nil +} + +func (x *Complex) GetFloat() *wrapperspb.FloatValue { + if x != nil { + return x.Float + } + return nil +} + +func (x *Complex) GetInt64() *wrapperspb.Int64Value { + if x != nil { + return x.Int64 + } + return nil +} + +func (x *Complex) GetInt32() *wrapperspb.Int32Value { + if x != nil { + return x.Int32 + } + return nil +} + +func (x *Complex) GetUint64() *wrapperspb.UInt64Value { + if x != nil { + return x.Uint64 + } + return nil +} + +func (x *Complex) GetUint32() *wrapperspb.UInt32Value { + if x != nil { + return x.Uint32 + } + return nil +} + +func (x *Complex) GetBool() *wrapperspb.BoolValue { + if x != nil { + return x.Bool + } + return nil +} + +func (x *Complex) GetString_() *wrapperspb.StringValue { + if x != nil { + return x.String_ + } + return nil +} + +func (x *Complex) GetBytes() *wrapperspb.BytesValue { + if x != nil { + return x.Bytes + } + return nil +} + +func (x *Complex) GetMap() map[string]string { + if x != nil { + return x.Map + } + return nil +} + +func (x *Complex) GetMapInt64Key() map[int64]string { + if x != nil { + return x.MapInt64Key + } + return nil +} + +type Simple struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Component string `protobuf:"bytes,1,opt,name=component,proto3" json:"component,omitempty"` +} + +func (x *Simple) Reset() { + *x = Simple{} + if protoimpl.UnsafeEnabled { + mi := &file_complex_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Simple) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Simple) ProtoMessage() {} + +func (x *Simple) ProtoReflect() protoreflect.Message { + mi := &file_complex_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Simple.ProtoReflect.Descriptor instead. +func (*Simple) Descriptor() ([]byte, []int) { + return file_complex_proto_rawDescGZIP(), []int{1} +} + +func (x *Simple) GetComponent() string { + if x != nil { + return x.Component + } + return "" +} + +var File_complex_proto protoreflect.FileDescriptor + +var file_complex_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x09, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb2, 0x09, + 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x06, 0x6e, 0x6f, 0x5f, + 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x75, 0x6d, 0x62, 0x65, + 0x72, 0x4f, 0x6e, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x0b, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x69, + 0x6d, 0x70, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2b, + 0x0a, 0x07, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x18, 0x1b, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x6d, 0x70, + 0x6c, 0x65, 0x52, 0x07, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x0c, 0x0a, 0x01, 0x62, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x01, 0x62, 0x12, 0x20, 0x0a, 0x03, 0x73, 0x65, 0x78, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x78, 0x52, 0x03, 0x73, 0x65, 0x78, 0x12, 0x10, 0x0a, 0x03, 0x61, + 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x67, 0x65, 0x12, 0x0c, 0x0a, + 0x01, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x01, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x02, + 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x0c, 0x0a, 0x01, 0x64, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x01, 0x52, 0x01, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x79, 0x74, 0x65, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x79, 0x74, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x12, 0x35, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x05, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x34, 0x0a, 0x06, + 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, + 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x64, 0x6f, 0x75, 0x62, + 0x6c, 0x65, 0x12, 0x31, 0x0a, 0x05, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, + 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x12, 0x31, 0x0a, 0x05, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x18, 0x12, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x05, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x12, 0x31, 0x0a, 0x05, 0x69, 0x6e, 0x74, 0x33, + 0x32, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x12, 0x34, 0x0a, 0x06, 0x75, + 0x69, 0x6e, 0x74, 0x36, 0x34, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, + 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x75, 0x69, 0x6e, 0x74, 0x36, + 0x34, 0x12, 0x34, 0x0a, 0x06, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x18, 0x15, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x06, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x12, 0x2e, 0x0a, 0x04, 0x62, 0x6f, 0x6f, 0x6c, 0x18, + 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x04, 0x62, 0x6f, 0x6f, 0x6c, 0x12, 0x34, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, + 0x67, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x31, 0x0a, + 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, + 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, + 0x12, 0x2d, 0x0a, 0x03, 0x6d, 0x61, 0x70, 0x18, 0x19, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x78, 0x2e, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x6d, 0x61, 0x70, 0x12, + 0x49, 0x0a, 0x0d, 0x6d, 0x61, 0x70, 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x1a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x2e, 0x4d, 0x61, 0x70, 0x49, 0x6e, + 0x74, 0x36, 0x34, 0x4b, 0x65, 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0d, 0x6d, 0x61, 0x70, + 0x5f, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6b, 0x65, 0x79, 0x1a, 0x36, 0x0a, 0x08, 0x4d, 0x61, + 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x1a, 0x3e, 0x0a, 0x10, 0x4d, 0x61, 0x70, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4b, 0x65, + 0x79, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x26, 0x0a, 0x06, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2a, 0x19, 0x0a, 0x03, 0x73, 0x65, + 0x78, 0x12, 0x07, 0x0a, 0x03, 0x6d, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x77, 0x6f, + 0x6d, 0x61, 0x6e, 0x10, 0x01, 0x42, 0x57, 0x5a, 0x55, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b, 0x72, + 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, + 0x67, 0x65, 0x6e, 0x2d, 0x67, 0x6f, 0x2d, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x2f, 0x63, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x2f, 0x3b, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x78, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_complex_proto_rawDescOnce sync.Once + file_complex_proto_rawDescData = file_complex_proto_rawDesc +) + +func file_complex_proto_rawDescGZIP() []byte { + file_complex_proto_rawDescOnce.Do(func() { + file_complex_proto_rawDescData = protoimpl.X.CompressGZIP(file_complex_proto_rawDescData) + }) + return file_complex_proto_rawDescData +} + +var file_complex_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_complex_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_complex_proto_goTypes = []interface{}{ + (Sex)(0), // 0: testproto.sex + (*Complex)(nil), // 1: testproto.Complex + (*Simple)(nil), // 2: testproto.Simple + nil, // 3: testproto.Complex.MapEntry + nil, // 4: testproto.Complex.MapInt64KeyEntry + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 6: google.protobuf.Duration + (*fieldmaskpb.FieldMask)(nil), // 7: google.protobuf.FieldMask + (*wrapperspb.DoubleValue)(nil), // 8: google.protobuf.DoubleValue + (*wrapperspb.FloatValue)(nil), // 9: google.protobuf.FloatValue + (*wrapperspb.Int64Value)(nil), // 10: google.protobuf.Int64Value + (*wrapperspb.Int32Value)(nil), // 11: google.protobuf.Int32Value + (*wrapperspb.UInt64Value)(nil), // 12: google.protobuf.UInt64Value + (*wrapperspb.UInt32Value)(nil), // 13: google.protobuf.UInt32Value + (*wrapperspb.BoolValue)(nil), // 14: google.protobuf.BoolValue + (*wrapperspb.StringValue)(nil), // 15: google.protobuf.StringValue + (*wrapperspb.BytesValue)(nil), // 16: google.protobuf.BytesValue +} +var file_complex_proto_depIdxs = []int32{ + 2, // 0: testproto.Complex.simple:type_name -> testproto.Simple + 2, // 1: testproto.Complex.simples:type_name -> testproto.Simple + 0, // 2: testproto.Complex.sex:type_name -> testproto.sex + 5, // 3: testproto.Complex.timestamp:type_name -> google.protobuf.Timestamp + 6, // 4: testproto.Complex.duration:type_name -> google.protobuf.Duration + 7, // 5: testproto.Complex.field:type_name -> google.protobuf.FieldMask + 8, // 6: testproto.Complex.double:type_name -> google.protobuf.DoubleValue + 9, // 7: testproto.Complex.float:type_name -> google.protobuf.FloatValue + 10, // 8: testproto.Complex.int64:type_name -> google.protobuf.Int64Value + 11, // 9: testproto.Complex.int32:type_name -> google.protobuf.Int32Value + 12, // 10: testproto.Complex.uint64:type_name -> google.protobuf.UInt64Value + 13, // 11: testproto.Complex.uint32:type_name -> google.protobuf.UInt32Value + 14, // 12: testproto.Complex.bool:type_name -> google.protobuf.BoolValue + 15, // 13: testproto.Complex.string:type_name -> google.protobuf.StringValue + 16, // 14: testproto.Complex.bytes:type_name -> google.protobuf.BytesValue + 3, // 15: testproto.Complex.map:type_name -> testproto.Complex.MapEntry + 4, // 16: testproto.Complex.map_int64_key:type_name -> testproto.Complex.MapInt64KeyEntry + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name +} + +func init() { file_complex_proto_init() } +func file_complex_proto_init() { + if File_complex_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_complex_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Complex); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_complex_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Simple); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_complex_proto_rawDesc, + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_complex_proto_goTypes, + DependencyIndexes: file_complex_proto_depIdxs, + EnumInfos: file_complex_proto_enumTypes, + MessageInfos: file_complex_proto_msgTypes, + }.Build() + File_complex_proto = out.File + file_complex_proto_rawDesc = nil + file_complex_proto_goTypes = nil + file_complex_proto_depIdxs = nil +} diff --git a/internal/testdata/complex/complex.proto b/internal/testdata/complex/complex.proto new file mode 100644 index 0000000..caa4876 --- /dev/null +++ b/internal/testdata/complex/complex.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; + +option go_package = "github.com/go-kratos/kratos/cmd/protoc-gen-go-http/internal/encoding/complex/;complex"; + +package testproto; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/wrappers.proto"; + +// SimpleMessage represents a simple message sent to the Echo service. +message Complex { + // Id represents the message identifier. + int64 id = 1; + string no_one = 2 [json_name = "numberOne"]; + Simple simple = 3 [json_name = "very_simple"]; + repeated string strings = 4; + repeated Simple simples = 27; + bool b = 5; + sex sex = 6; + int32 age = 7; + uint32 a = 8; + uint64 count = 9; + float price = 10; + double d = 11; + bytes byte = 12; + + google.protobuf.Timestamp timestamp = 13; + google.protobuf.Duration duration = 14; + google.protobuf.FieldMask field = 15; + + google.protobuf.DoubleValue double = 16; + google.protobuf.FloatValue float = 17; + google.protobuf.Int64Value int64 = 18; + google.protobuf.Int32Value int32 = 19; + google.protobuf.UInt64Value uint64 = 20; + google.protobuf.UInt32Value uint32 = 21; + google.protobuf.BoolValue bool = 22; + google.protobuf.StringValue string = 23; + google.protobuf.BytesValue bytes = 24; + + map map = 25; + map map_int64_key = 26 [json_name = "map_int64_key"]; +} + +message Simple { + string component = 1; +} + +enum sex { + man = 0; + woman = 1; +} diff --git a/internal/testdata/complex/generate.go b/internal/testdata/complex/generate.go new file mode 100644 index 0000000..c3cfd9f --- /dev/null +++ b/internal/testdata/complex/generate.go @@ -0,0 +1,3 @@ +package complex + +//go:generate protoc -I . --go_out=paths=source_relative:. ./complex.proto diff --git a/internal/testdata/encoding/test.pb.go b/internal/testdata/encoding/test.pb.go new file mode 100644 index 0000000..b5b6c5d --- /dev/null +++ b/internal/testdata/encoding/test.pb.go @@ -0,0 +1,258 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.19.4 +// source: encoding/test.proto + +package encoding + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TestModel struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Hobby []string `protobuf:"bytes,3,rep,name=hobby,proto3" json:"hobby,omitempty"` + Attrs map[string]string `protobuf:"bytes,4,rep,name=attrs,proto3" json:"attrs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *TestModel) Reset() { + *x = TestModel{} + if protoimpl.UnsafeEnabled { + mi := &file_encoding_test_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestModel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestModel) ProtoMessage() {} + +func (x *TestModel) ProtoReflect() protoreflect.Message { + mi := &file_encoding_test_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestModel.ProtoReflect.Descriptor instead. +func (*TestModel) Descriptor() ([]byte, []int) { + return file_encoding_test_proto_rawDescGZIP(), []int{0} +} + +func (x *TestModel) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *TestModel) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TestModel) GetHobby() []string { + if x != nil { + return x.Hobby + } + return nil +} + +func (x *TestModel) GetAttrs() map[string]string { + if x != nil { + return x.Attrs + } + return nil +} + +type StructPb struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + DataList []*structpb.Struct `protobuf:"bytes,2,rep,name=data_list,json=dataList,proto3" json:"data_list,omitempty"` +} + +func (x *StructPb) Reset() { + *x = StructPb{} + if protoimpl.UnsafeEnabled { + mi := &file_encoding_test_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StructPb) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StructPb) ProtoMessage() {} + +func (x *StructPb) ProtoReflect() protoreflect.Message { + mi := &file_encoding_test_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StructPb.ProtoReflect.Descriptor instead. +func (*StructPb) Descriptor() ([]byte, []int) { + return file_encoding_test_proto_rawDescGZIP(), []int{1} +} + +func (x *StructPb) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +func (x *StructPb) GetDataList() []*structpb.Struct { + if x != nil { + return x.DataList + } + return nil +} + +var File_encoding_test_proto protoreflect.FileDescriptor + +var file_encoding_test_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x74, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb3, 0x01, 0x0a, 0x0a, 0x74, 0x65, + 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x68, 0x6f, 0x62, 0x62, 0x79, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x68, 0x6f, 0x62, + 0x62, 0x79, 0x12, 0x31, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x6d, 0x6f, + 0x64, 0x65, 0x6c, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, + 0x61, 0x74, 0x74, 0x72, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x41, 0x74, 0x74, 0x72, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x6d, 0x0a, 0x08, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x50, 0x62, 0x12, 0x2b, 0x0a, 0x04, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, + 0x63, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x34, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, + 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x0d, + 0x5a, 0x0b, 0x2e, 0x2e, 0x2f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_encoding_test_proto_rawDescOnce sync.Once + file_encoding_test_proto_rawDescData = file_encoding_test_proto_rawDesc +) + +func file_encoding_test_proto_rawDescGZIP() []byte { + file_encoding_test_proto_rawDescOnce.Do(func() { + file_encoding_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_encoding_test_proto_rawDescData) + }) + return file_encoding_test_proto_rawDescData +} + +var file_encoding_test_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_encoding_test_proto_goTypes = []interface{}{ + (*TestModel)(nil), // 0: test.test_model + (*StructPb)(nil), // 1: test.StructPb + nil, // 2: test.test_model.AttrsEntry + (*structpb.Struct)(nil), // 3: google.protobuf.Struct +} +var file_encoding_test_proto_depIdxs = []int32{ + 2, // 0: test.test_model.attrs:type_name -> test.test_model.AttrsEntry + 3, // 1: test.StructPb.data:type_name -> google.protobuf.Struct + 3, // 2: test.StructPb.data_list:type_name -> google.protobuf.Struct + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_encoding_test_proto_init() } +func file_encoding_test_proto_init() { + if File_encoding_test_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_encoding_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TestModel); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_encoding_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StructPb); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_encoding_test_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_encoding_test_proto_goTypes, + DependencyIndexes: file_encoding_test_proto_depIdxs, + MessageInfos: file_encoding_test_proto_msgTypes, + }.Build() + File_encoding_test_proto = out.File + file_encoding_test_proto_rawDesc = nil + file_encoding_test_proto_goTypes = nil + file_encoding_test_proto_depIdxs = nil +} diff --git a/internal/testdata/encoding/test.proto b/internal/testdata/encoding/test.proto new file mode 100644 index 0000000..ad39a7f --- /dev/null +++ b/internal/testdata/encoding/test.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package test; + +option go_package = "../encoding"; + +import "google/protobuf/struct.proto"; + +message test_model { + int64 id = 1; + string name = 2; + repeated string hobby = 3; + map attrs = 4; +} + +message StructPb { + google.protobuf.Struct data = 1; + repeated google.protobuf.Struct data_list = 2; +} diff --git a/internal/testdata/helloworld/generate.go b/internal/testdata/helloworld/generate.go new file mode 100644 index 0000000..05de6fc --- /dev/null +++ b/internal/testdata/helloworld/generate.go @@ -0,0 +1,3 @@ +package helloworld + +//go:generate protoc -I . -I ../../../third_party --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. --go-http_out=paths=source_relative:. ./helloworld.proto diff --git a/internal/testdata/helloworld/helloworld.pb.go b/internal/testdata/helloworld/helloworld.pb.go new file mode 100644 index 0000000..48d4417 --- /dev/null +++ b/internal/testdata/helloworld/helloworld.pb.go @@ -0,0 +1,227 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v3.17.3 +// source: helloworld.proto + +package helloworld + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The request message containing the user's name. +type HelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *HelloRequest) Reset() { + *x = HelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_helloworld_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloRequest) ProtoMessage() {} + +func (x *HelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_helloworld_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. +func (*HelloRequest) Descriptor() ([]byte, []int) { + return file_helloworld_proto_rawDescGZIP(), []int{0} +} + +func (x *HelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// The response message containing the greetings +type HelloReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *HelloReply) Reset() { + *x = HelloReply{} + if protoimpl.UnsafeEnabled { + mi := &file_helloworld_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HelloReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HelloReply) ProtoMessage() {} + +func (x *HelloReply) ProtoReflect() protoreflect.Message { + mi := &file_helloworld_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. +func (*HelloReply) Descriptor() ([]byte, []int) { + return file_helloworld_proto_rawDescGZIP(), []int{1} +} + +func (x *HelloReply) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_helloworld_proto protoreflect.FileDescriptor + +var file_helloworld_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x1a, 0x1c, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a, 0x0c, + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xab, 0x01, 0x0a, 0x07, 0x47, 0x72, 0x65, + 0x65, 0x74, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, + 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x12, 0x12, 0x2f, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x46, + 0x0a, 0x0e, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x28, 0x01, 0x30, 0x01, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b, + 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, + 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_helloworld_proto_rawDescOnce sync.Once + file_helloworld_proto_rawDescData = file_helloworld_proto_rawDesc +) + +func file_helloworld_proto_rawDescGZIP() []byte { + file_helloworld_proto_rawDescOnce.Do(func() { + file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_helloworld_proto_rawDescData) + }) + return file_helloworld_proto_rawDescData +} + +var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_helloworld_proto_goTypes = []interface{}{ + (*HelloRequest)(nil), // 0: helloworld.HelloRequest + (*HelloReply)(nil), // 1: helloworld.HelloReply +} +var file_helloworld_proto_depIdxs = []int32{ + 0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest + 0, // 1: helloworld.Greeter.SayHelloStream:input_type -> helloworld.HelloRequest + 1, // 2: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply + 1, // 3: helloworld.Greeter.SayHelloStream:output_type -> helloworld.HelloReply + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_helloworld_proto_init() } +func file_helloworld_proto_init() { + if File_helloworld_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HelloReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_helloworld_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_helloworld_proto_goTypes, + DependencyIndexes: file_helloworld_proto_depIdxs, + MessageInfos: file_helloworld_proto_msgTypes, + }.Build() + File_helloworld_proto = out.File + file_helloworld_proto_rawDesc = nil + file_helloworld_proto_goTypes = nil + file_helloworld_proto_depIdxs = nil +} diff --git a/internal/testdata/helloworld/helloworld.proto b/internal/testdata/helloworld/helloworld.proto new file mode 100644 index 0000000..c14f8f8 --- /dev/null +++ b/internal/testdata/helloworld/helloworld.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package helloworld; + +import "google/api/annotations.proto"; + +option go_package = "github.com/go-kratos/kratos/v3/internal/testdata/helloworld"; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/helloworld/{name}", + }; + } + // Sends a greeting + rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/internal/testdata/helloworld/helloworld_grpc.pb.go b/internal/testdata/helloworld/helloworld_grpc.pb.go new file mode 100644 index 0000000..3558cbe --- /dev/null +++ b/internal/testdata/helloworld/helloworld_grpc.pb.go @@ -0,0 +1,178 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.17.3 +// source: helloworld.proto + +package helloworld + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// GreeterClient is the client API for Greeter service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GreeterClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) + // Sends a greeting + SayHelloStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloStreamClient, error) +} + +type greeterClient struct { + cc grpc.ClientConnInterface +} + +func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *greeterClient) SayHelloStream(ctx context.Context, opts ...grpc.CallOption) (Greeter_SayHelloStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[0], "/helloworld.Greeter/SayHelloStream", opts...) + if err != nil { + return nil, err + } + x := &greeterSayHelloStreamClient{stream} + return x, nil +} + +type Greeter_SayHelloStreamClient interface { + Send(*HelloRequest) error + Recv() (*HelloReply, error) + grpc.ClientStream +} + +type greeterSayHelloStreamClient struct { + grpc.ClientStream +} + +func (x *greeterSayHelloStreamClient) Send(m *HelloRequest) error { + return x.ClientStream.SendMsg(m) +} + +func (x *greeterSayHelloStreamClient) Recv() (*HelloReply, error) { + m := new(HelloReply) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// GreeterServer is the server API for Greeter service. +// All implementations must embed UnimplementedGreeterServer +// for forward compatibility +type GreeterServer interface { + // Sends a greeting + SayHello(context.Context, *HelloRequest) (*HelloReply, error) + // Sends a greeting + SayHelloStream(Greeter_SayHelloStreamServer) error + mustEmbedUnimplementedGreeterServer() +} + +// UnimplementedGreeterServer must be embedded to have forward compatible implementations. +type UnimplementedGreeterServer struct { +} + +func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} +func (UnimplementedGreeterServer) SayHelloStream(Greeter_SayHelloStreamServer) error { + return status.Errorf(codes.Unimplemented, "method SayHelloStream not implemented") +} +func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {} + +// UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GreeterServer will +// result in compilation errors. +type UnsafeGreeterServer interface { + mustEmbedUnimplementedGreeterServer() +} + +func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) { + s.RegisterService(&Greeter_ServiceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/helloworld.Greeter/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Greeter_SayHelloStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(GreeterServer).SayHelloStream(&greeterSayHelloStreamServer{stream}) +} + +type Greeter_SayHelloStreamServer interface { + Send(*HelloReply) error + Recv() (*HelloRequest, error) + grpc.ServerStream +} + +type greeterSayHelloStreamServer struct { + grpc.ServerStream +} + +func (x *greeterSayHelloStreamServer) Send(m *HelloReply) error { + return x.ServerStream.SendMsg(m) +} + +func (x *greeterSayHelloStreamServer) Recv() (*HelloRequest, error) { + m := new(HelloRequest) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Greeter_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "helloworld.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "SayHelloStream", + Handler: _Greeter_SayHelloStream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "helloworld.proto", +} diff --git a/internal/testdata/helloworld/helloworld_http.pb.go b/internal/testdata/helloworld/helloworld_http.pb.go new file mode 100644 index 0000000..a8fd6ca --- /dev/null +++ b/internal/testdata/helloworld/helloworld_http.pb.go @@ -0,0 +1,77 @@ +// Code generated by protoc-gen-go-http. DO NOT EDIT. +// versions: +// protoc-gen-go-http v2.3.1 + +package helloworld + +import ( + context "context" + http "github.com/go-kratos/kratos/v3/transport/http" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the kratos package it is being compiled against. +var _ = new(context.Context) + +const _ = http.SupportPackageIsVersion3 + +const OperationGreeterSayHello = "/helloworld.Greeter/SayHello" + +type GreeterHTTPServer interface { + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +func RegisterGreeterHTTPServer(s *http.Server, srv GreeterHTTPServer) { + r := s.Route("/") + r.GET("/helloworld/{name}", _Greeter_SayHello0_HTTP_Handler(srv)) +} + +func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error { + return func(ctx http.Context) error { + var in HelloRequest + if err := ctx.BindQuery(&in); err != nil { + return err + } + if err := ctx.BindVars(&in); err != nil { + return err + } + http.SetOperation(ctx, OperationGreeterSayHello) + h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.SayHello(ctx, req.(*HelloRequest)) + }) + out, err := h(ctx, &in) + if err != nil { + return err + } + reply := out.(*HelloReply) + return ctx.Result(200, reply) + } +} + +type GreeterHTTPClient interface { + SayHello(ctx context.Context, req *HelloRequest, opts ...http.CallOption) (rsp *HelloReply, err error) +} + +type GreeterHTTPClientImpl struct { + cc *http.Client +} + +func NewGreeterHTTPClient(client *http.Client) GreeterHTTPClient { + return &GreeterHTTPClientImpl{client} +} + +func (c *GreeterHTTPClientImpl) SayHello(ctx context.Context, in *HelloRequest, opts ...http.CallOption) (*HelloReply, error) { + var out HelloReply + pattern := "/helloworld/{name}" + path := http.BuildPath(pattern, in, http.WithQueryParams()) + opts = append([]http.CallOption{ + http.Accept("application/protojson"), + http.Operation(OperationGreeterSayHello), + http.PathTemplate(pattern), + }, opts...) + err := c.cc.Invoke(ctx, "GET", path, nil, &out, opts...) + if err != nil { + return nil, err + } + return &out, err +} diff --git a/log/README.md b/log/README.md new file mode 100644 index 0000000..906ebe4 --- /dev/null +++ b/log/README.md @@ -0,0 +1,89 @@ +# Logger + +## Usage + +### slog + +```go +logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, +})) + +log.SetDefault(logger) + +logger.InfoContext(ctx, "user created", + "user_id", userID, + "service.name", "helloworld", +) +``` + +### Global logger + +Common global helpers are still available for gradual migration. The signatures +now mirror slog: the first argument is the message, followed by key/value pairs +or `slog.Attr` values. + +```go +log.Info("started") +log.Info("listening", "addr", addr) +log.Info("service started", "service.name", "helloworld", "service.version", "v1.0.0") +log.InfoContext(ctx, "user created", "user_id", userID) +``` + +### Builder + +`log.NewHandler` builds a default handler. `log.NewLogger` wraps an existing +handler with Kratos decorators. Attach fixed service attrs with `logger.With`. + +```go +logger := log.NewLogger( + slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + }), + log.WithFilter(log.FilterKey("password")), // redact sensitive keys +).With( + slog.String("service.id", id), + slog.String("service.name", name), + slog.String("service.version", version), +) +log.SetDefault(logger) +``` + +### Context attrs + +Attach attributes to a `context.Context` and they will flow through any +ctx-aware log call automatically. + +```go +ctx = log.ContextWithAttrs(ctx, slog.String("request_id", id)) +log.InfoContext(ctx, "handling request") +``` + +### OpenTelemetry + +```go +import ( + otel "github.com/go-kratos/kratos/contrib/otel/v3/log" + "github.com/go-kratos/kratos/v3/log" +) + +logger := log.NewLogger(otel.NewHandler("helloworld")) +log.SetDefault(logger) +``` + +The `github.com/go-kratos/kratos/contrib/otel/v3/log` handler bridges slog records to +OpenTelemetry Logs. Use the core log builder when you need Kratos logger options: + +```go +import ( + "log/slog" + + otel "github.com/go-kratos/kratos/contrib/otel/v3/log" + "github.com/go-kratos/kratos/v3/log" +) + +logger := log.NewLogger( + otel.NewHandler("helloworld"), + log.WithFilter(log.FilterKey("password")), +).With(slog.String("service.name", "helloworld")) +``` diff --git a/log/builder.go b/log/builder.go new file mode 100644 index 0000000..e84edfd --- /dev/null +++ b/log/builder.go @@ -0,0 +1,129 @@ +package log + +import ( + "context" + "io" + "log/slog" + "os" +) + +// Format selects the encoding used by the default handler builder. +type Format int + +const ( + // FormatText writes records using [slog.NewTextHandler]. + FormatText Format = iota + // FormatJSON writes records using [slog.NewJSONHandler]. + FormatJSON +) + +// Option configures [NewHandler] and the decorators applied by [NewLogger]. +type Option func(*handlerConfig) + +// Extractor extracts attrs from a log call context. +type Extractor func(context.Context) []slog.Attr + +type handlerConfig struct { + writer io.Writer + format Format + level Leveler + addSource bool + replaceAttr func(groups []string, a slog.Attr) slog.Attr + extractors []Extractor + filter []FilterOption +} + +// WithExtractor appends attrs extracted from each log call context. +func WithExtractor(extractors ...Extractor) Option { + return func(c *handlerConfig) { + for _, e := range extractors { + if e != nil { + c.extractors = append(c.extractors, e) + } + } + } +} + +// WithWriter sets the destination writer for the base handler. Defaults to +// [os.Stderr]. +func WithWriter(w io.Writer) Option { + return func(c *handlerConfig) { c.writer = w } +} + +// WithFormat selects between text and JSON encoding. Defaults to [FormatText]. +func WithFormat(f Format) Option { + return func(c *handlerConfig) { c.format = f } +} + +// WithLevel sets the minimum level for the base handler. +func WithLevel(l Leveler) Option { + return func(c *handlerConfig) { c.level = l } +} + +// WithAddSource toggles inclusion of the source file/line. +func WithAddSource(b bool) Option { + return func(c *handlerConfig) { c.addSource = b } +} + +// WithReplaceAttr installs a custom ReplaceAttr on the base handler. +func WithReplaceAttr(fn func(groups []string, a slog.Attr) slog.Attr) Option { + return func(c *handlerConfig) { c.replaceAttr = fn } +} + +// WithFilter applies the provided filter options on top of the composed +// handler. +func WithFilter(opts ...FilterOption) Option { + return func(c *handlerConfig) { c.filter = append(c.filter, opts...) } +} + +// NewHandler builds a composed [slog.Handler] with kratos defaults: +// - text encoding to stderr at LevelInfo +// - context attrs from [ContextWithAttrs] are merged in +// +// Additional decorators are layered as configured. +func NewHandler(opts ...Option) slog.Handler { + cfg := &handlerConfig{ + writer: os.Stderr, + format: FormatText, + level: LevelInfo, + extractors: []Extractor{AttrsFromContext}, + } + for _, o := range opts { + o(cfg) + } + h := newBaseHandler(cfg) + return newComposedHandler(h, cfg) +} + +// NewLogger returns a slog logger backed by handler with kratos decorators +// applied. +func NewLogger(handler slog.Handler, opts ...Option) *slog.Logger { + cfg := &handlerConfig{ + extractors: []Extractor{AttrsFromContext}, + } + for _, o := range opts { + o(cfg) + } + return slog.New(newComposedHandler(handler, cfg)) +} + +func newComposedHandler(h slog.Handler, cfg *handlerConfig) slog.Handler { + if len(cfg.filter) > 0 { + h = newFilterHandler(h, cfg.filter...) + } + return newContextHandler(h, cfg.extractors...) +} + +func newBaseHandler(cfg *handlerConfig) slog.Handler { + hopts := &slog.HandlerOptions{ + Level: cfg.level, + AddSource: cfg.addSource, + ReplaceAttr: cfg.replaceAttr, + } + switch cfg.format { + case FormatJSON: + return slog.NewJSONHandler(cfg.writer, hopts) + default: + return slog.NewTextHandler(cfg.writer, hopts) + } +} diff --git a/log/builder_test.go b/log/builder_test.go new file mode 100644 index 0000000..b0d2d13 --- /dev/null +++ b/log/builder_test.go @@ -0,0 +1,124 @@ +package log + +import ( + "bytes" + "context" + "encoding/json" + "log/slog" + "strings" + "testing" +) + +func TestBuilderDefaultsText(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(NewHandler(WithWriter(&buf))) + logger.Info("hello", "k", "v") + if !strings.Contains(buf.String(), "hello") || !strings.Contains(buf.String(), "k=v") { + t.Fatalf("output = %q", buf.String()) + } +} + +func TestBuilderJSON(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(NewHandler(WithWriter(&buf), WithFormat(FormatJSON))) + logger.LogAttrs(context.Background(), LevelFatal, "boom") + var got map[string]any + if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &got); err != nil { + t.Fatalf("unmarshal: %v (%q)", err, buf.String()) + } + if got["level"] != LevelFatal.String() { + t.Fatalf("level = %v, want %s", got["level"], LevelFatal.String()) + } +} + +func TestBuilderWithAttrs(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(NewHandler( + WithWriter(&buf), + WithFormat(FormatJSON), + )).With( + slog.String("service.id", "i"), + slog.String("service.name", "n"), + slog.String("service.version", "v"), + ) + logger.Info("hi") + var got map[string]any + _ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &got) + if got["service.name"] != "n" { + t.Fatalf("service.name = %v", got["service.name"]) + } +} + +func TestBuilderWithHandler(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewJSONHandler(&buf, nil) + logger := NewLogger(handler).With(slog.String("service.name", "n")) + logger.Info("hi") + var got map[string]any + _ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &got) + if got["service.name"] != "n" { + t.Fatalf("service.name = %v", got["service.name"]) + } +} + +func TestBuilderWithFilter(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: LevelWarn}) + logger := NewLogger(handler, WithFilter(FilterKey("password"))) + logger.Info("ignored") + logger.Warn("login", "password", "secret") + out := buf.String() + if strings.Contains(out, "ignored") { + t.Fatalf("info should be filtered: %q", out) + } + if !strings.Contains(out, "password=***") { + t.Fatalf("expected password redacted: %q", out) + } +} + +func TestBuilderWithStoredContextAttrs(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(NewHandler(WithWriter(&buf), WithFormat(FormatJSON))) + ctx := ContextWithAttrs(context.Background(), slog.String("trace_id", "abc")) + logger.InfoContext(ctx, "hi") + var got map[string]any + _ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &got) + if got["trace_id"] != "abc" { + t.Fatalf("trace_id = %v", got["trace_id"]) + } +} + +func TestBuilderWithExtractor(t *testing.T) { + type userIDKey struct{} + type requestIDKey struct{} + + var buf bytes.Buffer + handler := slog.NewJSONHandler(&buf, nil) + logger := NewLogger( + handler, + WithExtractor(func(ctx context.Context) []slog.Attr { + id, _ := ctx.Value(userIDKey{}).(string) + if id == "" { + return nil + } + return []slog.Attr{slog.String("user_id", id)} + }, func(ctx context.Context) []slog.Attr { + id, _ := ctx.Value(requestIDKey{}).(string) + if id == "" { + return nil + } + return []slog.Attr{slog.String("request_id", id)} + }), + ) + ctx := context.WithValue(context.Background(), userIDKey{}, "u1") + ctx = context.WithValue(ctx, requestIDKey{}, "r1") + logger.InfoContext(ctx, "hi") + var got map[string]any + _ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &got) + if got["user_id"] != "u1" { + t.Fatalf("user_id = %v", got["user_id"]) + } + if got["request_id"] != "r1" { + t.Fatalf("request_id = %v", got["request_id"]) + } +} diff --git a/log/context.go b/log/context.go new file mode 100644 index 0000000..990373f --- /dev/null +++ b/log/context.go @@ -0,0 +1,98 @@ +package log + +import ( + "context" + "log/slog" +) + +type ctxAttrsKey struct{} + +// ContextWithAttrs returns a copy of ctx with the given attrs attached. Attrs +// already on the context are preserved; new attrs are appended. +// +// Use [NewLogger] or [NewHandler] so these attrs are +// automatically added to every record handled with that context. +func ContextWithAttrs(ctx context.Context, attrs ...slog.Attr) context.Context { + if ctx == nil { + ctx = context.Background() + } + if len(attrs) == 0 { + return ctx + } + prev := AttrsFromContext(ctx) + merged := make([]slog.Attr, 0, len(prev)+len(attrs)) + merged = append(merged, prev...) + merged = append(merged, attrs...) + return context.WithValue(ctx, ctxAttrsKey{}, merged) +} + +// AttrsFromContext returns the attrs previously attached with [ContextWithAttrs]. +// The returned slice must not be mutated by callers. +func AttrsFromContext(ctx context.Context) []slog.Attr { + if ctx == nil { + return nil + } + attrs, _ := ctx.Value(ctxAttrsKey{}).([]slog.Attr) + return attrs +} + +func newContextHandler(next slog.Handler, extractors ...Extractor) slog.Handler { + if next == nil { + next = discardHandler{} + } + extractors = compactExtractors(extractors) + if len(extractors) == 0 { + return next + } + if h, ok := next.(*contextHandler); ok { + merged := make([]Extractor, 0, len(h.extractors)+len(extractors)) + merged = append(merged, h.extractors...) + merged = append(merged, extractors...) + return &contextHandler{next: h.next, extractors: merged} + } + return &contextHandler{next: next, extractors: extractors} +} + +func compactExtractors(extractors []Extractor) []Extractor { + out := extractors[:0] + for _, fn := range extractors { + if fn != nil { + out = append(out, fn) + } + } + return out +} + +type contextHandler struct { + next slog.Handler + extractors []Extractor +} + +func (h *contextHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.next.Enabled(ctx, level) +} + +func (h *contextHandler) Handle(ctx context.Context, record slog.Record) error { + attrs := h.attrs(ctx) + if len(attrs) > 0 { + record = record.Clone() + record.AddAttrs(attrs...) + } + return h.next.Handle(ctx, record) +} + +func (h *contextHandler) attrs(ctx context.Context) []slog.Attr { + attrs := make([]slog.Attr, 0, len(h.extractors)) + for _, fn := range h.extractors { + attrs = append(attrs, fn(ctx)...) + } + return attrs +} + +func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &contextHandler{next: h.next.WithAttrs(attrs), extractors: h.extractors} +} + +func (h *contextHandler) WithGroup(name string) slog.Handler { + return &contextHandler{next: h.next.WithGroup(name), extractors: h.extractors} +} diff --git a/log/context_test.go b/log/context_test.go new file mode 100644 index 0000000..18ea2d2 --- /dev/null +++ b/log/context_test.go @@ -0,0 +1,63 @@ +package log + +import ( + "context" + "log/slog" + "testing" + "time" +) + +func TestContextWithAttrsAppendsAttrs(t *testing.T) { + ctx := ContextWithAttrs(context.Background(), slog.String("a", "1")) + ctx = ContextWithAttrs(ctx, slog.String("b", "2")) + + attrs := AttrsFromContext(ctx) + if len(attrs) != 2 { + t.Fatalf("len(attrs) = %d, want 2", len(attrs)) + } + if attrs[0].Key != "a" || attrs[0].Value.String() != "1" { + t.Fatalf("attrs[0] = %v", attrs[0]) + } + if attrs[1].Key != "b" || attrs[1].Value.String() != "2" { + t.Fatalf("attrs[1] = %v", attrs[1]) + } +} + +func TestContextWithAttrsNilCtx(t *testing.T) { + ctx := ContextWithAttrs(nil, slog.Int("n", 1)) //nolint:staticcheck + if attrs := AttrsFromContext(ctx); len(attrs) != 1 { + t.Fatalf("len(attrs) = %d, want 1", len(attrs)) + } +} + +func TestContextHandlerMerges(t *testing.T) { + capture := &captureHandler{} + h := newContextHandler(capture, AttrsFromContext) + + ctx := ContextWithAttrs(context.Background(), slog.String("trace_id", "t1")) + record := slog.NewRecord(now(), LevelInfo, "msg", 0) + record.AddAttrs(slog.String("k", "v")) + if err := h.Handle(ctx, record); err != nil { + t.Fatalf("handle: %v", err) + } + if got := capture.attrs[0]["trace_id"]; got != "t1" { + t.Fatalf("trace_id = %v", got) + } + if got := capture.attrs[0]["k"]; got != "v" { + t.Fatalf("k = %v", got) + } +} + +func TestContextHandlerNoAttrs(t *testing.T) { + capture := &captureHandler{} + h := newContextHandler(capture, AttrsFromContext) + record := slog.NewRecord(now(), LevelInfo, "msg", 0) + if err := h.Handle(context.Background(), record); err != nil { + t.Fatalf("handle: %v", err) + } + if len(capture.records) != 1 { + t.Fatalf("records = %d", len(capture.records)) + } +} + +func now() time.Time { return time.Now() } diff --git a/log/filter.go b/log/filter.go new file mode 100644 index 0000000..97c2fde --- /dev/null +++ b/log/filter.go @@ -0,0 +1,151 @@ +package log + +import ( + "context" + "log/slog" + "strings" +) + +const redactedValue = "***" + +// FilterOption configures filtering in [WithFilter]. +type FilterOption func(*filterConfig) + +type filterConfig struct { + keys map[string]struct{} + filter func(ctx context.Context, record slog.Record) bool +} + +// FilterKey redacts the values of attributes whose key matches any of the +// provided keys. Keys may be leaf names ("password") or dotted group paths +// ("user.password"). +func FilterKey(keys ...string) FilterOption { + return func(c *filterConfig) { + if c.keys == nil { + c.keys = make(map[string]struct{}, len(keys)) + } + for _, k := range keys { + c.keys[k] = struct{}{} + } + } +} + +// FilterFunc drops records for which fn returns true. fn is evaluated after key +// redaction. +func FilterFunc(fn func(ctx context.Context, record slog.Record) bool) FilterOption { + return func(c *filterConfig) { c.filter = fn } +} + +func newFilterHandler(next slog.Handler, opts ...FilterOption) slog.Handler { + if next == nil { + next = discardHandler{} + } + cfg := &filterConfig{} + for _, o := range opts { + o(cfg) + } + if len(cfg.keys) == 0 && cfg.filter == nil { + return next + } + return &filterHandler{next: next, cfg: cfg} +} + +type filterHandler struct { + next slog.Handler + cfg *filterConfig + groups []string +} + +func (h *filterHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.next.Enabled(ctx, level) +} + +func (h *filterHandler) Handle(ctx context.Context, record slog.Record) error { + if h.needsRewrite() { + record = h.rewrite(record) + } + if h.cfg.filter != nil && h.cfg.filter(ctx, record) { + return nil + } + return h.next.Handle(ctx, record) +} + +func (h *filterHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + if h.needsRewrite() { + attrs = h.redactAttrs(h.groups, attrs) + } + next := *h + next.next = h.next.WithAttrs(attrs) + return &next +} + +func (h *filterHandler) WithGroup(name string) slog.Handler { + next := *h + next.groups = append(append([]string{}, h.groups...), name) + next.next = h.next.WithGroup(name) + return &next +} + +func (h *filterHandler) needsRewrite() bool { + return len(h.cfg.keys) > 0 +} + +func (h *filterHandler) rewrite(record slog.Record) slog.Record { + cloned := record.Clone() + attrs := make([]slog.Attr, 0, cloned.NumAttrs()) + cloned.Attrs(func(a slog.Attr) bool { + attrs = append(attrs, a) + return true + }) + redacted := h.redactAttrs(h.groups, attrs) + out := slog.NewRecord(cloned.Time, cloned.Level, cloned.Message, cloned.PC) + out.AddAttrs(redacted...) + return out +} + +func (h *filterHandler) redactAttrs(groups []string, attrs []slog.Attr) []slog.Attr { + out := make([]slog.Attr, len(attrs)) + for i, a := range attrs { + out[i] = h.redactAttr(groups, a) + } + return out +} + +func (h *filterHandler) redactAttr(groups []string, a slog.Attr) slog.Attr { + a.Value = a.Value.Resolve() + if a.Value.Kind() == slog.KindGroup { + group := a.Value.Group() + next := make([]slog.Attr, len(group)) + nextGroups := appendPath(groups, a.Key) + for i, ga := range group { + next[i] = h.redactAttr(nextGroups, ga) + } + return slog.Attr{Key: a.Key, Value: slog.GroupValue(next...)} + } + if h.matchesKey(groups, a.Key) { + return slog.Attr{Key: a.Key, Value: slog.StringValue(redactedValue)} + } + return a +} + +func (h *filterHandler) matchesKey(groups []string, key string) bool { + if _, ok := h.cfg.keys[key]; ok { + return true + } + if len(groups) == 0 { + return false + } + path := strings.Join(appendPath(groups, key), ".") + _, ok := h.cfg.keys[path] + return ok +} + +func appendPath(groups []string, key string) []string { + if key == "" { + return groups + } + next := make([]string, 0, len(groups)+1) + next = append(next, groups...) + next = append(next, key) + return next +} diff --git a/log/filter_test.go b/log/filter_test.go new file mode 100644 index 0000000..0f0518f --- /dev/null +++ b/log/filter_test.go @@ -0,0 +1,96 @@ +package log + +import ( + "bytes" + "context" + "encoding/json" + "log/slog" + "testing" + "time" +) + +func TestFilterKeyRedacts(t *testing.T) { + capture := &captureHandler{} + h := newFilterHandler(capture, FilterKey("password")) + record := slog.NewRecord(time.Now(), LevelInfo, "login", 0) + record.AddAttrs(slog.String("user", "alice"), slog.String("password", "secret")) + _ = h.Handle(context.Background(), record) + + if got := capture.attrs[0]["password"]; got != redactedValue { + t.Fatalf("password = %v, want %q", got, redactedValue) + } + if got := capture.attrs[0]["user"]; got != "alice" { + t.Fatalf("user = %v", got) + } +} + +func TestFilterFuncDrops(t *testing.T) { + capture := &captureHandler{} + h := newFilterHandler(capture, FilterFunc(func(_ context.Context, r slog.Record) bool { + return r.Message == "drop" + })) + r1 := slog.NewRecord(time.Now(), LevelInfo, "drop", 0) + r2 := slog.NewRecord(time.Now(), LevelInfo, "keep", 0) + _ = h.Handle(context.Background(), r1) + _ = h.Handle(context.Background(), r2) + if len(capture.records) != 1 || capture.records[0].Message != "keep" { + t.Fatalf("records = %+v", capture.records) + } +} + +func TestFilterKeyRedactsGroupPath(t *testing.T) { + var buf bytes.Buffer + h := newFilterHandler(slog.NewJSONHandler(&buf, nil), FilterKey("user.password")) + logger := slog.New(h) + + logger.Info("login", slog.Group("user", + slog.String("name", "alice"), + slog.String("password", "secret"), + )) + + got := decodeJSONLog(t, &buf) + user, ok := got["user"].(map[string]any) + if !ok { + t.Fatalf("user attr = %#v, want object", got["user"]) + } + if user["password"] != redactedValue { + t.Fatalf("password = %v, want %q", user["password"], redactedValue) + } + if user["name"] != "alice" { + t.Fatalf("name = %v, want alice", user["name"]) + } +} + +func TestFilterKeyRedactsHandlerGroupPath(t *testing.T) { + var buf bytes.Buffer + h := newFilterHandler(slog.NewJSONHandler(&buf, nil), FilterKey("request.password")) + logger := slog.New(h).WithGroup("request") + + logger.Info("login", "password", "secret") + + got := decodeJSONLog(t, &buf) + request, ok := got["request"].(map[string]any) + if !ok { + t.Fatalf("request attr = %#v, want object", got["request"]) + } + if request["password"] != redactedValue { + t.Fatalf("password = %v, want %q", request["password"], redactedValue) + } +} + +func TestFilterEmptyReturnsNext(t *testing.T) { + capture := &captureHandler{} + h := newFilterHandler(capture) + if h != capture { + t.Fatal("expected next returned as-is when no options") + } +} + +func decodeJSONLog(t *testing.T, buf *bytes.Buffer) map[string]any { + t.Helper() + var got map[string]any + if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &got); err != nil { + t.Fatalf("unmarshal log: %v (%q)", err, buf.String()) + } + return got +} diff --git a/log/handler.go b/log/handler.go new file mode 100644 index 0000000..115f9f0 --- /dev/null +++ b/log/handler.go @@ -0,0 +1,15 @@ +package log + +import ( + "context" + "log/slog" +) + +// discardHandler drops all records. It mirrors discardHandler{} from go1.24 +// while keeping the module compatible with go1.22. +type discardHandler struct{} + +func (discardHandler) Enabled(context.Context, slog.Level) bool { return false } +func (discardHandler) Handle(context.Context, slog.Record) error { return nil } +func (h discardHandler) WithAttrs([]slog.Attr) slog.Handler { return h } +func (h discardHandler) WithGroup(string) slog.Handler { return h } diff --git a/log/level.go b/log/level.go new file mode 100644 index 0000000..29beaf9 --- /dev/null +++ b/log/level.go @@ -0,0 +1,43 @@ +package log + +import ( + "log/slog" + "strings" +) + +// Level is a logger level. +type Level = slog.Level + +// Leveler provides a log level. +type Leveler = slog.Leveler + +// LevelVar is a variable log level. +type LevelVar = slog.LevelVar + +// LevelKey is logger level key. +const LevelKey = slog.LevelKey + +const ( + // LevelDebug is logger debug level. + LevelDebug Level = slog.LevelDebug + // LevelInfo is logger info level. + LevelInfo Level = slog.LevelInfo + // LevelWarn is logger warn level. + LevelWarn Level = slog.LevelWarn + // LevelError is logger error level. + LevelError Level = slog.LevelError + // LevelFatal is logger fatal level. + LevelFatal Level = slog.LevelError + 4 +) + +// ParseLevel parses a level string into a logger Level value. +func ParseLevel(s string) Level { + if strings.EqualFold(s, "FATAL") { + return LevelFatal + } + var level slog.Level + if err := level.UnmarshalText([]byte(s)); err == nil { + return level + } + return LevelInfo +} diff --git a/log/level_test.go b/log/level_test.go new file mode 100644 index 0000000..2503e66 --- /dev/null +++ b/log/level_test.go @@ -0,0 +1,39 @@ +package log + +import ( + "log/slog" + "testing" +) + +func TestLevelAliases(t *testing.T) { + if LevelDebug != slog.LevelDebug { + t.Fatalf("LevelDebug = %v, want %v", LevelDebug, slog.LevelDebug) + } + if LevelFatal != slog.LevelError+4 { + t.Fatalf("LevelFatal = %v, want %v", LevelFatal, slog.LevelError+4) + } +} + +func TestParseLevel(t *testing.T) { + tests := []struct { + name string + in string + want Level + }{ + {name: "debug", in: "debug", want: LevelDebug}, + {name: "info", in: "info", want: LevelInfo}, + {name: "warn", in: "warn", want: LevelWarn}, + {name: "error", in: "error", want: LevelError}, + {name: "fatal", in: "fatal", want: LevelFatal}, + {name: "custom", in: "INFO+1", want: LevelInfo + 1}, + {name: "default", in: "unknown", want: LevelInfo}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseLevel(tt.in); got != tt.want { + t.Fatalf("ParseLevel(%q) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..a30d880 --- /dev/null +++ b/log/log.go @@ -0,0 +1,119 @@ +package log + +import ( + "context" + "log/slog" + "runtime" + "time" +) + +// SetDefault sets the default logger used by the package-level helpers and by +// [slog.Default]. +func SetDefault(logger *slog.Logger) { + slog.SetDefault(logger) +} + +// Default returns the default logger. +func Default() *slog.Logger { + return slog.Default() +} + +// With returns a logger that includes the given attributes in each output +// operation. It mirrors [slog.Logger.With] on the default logger. +func With(args ...any) *slog.Logger { + return slog.With(args...) +} + +// WithGroup returns a logger that starts a group. It mirrors +// [slog.Logger.WithGroup] on the default logger. +func WithGroup(name string) *slog.Logger { + return Default().WithGroup(name) +} + +// Handler returns the default logger's handler. It mirrors +// [slog.Logger.Handler] on the default logger. +func Handler() slog.Handler { + return Default().Handler() +} + +// Enabled reports whether the default logger emits log records at the given +// context and level. It mirrors [slog.Logger.Enabled] on the default logger. +func Enabled(ctx context.Context, level Level) bool { + return Default().Enabled(ctx, level) +} + +// Debug logs at debug level. Signature mirrors [slog.Logger.Debug]. +func Debug(msg string, args ...any) { + log(context.Background(), LevelDebug, msg, args...) +} + +// DebugContext logs at debug level with the provided context. +func DebugContext(ctx context.Context, msg string, args ...any) { + log(ctx, LevelDebug, msg, args...) +} + +// Info logs at info level. Signature mirrors [slog.Logger.Info]. +func Info(msg string, args ...any) { + log(context.Background(), LevelInfo, msg, args...) +} + +// InfoContext logs at info level with the provided context. +func InfoContext(ctx context.Context, msg string, args ...any) { + log(ctx, LevelInfo, msg, args...) +} + +// Warn logs at warn level. Signature mirrors [slog.Logger.Warn]. +func Warn(msg string, args ...any) { + log(context.Background(), LevelWarn, msg, args...) +} + +// WarnContext logs at warn level with the provided context. +func WarnContext(ctx context.Context, msg string, args ...any) { + log(ctx, LevelWarn, msg, args...) +} + +// Error logs at error level. Signature mirrors [slog.Logger.Error]. +func Error(msg string, args ...any) { + log(context.Background(), LevelError, msg, args...) +} + +// ErrorContext logs at error level with the provided context. +func ErrorContext(ctx context.Context, msg string, args ...any) { + log(ctx, LevelError, msg, args...) +} + +// Log emits a record at the given level. It mirrors [slog.Logger.Log] on the +// default logger. +func Log(ctx context.Context, level Level, msg string, args ...any) { + log(ctx, level, msg, args...) +} + +// LogAttrs emits a typed-attr record at the given level. It mirrors +// [slog.Logger.LogAttrs] on the default logger. +// +//nolint:revive // LogAttrs intentionally mirrors slog.Logger.LogAttrs. +func LogAttrs(ctx context.Context, level Level, msg string, attrs ...slog.Attr) { + handler := slog.Default().Handler() + if !handler.Enabled(ctx, level) { + return + } + var pcs [1]uintptr + // Skip [runtime.Callers, LogAttrs]. + runtime.Callers(2, pcs[:]) + record := slog.NewRecord(time.Now(), level, msg, pcs[0]) + record.AddAttrs(attrs...) + _ = handler.Handle(ctx, record) +} + +func log(ctx context.Context, level Level, msg string, args ...any) { + handler := slog.Default().Handler() + if !handler.Enabled(ctx, level) { + return + } + var pcs [1]uintptr + // Skip [runtime.Callers, log, exported helper]. + runtime.Callers(3, pcs[:]) + record := slog.NewRecord(time.Now(), level, msg, pcs[0]) + record.Add(args...) + _ = handler.Handle(ctx, record) +} diff --git a/log/log_benchmark_test.go b/log/log_benchmark_test.go new file mode 100644 index 0000000..a085c92 --- /dev/null +++ b/log/log_benchmark_test.go @@ -0,0 +1,66 @@ +package log + +import ( + "context" + "log/slog" + "testing" +) + +type benchmarkHandler struct { + enabled bool +} + +func (h benchmarkHandler) Enabled(context.Context, slog.Level) bool { + return h.enabled +} + +func (h benchmarkHandler) Handle(context.Context, slog.Record) error { + return nil +} + +func (h benchmarkHandler) WithAttrs([]slog.Attr) slog.Handler { + return h +} + +func (h benchmarkHandler) WithGroup(string) slog.Handler { + return h +} + +func benchmarkWithDefault(b *testing.B, h slog.Handler) { + b.Helper() + old := Default() + SetDefault(slog.New(h)) + b.Cleanup(func() { + SetDefault(old) + }) + b.ReportAllocs() +} + +func BenchmarkInfoDisabled(b *testing.B) { + benchmarkWithDefault(b, benchmarkHandler{}) + for i := 0; i < b.N; i++ { + Info("hello", "kind", "test") + } +} + +func BenchmarkInfoEnabled(b *testing.B) { + benchmarkWithDefault(b, benchmarkHandler{enabled: true}) + for i := 0; i < b.N; i++ { + Info("hello", "kind", "test") + } +} + +func BenchmarkLogAttrsEnabled(b *testing.B) { + benchmarkWithDefault(b, benchmarkHandler{enabled: true}) + attr := slog.String("kind", "test") + for i := 0; i < b.N; i++ { + LogAttrs(context.Background(), LevelInfo, "hello", attr) + } +} + +func BenchmarkSlogInfoEnabled(b *testing.B) { + benchmarkWithDefault(b, benchmarkHandler{enabled: true}) + for i := 0; i < b.N; i++ { + slog.Info("hello", "kind", "test") + } +} diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 0000000..9929815 --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,246 @@ +package log + +import ( + "bytes" + "context" + "encoding/json" + "log/slog" + "strings" + "testing" +) + +func TestSetDefault(t *testing.T) { + old := Default() + defer SetDefault(old) + + handler := &captureHandler{} + logger := slog.New(handler) + SetDefault(logger) + + if got := Default(); got != logger { + t.Fatalf("Default() = %v, want %v", got, logger) + } + + Info("hello", "kind", "test") + + if len(handler.records) != 1 { + t.Fatalf("records len = %d, want 1", len(handler.records)) + } + if handler.records[0].Message != "hello" { + t.Fatalf("message = %q, want %q", handler.records[0].Message, "hello") + } + if got := handler.attrs[0]["kind"]; got != "test" { + t.Fatalf("kind = %v, want %q", got, "test") + } +} + +func TestWith(t *testing.T) { + old := Default() + defer SetDefault(old) + + handler := &captureHandler{} + SetDefault(slog.New(handler)) + + logger := With("service", "kratos", slog.Int("version", 2)) + logger.Info("hello", "kind", "test") + + if len(handler.records) != 1 { + t.Fatalf("records len = %d, want 1", len(handler.records)) + } + if got := handler.attrs[0]["service"]; got != "kratos" { + t.Fatalf("service = %v, want %q", got, "kratos") + } + if got := handler.attrs[0]["version"]; got != int64(2) { + t.Fatalf("version = %v, want 2", got) + } + if got := handler.attrs[0]["kind"]; got != "test" { + t.Fatalf("kind = %v, want %q", got, "test") + } +} + +func TestWithGroup(t *testing.T) { + old := Default() + defer SetDefault(old) + + var out bytes.Buffer + SetDefault(slog.New(slog.NewJSONHandler(&out, nil))) + + logger := WithGroup("request") + logger.Info("hello", "id", "r1") + + var got map[string]any + if err := json.Unmarshal(bytes.TrimSpace(out.Bytes()), &got); err != nil { + t.Fatalf("unmarshal: %v (%q)", err, out.String()) + } + group, ok := got["request"].(map[string]any) + if !ok { + t.Fatalf("request group = %v, want object", got["request"]) + } + if group["id"] != "r1" { + t.Fatalf("request.id = %v, want r1", group["id"]) + } +} + +func TestHandler(t *testing.T) { + old := Default() + defer SetDefault(old) + + handler := &captureHandler{} + SetDefault(slog.New(handler)) + + if got := Handler(); got != handler { + t.Fatalf("Handler() = %v, want %v", got, handler) + } +} + +func TestEnabled(t *testing.T) { + old := Default() + defer SetDefault(old) + + var out bytes.Buffer + SetDefault(slog.New(slog.NewTextHandler(&out, &slog.HandlerOptions{Level: LevelWarn}))) + + if Enabled(context.Background(), LevelInfo) { + t.Fatal("Enabled(_, LevelInfo) = true, want false") + } + if !Enabled(context.Background(), LevelWarn) { + t.Fatal("Enabled(_, LevelWarn) = false, want true") + } +} + +func TestLog(t *testing.T) { + old := Default() + defer SetDefault(old) + + handler := &captureHandler{} + SetDefault(slog.New(handler)) + + Log(context.Background(), LevelInfo, "hello", "kind", "test") + + if len(handler.records) != 1 { + t.Fatalf("records len = %d, want 1", len(handler.records)) + } + if handler.records[0].Message != "hello" { + t.Fatalf("message = %q, want %q", handler.records[0].Message, "hello") + } + if got := handler.attrs[0]["kind"]; got != "test" { + t.Fatalf("kind = %v, want %q", got, "test") + } +} + +func TestLogAttrs(t *testing.T) { + old := Default() + defer SetDefault(old) + + handler := &captureHandler{} + SetDefault(slog.New(handler)) + + LogAttrs(context.Background(), LevelWarn, "msg", slog.Int("n", 42)) + + if got := handler.attrs[0]["n"]; got != int64(42) { + t.Fatalf("n = %v, want 42", got) + } + if handler.records[0].Level != LevelWarn { + t.Fatalf("level = %v, want %v", handler.records[0].Level, LevelWarn) + } +} + +func TestInfoContextPropagatesAttrs(t *testing.T) { + old := Default() + defer SetDefault(old) + + handler := &captureHandler{} + SetDefault(NewLogger(handler)) + + ctx := ContextWithAttrs(context.Background(), slog.String("trace_id", "abc")) + InfoContext(ctx, "hello") + + if got := handler.attrs[0]["trace_id"]; got != "abc" { + t.Fatalf("trace_id = %v, want abc", got) + } +} + +func TestPackageHelpersReportCaller(t *testing.T) { + old := Default() + defer SetDefault(old) + + var out bytes.Buffer + SetDefault(slog.New(NewHandler( + WithWriter(&out), + WithFormat(FormatJSON), + WithAddSource(true), + ))) + + Info("source") + assertLogSource(t, &out) + + out.Reset() + LogAttrs(context.Background(), LevelInfo, "source") + assertLogSource(t, &out) +} + +func assertLogSource(t *testing.T, out *bytes.Buffer) { + t.Helper() + var got map[string]any + if err := json.Unmarshal(bytes.TrimSpace(out.Bytes()), &got); err != nil { + t.Fatalf("unmarshal: %v (%q)", err, out.String()) + } + source, ok := got["source"].(map[string]any) + if !ok { + t.Fatalf("source = %v, want object", got["source"]) + } + file, _ := source["file"].(string) + if !strings.HasSuffix(file, "log/log_test.go") { + t.Fatalf("source.file = %q, want log/log_test.go suffix", file) + } +} + +type captureHandler struct { + parent *captureHandler + prefix []slog.Attr + groups []string + + records []slog.Record + attrs []map[string]any +} + +func (h *captureHandler) root() *captureHandler { + if h.parent != nil { + return h.parent.root() + } + return h +} + +func (h *captureHandler) Enabled(context.Context, slog.Level) bool { + return true +} + +func (h *captureHandler) Handle(_ context.Context, record slog.Record) error { + attrs := make(map[string]any) + for _, a := range h.collectPrefix() { + attrs[a.Key] = a.Value.Any() + } + record.Attrs(func(attr slog.Attr) bool { + attrs[attr.Key] = attr.Value.Any() + return true + }) + root := h.root() + root.records = append(root.records, record.Clone()) + root.attrs = append(root.attrs, attrs) + return nil +} + +func (h *captureHandler) collectPrefix() []slog.Attr { + if h.parent == nil { + return h.prefix + } + return append(h.parent.collectPrefix(), h.prefix...) +} + +func (h *captureHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &captureHandler{parent: h, prefix: append([]slog.Attr{}, attrs...)} +} + +func (h *captureHandler) WithGroup(name string) slog.Handler { + return &captureHandler{parent: h, groups: append([]string{}, name)} +} diff --git a/metadata/metadata.go b/metadata/metadata.go new file mode 100644 index 0000000..ccccb9c --- /dev/null +++ b/metadata/metadata.go @@ -0,0 +1,126 @@ +package metadata + +import ( + "context" + "fmt" + "slices" + "strings" +) + +// Metadata is our way of representing request headers internally. +// They're used at the RPC level and translate back and forth +// from Transport headers. +type Metadata map[string][]string + +// New creates an MD from a given key-values map. +func New(mds ...map[string][]string) Metadata { + md := Metadata{} + for _, m := range mds { + for k, vList := range m { + for _, v := range vList { + md.Add(k, v) + } + } + } + return md +} + +// Add adds the key, value pair to the header. +func (m Metadata) Add(key, value string) { + if key == "" { + return + } + + lowerKey := strings.ToLower(key) + m[lowerKey] = append(m[lowerKey], value) +} + +// Get returns the value associated with the passed key. +func (m Metadata) Get(key string) string { + v := m[strings.ToLower(key)] + if len(v) == 0 { + return "" + } + return v[0] +} + +// Set stores the key-value pair. +func (m Metadata) Set(key string, value string) { + if key == "" || value == "" { + return + } + m[strings.ToLower(key)] = []string{value} +} + +// Range iterate over element in metadata. +func (m Metadata) Range(f func(k string, v []string) bool) { + for k, v := range m { + if !f(k, v) { + break + } + } +} + +// Values returns a slice of values associated with the passed key. +func (m Metadata) Values(key string) []string { + return m[strings.ToLower(key)] +} + +// Clone returns a deep copy of Metadata +func (m Metadata) Clone() Metadata { + md := make(Metadata, len(m)) + for k, v := range m { + md[k] = slices.Clone(v) + } + return md +} + +type serverMetadataKey struct{} + +// NewServerContext creates a new context with client md attached. +func NewServerContext(ctx context.Context, md Metadata) context.Context { + return context.WithValue(ctx, serverMetadataKey{}, md) +} + +// FromServerContext returns the server metadata in ctx if it exists. +func FromServerContext(ctx context.Context) (Metadata, bool) { + md, ok := ctx.Value(serverMetadataKey{}).(Metadata) + return md, ok +} + +type clientMetadataKey struct{} + +// NewClientContext creates a new context with client md attached. +func NewClientContext(ctx context.Context, md Metadata) context.Context { + return context.WithValue(ctx, clientMetadataKey{}, md) +} + +// FromClientContext returns the client metadata in ctx if it exists. +func FromClientContext(ctx context.Context) (Metadata, bool) { + md, ok := ctx.Value(clientMetadataKey{}).(Metadata) + return md, ok +} + +// AppendToClientContext returns a new context with the provided kv merged +// with any existing metadata in the context. +func AppendToClientContext(ctx context.Context, kv ...string) context.Context { + if len(kv)%2 == 1 { + panic(fmt.Sprintf("metadata: AppendToClientContext got an odd number of input pairs for metadata: %d", len(kv))) + } + md, _ := FromClientContext(ctx) + md = md.Clone() + for i := 0; i < len(kv); i += 2 { + md.Set(kv[i], kv[i+1]) + } + return NewClientContext(ctx, md) +} + +// MergeToClientContext merge new metadata into ctx. +func MergeToClientContext(ctx context.Context, cmd Metadata) context.Context { + md, _ := FromClientContext(ctx) + md = md.Clone() + for k, v := range cmd { + md[k] = v + } + return NewClientContext(ctx, md) +} diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go new file mode 100644 index 0000000..fc44c75 --- /dev/null +++ b/metadata/metadata_test.go @@ -0,0 +1,435 @@ +package metadata + +import ( + "context" + "reflect" + "testing" +) + +func TestNew(t *testing.T) { + type args struct { + mds []map[string][]string + } + tests := []struct { + name string + args args + want Metadata + }{ + { + name: "hello", + args: args{[]map[string][]string{{"hello": {"kratos"}}, {"hello2": {"go-kratos"}}}}, + want: Metadata{"hello": {"kratos"}, "hello2": {"go-kratos"}}, + }, + { + name: "hi", + args: args{[]map[string][]string{{"hi": {"kratos"}}, {"hi2": {"go-kratos"}}}}, + want: Metadata{"hi": {"kratos"}, "hi2": {"go-kratos"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := New(tt.args.mds...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMetadata_Get(t *testing.T) { + type args struct { + key string + } + tests := []struct { + name string + m Metadata + args args + want string + }{ + { + name: "kratos", + m: Metadata{"kratos": {"value"}, "env": {"dev"}}, + args: args{key: "kratos"}, + want: "value", + }, + { + name: "env", + m: Metadata{"kratos": {"value"}, "env": {"dev"}}, + args: args{key: "env"}, + want: "dev", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.m.Get(tt.args.key); got != tt.want { + t.Errorf("Get() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMetadata_Values(t *testing.T) { + type args struct { + key string + } + tests := []struct { + name string + m Metadata + args args + want []string + }{ + { + name: "kratos", + m: Metadata{"kratos": {"value", "value2"}, "env": {"dev"}}, + args: args{key: "kratos"}, + want: []string{"value", "value2"}, + }, + { + name: "env", + m: Metadata{"kratos": {"value", "value2"}, "env": {"dev"}}, + args: args{key: "env"}, + want: []string{"dev"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.m.Values(tt.args.key); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Get() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMetadata_Set(t *testing.T) { + type args struct { + key string + value string + } + tests := []struct { + name string + m Metadata + args args + want Metadata + }{ + { + name: "kratos", + m: Metadata{}, + args: args{key: "hello", value: "kratos"}, + want: Metadata{"hello": {"kratos"}}, + }, + { + name: "env", + m: Metadata{"hello": {"kratos"}}, + args: args{key: "env", value: "pro"}, + want: Metadata{"hello": {"kratos"}, "env": {"pro"}}, + }, + { + name: "empty", + m: Metadata{}, + args: args{key: "", value: ""}, + want: Metadata{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.m.Set(tt.args.key, tt.args.value) + if !reflect.DeepEqual(tt.m, tt.want) { + t.Errorf("Set() = %v, want %v", tt.m, tt.want) + } + }) + } +} + +func TestMetadata_Add(t *testing.T) { + type args struct { + key string + value string + } + tests := []struct { + name string + m Metadata + args args + want Metadata + }{ + { + name: "kratos", + m: Metadata{}, + args: args{key: "hello", value: "kratos"}, + want: Metadata{"hello": {"kratos"}}, + }, + { + name: "env", + m: Metadata{"hello": {"kratos"}}, + args: args{key: "hello", value: "again"}, + want: Metadata{"hello": {"kratos", "again"}}, + }, + { + name: "empty", + m: Metadata{}, + args: args{key: "", value: ""}, + want: Metadata{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.m.Add(tt.args.key, tt.args.value) + if !reflect.DeepEqual(tt.m, tt.want) { + t.Errorf("Set() = %v, want %v", tt.m, tt.want) + } + }) + } +} + +func TestClientContext(t *testing.T) { + type args struct { + ctx context.Context + md Metadata + } + tests := []struct { + name string + args args + }{ + { + name: "kratos", + args: args{context.Background(), Metadata{"hello": {"kratos"}, "kratos": {"https://go-kratos.dev"}}}, + }, + { + name: "hello", + args: args{context.Background(), Metadata{"hello": {"kratos"}, "hello2": {"https://go-kratos.dev"}}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := NewClientContext(tt.args.ctx, tt.args.md) + m, ok := FromClientContext(ctx) + if !ok { + t.Errorf("FromClientContext() = %v, want %v", ok, true) + } + + if !reflect.DeepEqual(m, tt.args.md) { + t.Errorf("meta = %v, want %v", m, tt.args.md) + } + }) + } +} + +func TestServerContext(t *testing.T) { + type args struct { + ctx context.Context + md Metadata + } + tests := []struct { + name string + args args + }{ + { + name: "kratos", + args: args{context.Background(), Metadata{"hello": {"kratos"}, "kratos": {"https://go-kratos.dev"}}}, + }, + { + name: "hello", + args: args{context.Background(), Metadata{"hello": {"kratos"}, "hello2": {"https://go-kratos.dev"}}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := NewServerContext(tt.args.ctx, tt.args.md) + m, ok := FromServerContext(ctx) + if !ok { + t.Errorf("FromServerContext() = %v, want %v", ok, true) + } + + if !reflect.DeepEqual(m, tt.args.md) { + t.Errorf("meta = %v, want %v", m, tt.args.md) + } + }) + } +} + +func TestAppendToClientContext(t *testing.T) { + type args struct { + md Metadata + kv []string + } + tests := []struct { + name string + args args + want Metadata + }{ + { + name: "kratos", + args: args{Metadata{}, []string{"hello", "kratos", "env", "dev"}}, + want: Metadata{"hello": {"kratos"}, "env": {"dev"}}, + }, + { + name: "hello", + args: args{Metadata{"hi": {"https://go-kratos.dev/"}}, []string{"hello", "kratos", "env", "dev"}}, + want: Metadata{"hello": {"kratos"}, "env": {"dev"}, "hi": {"https://go-kratos.dev/"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := NewClientContext(context.Background(), tt.args.md) + ctx = AppendToClientContext(ctx, tt.args.kv...) + md, ok := FromClientContext(ctx) + if !ok { + t.Errorf("FromServerContext() = %v, want %v", ok, true) + } + if !reflect.DeepEqual(md, tt.want) { + t.Errorf("metadata = %v, want %v", md, tt.want) + } + }) + } +} + +// nolint directives: sa5012 +func TestAppendToClientContextThatPanics(t *testing.T) { + kvs := []string{"hello", "kratos", "env"} + defer func() { + if r := recover(); r == nil { + t.Errorf("append to client context singular kvs did not panic") + } + }() + ctx := NewClientContext(context.Background(), Metadata{}) + ctx = AppendToClientContext(ctx, kvs...) + md, ok := FromClientContext(ctx) + if !ok { + t.Errorf("FromServerContext() = %v, want %v", ok, true) + } + if !reflect.DeepEqual(md, Metadata{}) { + t.Errorf("metadata = %v, want %v", md, Metadata{}) + } +} + +func TestMergeToClientContext(t *testing.T) { + type args struct { + md Metadata + appendMd Metadata + } + tests := []struct { + name string + args args + want Metadata + }{ + { + name: "kratos", + args: args{Metadata{}, Metadata{"hello": {"kratos"}, "env": {"dev"}}}, + want: Metadata{"hello": {"kratos"}, "env": {"dev"}}, + }, + { + name: "hello", + args: args{Metadata{"hi": {"https://go-kratos.dev/"}}, Metadata{"hello": {"kratos"}, "env": {"dev"}}}, + want: Metadata{"hello": {"kratos"}, "env": {"dev"}, "hi": {"https://go-kratos.dev/"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := NewClientContext(context.Background(), tt.args.md) + ctx = MergeToClientContext(ctx, tt.args.appendMd) + md, ok := FromClientContext(ctx) + if !ok { + t.Errorf("FromServerContext() = %v, want %v", ok, true) + } + if !reflect.DeepEqual(md, tt.want) { + t.Errorf("metadata = %v, want %v", md, tt.want) + } + }) + } +} + +func TestMetadata_Range(t *testing.T) { + md := Metadata{"kratos": {"kratos"}, "https://go-kratos.dev/": {"https://go-kratos.dev/"}, "go-kratos": {"go-kratos"}} + tmp := Metadata{} + md.Range(func(k string, v []string) bool { + if k == "https://go-kratos.dev/" || k == "kratos" { + tmp[k] = v + } + return true + }) + if !reflect.DeepEqual(tmp, Metadata{"https://go-kratos.dev/": {"https://go-kratos.dev/"}, "kratos": {"kratos"}}) { + t.Errorf("metadata = %v, want %v", tmp, Metadata{"https://go-kratos.dev/": {"https://go-kratos.dev/"}, "kratos": {"kratos"}}) + } + tmp = Metadata{} + md.Range(func(string, []string) bool { + return false + }) + if !reflect.DeepEqual(tmp, Metadata{}) { + t.Errorf("metadata = %v, want %v", tmp, Metadata{}) + } +} + +func TestMetadata_Clone(t *testing.T) { + tests := []struct { + name string + m Metadata + want Metadata + }{ + { + name: "kratos", + m: Metadata{"kratos": {"kratos"}, "https://go-kratos.dev/": {"https://go-kratos.dev/"}, "go-kratos": {"go-kratos"}}, + want: Metadata{"kratos": {"kratos"}, "https://go-kratos.dev/": {"https://go-kratos.dev/"}, "go-kratos": {"go-kratos"}}, + }, + { + name: "go", + m: Metadata{"language": {"golang"}}, + want: Metadata{"language": {"golang"}}, + }, + { + name: "plan9", + m: Metadata{"k0": []string{}, "k1": nil}, + want: Metadata{"k0": []string{}, "k1": nil}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.m.Clone() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Clone() = %v, want %v", got, tt.want) + } + got["kratos"] = []string{"go"} + if reflect.DeepEqual(got, tt.want) { + t.Errorf("want got != want got %v want %v", got, tt.want) + } + }) + } +} + +// TestMetadata_CloneDeepCopy tests that Clone creates a deep copy of metadata, +// so modifications to the original metadata's slices don't affect the cloned one. +func TestMetadata_CloneDeepCopy(t *testing.T) { + original := Metadata{ + "test-key": {"value1", "value2", "value3"}, + "single-key": {"single-value"}, + } + + cloned := original.Clone() + + // Test 1: Modify an element in the original metadata's slice + { + original["test-key"][1] = "modified-value" + + // Verify that the cloned metadata's slice is not affected + if cloned["test-key"][1] != "value2" { + t.Errorf("Clone() modify leaked: original=%v, cloned=%v", original["test-key"], cloned["test-key"]) + } + } + + // Test 2: Append to the original metadata's slice + { + original["test-key"] = append(original["test-key"], "new-value") + if len(cloned["test-key"]) != 3 { + t.Errorf("Clone() append leaked: original len=%d, cloned len=%d", len(original["test-key"]), len(cloned["test-key"])) + } + expected := []string{"value1", "value2", "value3"} + if !reflect.DeepEqual(cloned["test-key"], expected) { + t.Errorf("Clone() append values: got=%v, want=%v", cloned["test-key"], expected) + } + } + + // Test 3: Replace the entire slice in the original metadata + { + original["single-key"] = []string{"replaced-value"} + if cloned["single-key"][0] != "single-value" { + t.Errorf("Clone() replace leaked: original=%v, cloned=%v", original["single-key"], cloned["single-key"]) + } + } +} diff --git a/middleware/circuitbreaker/circuitbreaker.go b/middleware/circuitbreaker/circuitbreaker.go new file mode 100644 index 0000000..5aa7fea --- /dev/null +++ b/middleware/circuitbreaker/circuitbreaker.go @@ -0,0 +1,68 @@ +package circuitbreaker + +import ( + "context" + + "github.com/go-kratos/kratos/v3/errors" + internalbreaker "github.com/go-kratos/kratos/v3/internal/circuitbreaker" + "github.com/go-kratos/kratos/v3/internal/group" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +// ErrNotAllowed is request failed due to circuit breaker triggered. +var ErrNotAllowed = errors.New(503, "CIRCUITBREAKER", "request failed due to circuit breaker triggered") + +// CircuitBreaker is a circuit breaker. +type CircuitBreaker = internalbreaker.CircuitBreaker + +// Option is circuit breaker option. +type Option func(*options) + +// WithBreakerFactory configures a factory used to lazily create one circuit breaker per operation. +func WithBreakerFactory(factory func() CircuitBreaker) Option { + return func(o *options) { + if factory == nil { + return + } + o.group = group.NewGroup(factory) + } +} + +type options struct { + group *group.Group[CircuitBreaker] +} + +// Client circuitbreaker middleware will return errBreakerTriggered when the circuit +// breaker is triggered and the request is rejected directly. +func Client(opts ...Option) middleware.Middleware { + opt := &options{ + group: group.NewGroup(func() CircuitBreaker { + return internalbreaker.NewBreaker() + }), + } + for _, o := range opts { + o(opt) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + info, _ := transport.FromClientContext(ctx) + breaker := opt.group.Get(info.Operation()) + if err := breaker.Allow(); err != nil { + // rejected + // NOTE: when client reject requests locally, + // continue to add counter let the drop ratio higher. + breaker.MarkFailed() + return nil, ErrNotAllowed + } + // allowed + reply, err := handler(ctx, req) + if err != nil && (errors.IsInternalServer(err) || errors.IsServiceUnavailable(err) || errors.IsGatewayTimeout(err)) { + breaker.MarkFailed() + } else { + breaker.MarkSuccess() + } + return reply, err + } + } +} diff --git a/middleware/circuitbreaker/circuitbreaker_test.go b/middleware/circuitbreaker/circuitbreaker_test.go new file mode 100644 index 0000000..9520b2d --- /dev/null +++ b/middleware/circuitbreaker/circuitbreaker_test.go @@ -0,0 +1,133 @@ +package circuitbreaker + +import ( + "context" + stderrors "errors" + "testing" + + kratoserrors "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/transport" +) + +type transportMock struct { + kind transport.Kind + endpoint string + operation string +} + +type circuitBreakerMock struct { + err error + success int + failed int +} + +func (tr *transportMock) Kind() transport.Kind { + return tr.kind +} + +func (tr *transportMock) Endpoint() string { + return tr.endpoint +} + +func (tr *transportMock) Operation() string { + return tr.operation +} + +func (tr *transportMock) RequestHeader() transport.Header { + return nil +} + +func (tr *transportMock) ReplyHeader() transport.Header { + return nil +} + +func (c *circuitBreakerMock) Allow() error { return c.err } +func (c *circuitBreakerMock) MarkSuccess() { c.success++ } +func (c *circuitBreakerMock) MarkFailed() { c.failed++ } + +func TestWithBreakerFactory(t *testing.T) { + var created int + var o options + + WithBreakerFactory(func() CircuitBreaker { + created++ + return &circuitBreakerMock{} + })(&o) + + if o.group == nil { + t.Fatal("breaker group must be updated") + } + first := o.group.Get("/foo") + second := o.group.Get("/foo") + third := o.group.Get("/bar") + if first != second { + t.Fatal("same operation must reuse the same circuit breaker") + } + if first == third { + t.Fatal("different operations must use different circuit breakers") + } + if created != 2 { + t.Fatalf("factory created %d breakers, want 2", created) + } +} + +func TestClient(t *testing.T) { + breaker := &circuitBreakerMock{} + ctx := transport.NewClientContext(context.Background(), &transportMock{operation: "/foo"}) + next := func(context.Context, any) (any, error) { + return "Hello valid", nil + } + + reply, err := Client(WithBreakerFactory(func() CircuitBreaker { + return breaker + }))(next)(ctx, nil) + if err != nil { + t.Fatal(err) + } + if reply != "Hello valid" { + t.Fatalf("reply = %v, want Hello valid", reply) + } + if breaker.success != 1 || breaker.failed != 0 { + t.Fatalf("breaker success=%d failed=%d, want success=1 failed=0", breaker.success, breaker.failed) + } +} + +func TestClientRejectsWhenBreakerIsOpen(t *testing.T) { + breaker := &circuitBreakerMock{err: stderrors.New("circuitbreaker error")} + ctx := transport.NewClientContext(context.Background(), &transportMock{operation: "/foo"}) + called := false + next := func(context.Context, any) (any, error) { + called = true + return nil, nil + } + + _, err := Client(WithBreakerFactory(func() CircuitBreaker { + return breaker + }))(next)(ctx, nil) + + if !stderrors.Is(err, ErrNotAllowed) { + t.Fatalf("err = %v, want ErrNotAllowed", err) + } + if called { + t.Fatal("handler must not be called when breaker is open") + } + if breaker.success != 0 || breaker.failed != 1 { + t.Fatalf("breaker success=%d failed=%d, want success=0 failed=1", breaker.success, breaker.failed) + } +} + +func TestClientMarksServerErrorAsFailed(t *testing.T) { + breaker := &circuitBreakerMock{} + ctx := transport.NewClientContext(context.Background(), &transportMock{operation: "/foo"}) + next := func(context.Context, any) (any, error) { + return nil, kratoserrors.InternalServer("", "") + } + + _, _ = Client(WithBreakerFactory(func() CircuitBreaker { + return breaker + }))(next)(ctx, nil) + + if breaker.success != 0 || breaker.failed != 1 { + t.Fatalf("breaker success=%d failed=%d, want success=0 failed=1", breaker.success, breaker.failed) + } +} diff --git a/middleware/logging/logging.go b/middleware/logging/logging.go new file mode 100644 index 0000000..28bd8e6 --- /dev/null +++ b/middleware/logging/logging.go @@ -0,0 +1,137 @@ +package logging + +import ( + "context" + "fmt" + "log/slog" + "time" + + "google.golang.org/grpc/codes" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/http/status" +) + +// Redacter defines how to log an object +type Redacter interface { + Redact() string +} + +// Server is an server logging middleware. +func Server(logger *slog.Logger) middleware.Middleware { + if logger == nil { + logger = slog.Default() + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + var ( + code int32 + reason string + kind string + operation string + ) + + // default code + code = int32(status.FromGRPCCode(codes.OK)) + + startTime := time.Now() + if info, ok := transport.FromServerContext(ctx); ok { + kind = info.Kind().String() + operation = info.Operation() + } + reply, err = handler(ctx, req) + if se := errors.FromError(err); se != nil { + code = se.Code + reason = se.Reason + } + level, stack := extractError(err) + attrs := []slog.Attr{ + slog.String("kind", "server"), + slog.String("component", kind), + slog.String("operation", operation), + slog.String("args", extractArgs(req)), + slog.Int64("code", int64(code)), + slog.String("reason", reason), + slog.Float64("latency", time.Since(startTime).Seconds()), + } + if err != nil { + attrs = append(attrs, slog.Any("error", err)) + if stack != "" { + attrs = append(attrs, slog.String("stack", stack)) + } + } + logger.LogAttrs(ctx, level, "server request", attrs...) + return + } + } +} + +// Client is a client logging middleware. +func Client(logger *slog.Logger) middleware.Middleware { + if logger == nil { + logger = slog.Default() + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + var ( + code int32 + reason string + kind string + operation string + ) + + // default code + code = int32(status.FromGRPCCode(codes.OK)) + + startTime := time.Now() + if info, ok := transport.FromClientContext(ctx); ok { + kind = info.Kind().String() + operation = info.Operation() + } + reply, err = handler(ctx, req) + if se := errors.FromError(err); se != nil { + code = se.Code + reason = se.Reason + } + level, stack := extractError(err) + attrs := []slog.Attr{ + slog.String("kind", "client"), + slog.String("component", kind), + slog.String("operation", operation), + slog.String("args", extractArgs(req)), + slog.Int64("code", int64(code)), + slog.String("reason", reason), + slog.Float64("latency", time.Since(startTime).Seconds()), + } + if err != nil { + attrs = append(attrs, slog.Any("error", err)) + if stack != "" { + attrs = append(attrs, slog.String("stack", stack)) + } + } + logger.LogAttrs(ctx, level, "client request", attrs...) + return + } + } +} + +// extractArgs returns the string of the req +func extractArgs(req any) string { + if redacter, ok := req.(Redacter); ok { + return redacter.Redact() + } + if stringer, ok := req.(fmt.Stringer); ok { + return stringer.String() + } + return fmt.Sprintf("%+v", req) +} + +// extractError returns the level and stack to attach for err. +func extractError(err error) (slog.Level, string) { + if err != nil { + return slog.LevelError, fmt.Sprintf("%+v", err) + } + return slog.LevelInfo, "" +} diff --git a/middleware/logging/logging_test.go b/middleware/logging/logging_test.go new file mode 100644 index 0000000..cd62d06 --- /dev/null +++ b/middleware/logging/logging_test.go @@ -0,0 +1,211 @@ +package logging + +import ( + "context" + "errors" + "log/slog" + "testing" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +var _ transport.Transporter = (*Transport)(nil) + +type Transport struct { + kind transport.Kind + endpoint string + operation string +} + +func (tr *Transport) Kind() transport.Kind { + return tr.kind +} + +func (tr *Transport) Endpoint() string { + return tr.endpoint +} + +func (tr *Transport) Operation() string { + return tr.operation +} + +func (tr *Transport) RequestHeader() transport.Header { + return nil +} + +func (tr *Transport) ReplyHeader() transport.Header { + return nil +} + +func TestHTTP(t *testing.T) { + err := errors.New("reply.error") + handler := &captureHandler{} + logger := slog.New(handler) + + tests := []struct { + name string + kind func(*slog.Logger) middleware.Middleware + err error + ctx context.Context + want slog.Level + }{ + { + name: "http-server@fail", + kind: Server, + err: err, + ctx: transport.NewServerContext(context.Background(), &Transport{kind: transport.KindHTTP, endpoint: "endpoint", operation: "/package.service/method"}), + want: slog.LevelError, + }, + { + name: "http-server@succ", + kind: Server, + ctx: transport.NewServerContext(context.Background(), &Transport{kind: transport.KindHTTP, endpoint: "endpoint", operation: "/package.service/method"}), + want: slog.LevelInfo, + }, + { + name: "http-client@succ", + kind: Client, + ctx: transport.NewClientContext(context.Background(), &Transport{kind: transport.KindHTTP, endpoint: "endpoint", operation: "/package.service/method"}), + want: slog.LevelInfo, + }, + { + name: "http-client@fail", + kind: Client, + err: err, + ctx: transport.NewClientContext(context.Background(), &Transport{kind: transport.KindHTTP, endpoint: "endpoint", operation: "/package.service/method"}), + want: slog.LevelError, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + handler.reset() + next := func(context.Context, any) (any, error) { + return "reply", test.err + } + next = test.kind(logger)(next) + reply, gotErr := next(test.ctx, "req.args") + if reply != "reply" { + t.Fatalf("reply = %v, want %q", reply, "reply") + } + if gotErr != test.err { + t.Fatalf("err = %v, want %v", gotErr, test.err) + } + if len(handler.records) != 1 { + t.Fatalf("records len = %d, want 1", len(handler.records)) + } + if handler.records[0].Level != test.want { + t.Fatalf("level = %v, want %v", handler.records[0].Level, test.want) + } + if got := handler.attrs[0]["component"]; got != "http" { + t.Fatalf("component = %v, want %q", got, "http") + } + if got := handler.attrs[0]["operation"]; got != "/package.service/method" { + t.Fatalf("operation = %v, want %q", got, "/package.service/method") + } + if got := handler.attrs[0]["args"]; got != "req.args" { + t.Fatalf("args = %v, want %q", got, "req.args") + } + }) + } +} + +type ( + dummy struct { + field string + } + dummyStringer struct { + field string + } + dummyStringerRedacter struct { + field string + } +) + +func (d *dummyStringer) String() string { + return "my value" +} + +func (d *dummyStringerRedacter) String() string { + return "my value" +} + +func (d *dummyStringerRedacter) Redact() string { + return "my value redacted" +} + +func TestExtractArgs(t *testing.T) { + tests := []struct { + name string + req any + expected string + }{ + {name: "dummyStringer", req: &dummyStringer{field: ""}, expected: "my value"}, + {name: "dummy", req: &dummy{field: "value"}, expected: "&{field:value}"}, + {name: "dummyStringerRedacter", req: &dummyStringerRedacter{field: ""}, expected: "my value redacted"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if value := extractArgs(test.req); value != test.expected { + t.Errorf(`The stringified %s structure must be equal to "%s", %v given`, test.name, test.expected, value) + } + }) + } +} + +func TestExtractError(t *testing.T) { + tests := []struct { + name string + err error + wantLevel slog.Level + wantErrStr string + }{ + {name: "no error", err: nil, wantLevel: slog.LevelInfo, wantErrStr: ""}, + {name: "error", err: errors.New("test error"), wantLevel: slog.LevelError, wantErrStr: "test error"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + level, errStr := extractError(test.err) + if level != test.wantLevel { + t.Errorf("want: %d, got: %d", test.wantLevel, level) + } + if errStr != test.wantErrStr { + t.Errorf("want: %s, got: %s", test.wantErrStr, errStr) + } + }) + } +} + +type captureHandler struct { + records []slog.Record + attrs []map[string]any +} + +func (h *captureHandler) reset() { + h.records = nil + h.attrs = nil +} + +func (h *captureHandler) Enabled(context.Context, slog.Level) bool { + return true +} + +func (h *captureHandler) Handle(_ context.Context, record slog.Record) error { + attrs := make(map[string]any) + record.Attrs(func(attr slog.Attr) bool { + attrs[attr.Key] = attr.Value.Any() + return true + }) + h.records = append(h.records, record.Clone()) + h.attrs = append(h.attrs, attrs) + return nil +} + +func (h *captureHandler) WithAttrs([]slog.Attr) slog.Handler { + return h +} + +func (h *captureHandler) WithGroup(string) slog.Handler { + return h +} diff --git a/middleware/metadata/metadata.go b/middleware/metadata/metadata.go new file mode 100644 index 0000000..3e76d46 --- /dev/null +++ b/middleware/metadata/metadata.go @@ -0,0 +1,116 @@ +package metadata + +import ( + "context" + "strings" + + "github.com/go-kratos/kratos/v3/metadata" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +// Option is metadata option. +type Option func(*options) + +type options struct { + prefix []string + md metadata.Metadata +} + +func (o *options) hasPrefix(key string) bool { + k := strings.ToLower(key) + for _, prefix := range o.prefix { + if strings.HasPrefix(k, prefix) { + return true + } + } + return false +} + +// WithConstants with constant metadata key value. +func WithConstants(md metadata.Metadata) Option { + return func(o *options) { + o.md = md + } +} + +// WithPropagatedPrefix with propagated key prefix. +func WithPropagatedPrefix(prefix ...string) Option { + return func(o *options) { + o.prefix = prefix + } +} + +// Server is middleware server-side metadata. +func Server(opts ...Option) middleware.Middleware { + options := &options{ + prefix: []string{"x-md-"}, // x-md-global-, x-md-local + } + for _, o := range opts { + o(options) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + tr, ok := transport.FromServerContext(ctx) + if !ok { + return handler(ctx, req) + } + + md := options.md.Clone() + header := tr.RequestHeader() + for _, k := range header.Keys() { + if options.hasPrefix(k) { + for _, v := range header.Values(k) { + md.Add(k, v) + } + } + } + ctx = metadata.NewServerContext(ctx, md) + return handler(ctx, req) + } + } +} + +// Client is middleware client-side metadata. +func Client(opts ...Option) middleware.Middleware { + options := &options{ + prefix: []string{"x-md-global-"}, + } + for _, o := range opts { + o(options) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + tr, ok := transport.FromClientContext(ctx) + if !ok { + return handler(ctx, req) + } + + header := tr.RequestHeader() + // x-md-local- + for k, vList := range options.md { + for _, v := range vList { + header.Add(k, v) + } + } + if md, ok := metadata.FromClientContext(ctx); ok { + for k, vList := range md { + for _, v := range vList { + header.Add(k, v) + } + } + } + // x-md-global- + if md, ok := metadata.FromServerContext(ctx); ok { + for k, vList := range md { + if options.hasPrefix(k) { + for _, v := range vList { + header.Add(k, v) + } + } + } + } + return handler(ctx, req) + } + } +} diff --git a/middleware/metadata/metadata_test.go b/middleware/metadata/metadata_test.go new file mode 100644 index 0000000..0f5afda --- /dev/null +++ b/middleware/metadata/metadata_test.go @@ -0,0 +1,179 @@ +package metadata + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "github.com/go-kratos/kratos/v3/metadata" + "github.com/go-kratos/kratos/v3/transport" +) + +type headerCarrier http.Header + +func (hc headerCarrier) Get(key string) string { return http.Header(hc).Get(key) } + +func (hc headerCarrier) Set(key string, value string) { http.Header(hc).Set(key, value) } + +func (hc headerCarrier) Add(key string, value string) { http.Header(hc).Add(key, value) } + +// Keys lists the keys stored in this carrier. +func (hc headerCarrier) Keys() []string { + keys := make([]string, 0, len(hc)) + for k := range http.Header(hc) { + keys = append(keys, k) + } + return keys +} + +// Values returns a slice value associated with the passed key. +func (hc headerCarrier) Values(key string) []string { + return http.Header(hc).Values(key) +} + +type testTransport struct{ header headerCarrier } + +func (tr *testTransport) Kind() transport.Kind { return transport.KindHTTP } +func (tr *testTransport) Endpoint() string { return "" } +func (tr *testTransport) Operation() string { return "" } +func (tr *testTransport) RequestHeader() transport.Header { return tr.header } +func (tr *testTransport) ReplyHeader() transport.Header { return tr.header } + +var ( + globalKey = "x-md-global-key" + globalValue = "global-value" + localKey = "x-md-local-key" + localValue = "local-value" + customKey = "x-md-local-custom" + customValue = "custom-value" + constKey = "x-md-local-const" + constValue = "x-md-local-const" +) + +func TestSever(t *testing.T) { + hs := func(ctx context.Context, in any) (any, error) { + md, ok := metadata.FromServerContext(ctx) + if !ok { + return nil, errors.New("no md") + } + if md.Get(constKey) != constValue { + return nil, errors.New("const not equal") + } + if md.Get(globalKey) != globalValue { + return nil, errors.New("global not equal") + } + if md.Get(localKey) != localValue { + return nil, errors.New("local not equal") + } + return in, nil + } + hc := headerCarrier{} + hc.Set(globalKey, globalValue) + hc.Set(localKey, localValue) + ctx := transport.NewServerContext(context.Background(), &testTransport{hc}) + // const md + constMD := metadata.New() + constMD.Set(constKey, constValue) + reply, err := Server(WithConstants(constMD))(hs)(ctx, "foo") + if err != nil { + t.Fatal(err) + } + if reply.(string) != "foo" { + t.Fatalf("want foo got %v", reply) + } +} + +func TestClient(t *testing.T) { + hs := func(ctx context.Context, in any) (any, error) { + tr, ok := transport.FromClientContext(ctx) + if !ok { + return nil, errors.New("no md") + } + if tr.RequestHeader().Get(constKey) != constValue { + return nil, errors.New("const not equal") + } + if tr.RequestHeader().Get(customKey) != customValue { + return nil, errors.New("custom not equal") + } + if tr.RequestHeader().Get(globalKey) != globalValue { + return nil, errors.New("global not equal") + } + if tr.RequestHeader().Get(localKey) != "" { + return nil, errors.New("local must empty") + } + return in, nil + } + // server md + serverMD := metadata.New() + serverMD.Set(globalKey, globalValue) + serverMD.Set(localKey, localValue) + ctx := metadata.NewServerContext(context.Background(), serverMD) + // client md + clientMD := metadata.New() + clientMD.Set(customKey, customValue) + ctx = metadata.NewClientContext(ctx, clientMD) + // transport carrier + ctx = transport.NewClientContext(ctx, &testTransport{headerCarrier{}}) + // const md + constMD := metadata.New() + constMD.Set(constKey, constValue) + reply, err := Client(WithConstants(constMD))(hs)(ctx, "bar") + if err != nil { + t.Fatal(err) + } + if reply.(string) != "bar" { + t.Fatalf("want foo got %v", reply) + } +} + +func TestWithConstants(t *testing.T) { + md := metadata.Metadata{ + constKey: {constValue}, + } + options := &options{ + md: metadata.Metadata{ + "override": {"override"}, + }, + } + + WithConstants(md)(options) + if !reflect.DeepEqual(md, options.md) { + t.Errorf("want: %v, got: %v", md, options.md) + } +} + +func TestOptions_WithPropagatedPrefix(t *testing.T) { + options := &options{ + prefix: []string{"override"}, + } + prefixes := []string{"something", "another"} + + WithPropagatedPrefix(prefixes...)(options) + if !reflect.DeepEqual(prefixes, options.prefix) { + t.Error("The prefix must be overridden.") + } +} + +func TestOptions_hasPrefix(t *testing.T) { + tests := []struct { + name string + options *options + key string + exists bool + }{ + {"exists key upper", &options{prefix: []string{"prefix"}}, "PREFIX_true", true}, + {"exists key lower", &options{prefix: []string{"prefix"}}, "prefix_true", true}, + {"not exists key upper", &options{prefix: []string{"prefix"}}, "false_PREFIX", false}, + {"not exists key lower", &options{prefix: []string{"prefix"}}, "false_prefix", false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + exists := test.options.hasPrefix(test.key) + if test.exists != exists { + t.Errorf("key: '%sr', not exists prefixs: %v", test.key, test.options.prefix) + } + }) + } +} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000..5568fb5 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "context" +) + +// Handler defines the handler invoked by Middleware. +type Handler func(ctx context.Context, req any) (any, error) + +// Middleware is HTTP/gRPC transport middleware. +type Middleware func(Handler) Handler + +// Chain returns a Middleware that specifies the chained handler for endpoint. +func Chain(m ...Middleware) Middleware { + return func(next Handler) Handler { + for i := len(m) - 1; i >= 0; i-- { + next = m[i](next) + } + return next + } +} diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go new file mode 100644 index 0000000..2218bbd --- /dev/null +++ b/middleware/middleware_test.go @@ -0,0 +1,61 @@ +package middleware + +import ( + "context" + "fmt" + "reflect" + "testing" +) + +var i int + +func TestChain(t *testing.T) { + next := func(_ context.Context, req any) (any, error) { + if req != "hello kratos!" { + t.Errorf("expect %v, got %v", "hello kratos!", req) + } + i += 10 + return "reply", nil + } + + got, err := Chain(test1Middleware, test2Middleware, test3Middleware)(next)(context.Background(), "hello kratos!") + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if !reflect.DeepEqual(got, "reply") { + t.Errorf("expect %v, got %v", "reply", got) + } + if !reflect.DeepEqual(i, 16) { + t.Errorf("expect %v, got %v", 16, i) + } +} + +func test1Middleware(handler Handler) Handler { + return func(ctx context.Context, req any) (reply any, err error) { + fmt.Println("test1 before") + i++ + reply, err = handler(ctx, req) + fmt.Println("test1 after") + return + } +} + +func test2Middleware(handler Handler) Handler { + return func(ctx context.Context, req any) (reply any, err error) { + fmt.Println("test2 before") + i += 2 + reply, err = handler(ctx, req) + fmt.Println("test2 after") + return + } +} + +func test3Middleware(handler Handler) Handler { + return func(ctx context.Context, req any) (reply any, err error) { + fmt.Println("test3 before") + i += 3 + reply, err = handler(ctx, req) + fmt.Println("test3 after") + return + } +} diff --git a/middleware/ratelimit/ratelimit.go b/middleware/ratelimit/ratelimit.go new file mode 100644 index 0000000..1f88110 --- /dev/null +++ b/middleware/ratelimit/ratelimit.go @@ -0,0 +1,59 @@ +package ratelimit + +import ( + "context" + + "github.com/go-kratos/kratos/v3/errors" + internalratelimit "github.com/go-kratos/kratos/v3/internal/ratelimit" + "github.com/go-kratos/kratos/v3/middleware" +) + +// ErrLimitExceed is service unavailable due to rate limit exceeded. +var ErrLimitExceed = errors.New(429, "RATELIMIT", "service unavailable due to rate limit exceeded") + +// DoneFunc records request completion. +type DoneFunc = internalratelimit.DoneFunc + +// DoneInfo contains request completion metadata. +type DoneInfo = internalratelimit.DoneInfo + +// Limiter is a rate limiter. +type Limiter = internalratelimit.Limiter + +// Option is ratelimit option. +type Option func(*options) + +// WithLimiter set Limiter implementation, +// default is bbr limiter +func WithLimiter(limiter Limiter) Option { + return func(o *options) { + o.limiter = limiter + } +} + +type options struct { + limiter Limiter +} + +// Server ratelimiter middleware +func Server(opts ...Option) middleware.Middleware { + options := &options{ + limiter: internalratelimit.NewLimiter(), + } + for _, o := range opts { + o(options) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + done, e := options.limiter.Allow() + if e != nil { + // rejected + return nil, ErrLimitExceed + } + // allowed + reply, err = handler(ctx, req) + done(DoneInfo{Err: err}) + return + } + } +} diff --git a/middleware/ratelimit/ratelimit_test.go b/middleware/ratelimit/ratelimit_test.go new file mode 100644 index 0000000..35f9a44 --- /dev/null +++ b/middleware/ratelimit/ratelimit_test.go @@ -0,0 +1,62 @@ +package ratelimit + +import ( + "context" + "errors" + "testing" +) + +type ( + ratelimitMock struct { + reached bool + } + ratelimitReachedMock struct { + reached bool + } +) + +func (r *ratelimitMock) Allow() (DoneFunc, error) { + return func(_ DoneInfo) { + r.reached = true + }, nil +} + +func (r *ratelimitReachedMock) Allow() (DoneFunc, error) { + return func(_ DoneInfo) { + r.reached = true + }, errors.New("errored") +} + +func Test_WithLimiter(t *testing.T) { + o := options{ + limiter: &ratelimitMock{}, + } + + WithLimiter(nil)(&o) + if o.limiter != nil { + t.Error("The limiter property must be updated.") + } +} + +func TestServer(t *testing.T) { + nextValid := func(context.Context, any) (any, error) { + return "Hello valid", nil + } + + rlm := &ratelimitMock{} + rlrm := &ratelimitReachedMock{} + + _, _ = Server(func(o *options) { + o.limiter = rlm + })(nextValid)(context.Background(), nil) + if !rlm.reached { + t.Error("The ratelimit must run the done function.") + } + + _, _ = Server(func(o *options) { + o.limiter = rlrm + })(nextValid)(context.Background(), nil) + if rlrm.reached { + t.Error("The ratelimit must not run the done function and should be denied.") + } +} diff --git a/middleware/recovery/recovery.go b/middleware/recovery/recovery.go new file mode 100644 index 0000000..1d6e899 --- /dev/null +++ b/middleware/recovery/recovery.go @@ -0,0 +1,79 @@ +package recovery + +import ( + "context" + "log/slog" + "runtime" + "time" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" +) + +// Latency is recovery latency context key +type Latency struct{} + +// ErrUnknownRequest is unknown request error. +var ErrUnknownRequest = errors.InternalServer("UNKNOWN", "unknown request error") + +// HandlerFunc is recovery handler func. +type HandlerFunc func(ctx context.Context, req, err any) error + +// Option is recovery option. +type Option func(*options) + +type options struct { + handler HandlerFunc + logger *slog.Logger +} + +// WithHandler with recovery handler. +func WithHandler(h HandlerFunc) Option { + return func(o *options) { + o.handler = h + } +} + +// WithLogger sets the logger used to record recovered panics. Defaults to +// [slog.Default]. +func WithLogger(logger *slog.Logger) Option { + return func(o *options) { + o.logger = logger + } +} + +// Recovery is a server middleware that recovers from any panics. +func Recovery(opts ...Option) middleware.Middleware { + op := options{ + handler: func(context.Context, any, any) error { + return ErrUnknownRequest + }, + } + for _, o := range opts { + o(&op) + } + logger := op.logger + if logger == nil { + logger = slog.Default() + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + startTime := time.Now() + defer func() { + if rerr := recover(); rerr != nil { + buf := make([]byte, 64<<10) //nolint:mnd + n := runtime.Stack(buf, false) + buf = buf[:n] + logger.ErrorContext(ctx, "panic recovered", + slog.Any("panic", rerr), + slog.Any("request", req), + slog.String("stack", string(buf)), + ) + ctx = context.WithValue(ctx, Latency{}, time.Since(startTime).Seconds()) + err = op.handler(ctx, req, rerr) + } + }() + return handler(ctx, req) + } + } +} diff --git a/middleware/recovery/recovery_test.go b/middleware/recovery/recovery_test.go new file mode 100644 index 0000000..a4e21b5 --- /dev/null +++ b/middleware/recovery/recovery_test.go @@ -0,0 +1,42 @@ +package recovery + +import ( + "context" + "fmt" + "testing" + + "github.com/go-kratos/kratos/v3/errors" +) + +func TestOnce(t *testing.T) { + defer func() { + if recover() != nil { + t.Error("fail") + } + }() + + next := func(context.Context, any) (any, error) { + panic("panic reason") + } + _, e := Recovery(WithHandler(func(ctx context.Context, _, err any) error { + _, ok := ctx.Value(Latency{}).(float64) + if !ok { + t.Errorf("not latency") + } + return errors.InternalServer("RECOVERY", fmt.Sprintf("panic triggered: %v", err)) + }))(next)(context.Background(), "panic") + t.Logf("succ and reason is %v", e) +} + +func TestNotPanic(t *testing.T) { + next := func(_ context.Context, req any) (any, error) { + return req.(string) + "https://go-kratos.dev", nil + } + + _, e := Recovery(WithHandler(func(_ context.Context, _ any, err any) error { + return errors.InternalServer("RECOVERY", fmt.Sprintf("panic triggered: %v", err)) + }))(next)(context.Background(), "notPanic") + if e != nil { + t.Errorf("e isn't nil") + } +} diff --git a/middleware/selector/selector.go b/middleware/selector/selector.go new file mode 100644 index 0000000..bbd2ebd --- /dev/null +++ b/middleware/selector/selector.go @@ -0,0 +1,143 @@ +package selector + +import ( + "context" + "regexp" + "strings" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +type ( + transporter func(ctx context.Context) (transport.Transporter, bool) + MatchFunc func(ctx context.Context, operation string) bool +) + +var ( + // serverTransporter is get server transport.Transporter from ctx + serverTransporter transporter = func(ctx context.Context) (transport.Transporter, bool) { + return transport.FromServerContext(ctx) + } + // clientTransporter is get client transport.Transporter from ctx + clientTransporter transporter = func(ctx context.Context) (transport.Transporter, bool) { + return transport.FromClientContext(ctx) + } +) + +// Builder is a selector builder +type Builder struct { + client bool + + prefix []string + regex []string + path []string + match MatchFunc + compiled []*regexp.Regexp + + ms []middleware.Middleware +} + +// Server selector middleware +func Server(ms ...middleware.Middleware) *Builder { + return &Builder{ms: ms} +} + +// Client selector middleware +func Client(ms ...middleware.Middleware) *Builder { + return &Builder{client: true, ms: ms} +} + +// Prefix is with Builder's prefix +func (b *Builder) Prefix(prefix ...string) *Builder { + b.prefix = prefix + return b +} + +// Regex is with Builder's regex +func (b *Builder) Regex(regex ...string) *Builder { + b.regex = regex + return b +} + +// Path is with Builder's path +func (b *Builder) Path(path ...string) *Builder { + b.path = path + return b +} + +// Match is with Builder's match +func (b *Builder) Match(fn MatchFunc) *Builder { + b.match = fn + return b +} + +// Build is Builder's Build, for example: Server().Path(m1,m2).Build() +func (b *Builder) Build() middleware.Middleware { + var transporter func(ctx context.Context) (transport.Transporter, bool) + if b.client { + transporter = clientTransporter + } else { + transporter = serverTransporter + } + b.compiled = make([]*regexp.Regexp, 0, len(b.regex)) + for _, regex := range b.regex { + if r, err := regexp.Compile(regex); err == nil { + b.compiled = append(b.compiled, r) + } + } + return selector(transporter, b.matches, b.ms...) +} + +// matches is match operation compliance Builder +func (b *Builder) matches(ctx context.Context, transporter transporter) bool { + info, ok := transporter(ctx) + if !ok { + return false + } + + operation := info.Operation() + for _, prefix := range b.prefix { + if prefixMatch(prefix, operation) { + return true + } + } + for _, r := range b.compiled { + if r.FindString(operation) == operation { + return true + } + } + for _, path := range b.path { + if pathMatch(path, operation) { + return true + } + } + + if b.match != nil { + if b.match(ctx, operation) { + return true + } + } + + return false +} + +// selector middleware +func selector(transporter transporter, match func(context.Context, transporter) bool, ms ...middleware.Middleware) middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if !match(ctx, transporter) { + return handler(ctx, req) + } + return middleware.Chain(ms...)(handler)(ctx, req) + } + } +} + +func pathMatch(path string, operation string) bool { + return path == operation +} + +func prefixMatch(prefix string, operation string) bool { + return strings.HasPrefix(operation, prefix) +} diff --git a/middleware/selector/selector_test.go b/middleware/selector/selector_test.go new file mode 100644 index 0000000..54fcf9e --- /dev/null +++ b/middleware/selector/selector_test.go @@ -0,0 +1,356 @@ +package selector + +import ( + "context" + "reflect" + "strings" + "testing" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +var _ transport.Transporter = (*Transport)(nil) + +type Transport struct { + kind transport.Kind + endpoint string + operation string + headers *mockHeader +} + +func (tr *Transport) Kind() transport.Kind { + return tr.kind +} + +func (tr *Transport) Endpoint() string { + return tr.endpoint +} + +func (tr *Transport) Operation() string { + return tr.operation +} + +func (tr *Transport) RequestHeader() transport.Header { + return tr.headers +} + +func (tr *Transport) ReplyHeader() transport.Header { + return nil +} + +type mockHeader struct { + m map[string][]string +} + +func (m *mockHeader) Get(key string) string { + vals := m.m[key] + if len(vals) > 0 { + return vals[0] + } + return "" +} + +func (m *mockHeader) Set(key, value string) { + m.m[key] = []string{value} +} + +func (m *mockHeader) Add(key, value string) { + m.m[key] = append(m.m[key], value) +} + +func (m *mockHeader) Keys() []string { + keys := make([]string, 0, len(m.m)) + for k := range m.m { + keys = append(keys, k) + } + return keys +} + +func (m *mockHeader) Values(key string) []string { + return m.m[key] +} + +func TestMatch(t *testing.T) { + tests := []struct { + name string + ctx context.Context + }{ + // TODO: Add test cases. + { + name: "/hello/world", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/hello/world"}), + }, + { + name: "/hi/world", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/hi/world"}), + }, + { + name: "/test/1234", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/test/1234"}), + }, + { + name: "/example/kratos", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/example/kratos"}), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + next := func(_ context.Context, req any) (any, error) { + t.Log(req) + return "reply", nil + } + next = Server(testMiddleware).Prefix("/hello/").Regex(`/test/[0-9]+`). + Path("/example/kratos").Build()(next) + _, _ = next(test.ctx, test.name) + }) + } +} + +func TestMatchClient(t *testing.T) { + tests := []struct { + name string + ctx context.Context + }{ + // TODO: Add test cases. + { + name: "/hello/world", + ctx: transport.NewClientContext(context.Background(), &Transport{operation: "/hello/world"}), + }, + { + name: "/hi/world", + ctx: transport.NewClientContext(context.Background(), &Transport{operation: "/hi/world"}), + }, + { + name: "/test/1234", + ctx: transport.NewClientContext(context.Background(), &Transport{operation: "/test/1234"}), + }, + { + name: "/example/kratos", + ctx: transport.NewClientContext(context.Background(), &Transport{operation: "/example/kratos"}), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + next := func(_ context.Context, req any) (any, error) { + t.Log(req) + return "reply", nil + } + next = Client(testMiddleware).Prefix("/hello/").Regex(`/test/[0-9]+`). + Path("/example/kratos").Build()(next) + _, _ = next(test.ctx, test.name) + }) + } +} + +func TestFunc(t *testing.T) { + tests := []struct { + name string + ctx context.Context + }{ + { + name: "/hello.Update/world", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/hello.Update/world"}), + }, + { + name: "/hi.Create/world", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/hi.Create/world"}), + }, + { + name: "/test.Name/1234", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/test.Name/1234"}), + }, + { + name: "/go-kratos.dev/kratos", + ctx: transport.NewServerContext(context.Background(), &Transport{operation: "/go-kratos.dev/kratos"}), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + next := func(_ context.Context, req any) (any, error) { + t.Log(req) + return "reply", nil + } + next = Server(testMiddleware).Match(func(_ context.Context, operation string) bool { + if strings.HasPrefix(operation, "/go-kratos.dev") || strings.HasSuffix(operation, "world") { + return true + } + return false + }).Build()(next) + reply, err := next(test.ctx, test.name) + if err != nil { + t.Errorf("expect error is nil, but got %v", err) + } + if !reflect.DeepEqual(reply, "reply") { + t.Errorf("expect reply is reply,but got %v", reply) + } + }) + } +} + +func TestHeaderFunc(t *testing.T) { + tests := []struct { + name string + ctx context.Context + }{ + { + name: "/hello.Update/world", + ctx: transport.NewServerContext(context.Background(), &Transport{ + operation: "/hello.Update/world", + headers: &mockHeader{map[string][]string{"X-Test": {"test"}}}, + }), + }, + { + name: "/hi.Create/world", + ctx: transport.NewServerContext(context.Background(), &Transport{ + operation: "/hi.Create/world", + headers: &mockHeader{map[string][]string{"X-Test": {"test2"}, "go-kratos": {"kratos"}}}, + }), + }, + { + name: "/test.Name/1234", + ctx: transport.NewServerContext(context.Background(), &Transport{ + operation: "/test.Name/1234", + headers: &mockHeader{map[string][]string{"X-Test": {"test3"}}}, + }), + }, + { + name: "/go-kratos.dev/kratos", + ctx: transport.NewServerContext(context.Background(), &Transport{ + operation: "/go-kratos.dev/kratos", + headers: &mockHeader{map[string][]string{"X-Test": {"test"}}}, + }), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + next := func(_ context.Context, req any) (any, error) { + t.Log(req) + return "reply", nil + } + next = Server(testMiddleware).Match(func(ctx context.Context, _ string) bool { + tr, ok := transport.FromServerContext(ctx) + if !ok { + return false + } + if tr.RequestHeader().Get("X-Test") == "test" { + return true + } + if tr.RequestHeader().Get("go-kratos") == "kratos" { + return true + } + return false + }).Build()(next) + reply, err := next(test.ctx, test.name) + if err != nil { + t.Errorf("expect error is nil, but got %v", err) + } + if !reflect.DeepEqual(reply, "reply") { + t.Errorf("expect reply is reply,but got %v", reply) + } + }) + } +} + +func testMiddleware(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + reply, err = handler(ctx, req) + return + } +} + +func Test_RegexMatch(t *testing.T) { + tests := []struct { + name string + regex []string + operation string + want bool + }{ + { + name: "exact match with digits", + regex: []string{`/test/[0-9]+`}, + operation: "/test/1234", + want: true, + }, + { + name: "no match", + regex: []string{`/test/[0-9]+`}, + operation: "/test/abc", + want: false, + }, + { + name: "multiple patterns first matches", + regex: []string{`/api/v[0-9]+/.*`, `/test/.*`}, + operation: "/api/v2/users", + want: true, + }, + { + name: "multiple patterns second matches", + regex: []string{`/api/v[0-9]+/.*`, `/test/.*`}, + operation: "/test/hello", + want: true, + }, + { + name: "multiple patterns none match", + regex: []string{`/api/v[0-9]+/.*`, `/test/[0-9]+`}, + operation: "/other/path", + want: false, + }, + { + name: "invalid regex is skipped", + regex: []string{"^\b(?"}, + operation: "something", + want: false, + }, + { + name: "invalid regex mixed with valid", + regex: []string{"^\b(?", `/test/[0-9]+`}, + operation: "/test/1234", + want: true, + }, + { + name: "empty regex list", + regex: []string{}, + operation: "/test/1234", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var middlewareApplied bool + markMiddleware := func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + middlewareApplied = true + return handler(ctx, req) + } + } + next := func(_ context.Context, _ any) (any, error) { + return "reply", nil + } + ctx := transport.NewServerContext(context.Background(), &Transport{operation: tt.operation}) + handler := Server(markMiddleware).Regex(tt.regex...).Build()(next) + _, _ = handler(ctx, tt.operation) + if middlewareApplied != tt.want { + t.Errorf("middleware applied = %v, want %v", middlewareApplied, tt.want) + } + }) + } +} + +func Test_InvalidRegexSkipped(t *testing.T) { + b := Server(testMiddleware).Regex("^\b(?", `/valid/[0-9]+`) + m := b.Build() + if m == nil { + t.Fatal("Build() must not return nil") + } + if len(b.compiled) != 1 { + t.Errorf("expected 1 compiled regex, got %d", len(b.compiled)) + } +} + +func Test_matches(t *testing.T) { + b := Builder{} + if b.matches(context.Background(), func(_ context.Context) (transport.Transporter, bool) { return nil, false }) { + t.Error("The matches method must return false.") + } +} diff --git a/middleware/validate/validate.go b/middleware/validate/validate.go new file mode 100644 index 0000000..d95eec1 --- /dev/null +++ b/middleware/validate/validate.go @@ -0,0 +1,59 @@ +package validate + +import ( + "context" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" +) + +// ValidatorFunc defines a validation function type. +type ValidatorFunc func(v any) error + +// validator is an interface for types that can validate themselves. +type validator interface { + Validate() error +} + +// Validator returns a middleware that performs validation on requests. +// It validates requests that implement Validate and any custom validators. +// Example usage: +// +// buf validate(https://github.com/bufbuild/protovalidate): +// import "buf.build/go/protovalidate" +// import "google.golang.org/protobuf/proto" +// +// Validator(func(v any) error { +// if msg, ok := v.(proto.Message); ok { +// return protovalidate.Validate(msg) +// } +// return nil +// }) +// +// Google AIP field behavior validate(https://google.aip.dev/203): +// import "go.einride.tech/aip/fieldbehavior" +// import "google.golang.org/protobuf/proto" +// +// Validator(func(v any) error { +// if msg, ok := v.(proto.Message); ok { +// return fieldbehavior.ValidateRequiredFields(msg) +// } +// return nil +// }) +func Validator(validators ...ValidatorFunc) middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if v, ok := req.(validator); ok { + if err := v.Validate(); err != nil { + return nil, errors.BadRequest("VALIDATOR", err.Error()).WithCause(err) + } + } + for _, v := range validators { + if err := v(req); err != nil { + return nil, errors.BadRequest("VALIDATOR", err.Error()).WithCause(err) + } + } + return handler(ctx, req) + } + } +} diff --git a/middleware/validate/validate_test.go b/middleware/validate/validate_test.go new file mode 100644 index 0000000..c9b9061 --- /dev/null +++ b/middleware/validate/validate_test.go @@ -0,0 +1,43 @@ +package validate + +import ( + "context" + "errors" + "testing" + + kratoserrors "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" +) + +// protoVali implement validate.validator +type protoVali struct { + name string + age int + isErr bool +} + +func (v protoVali) Validate() error { + if v.name == "" || v.age < 0 { + return errors.New("err") + } + return nil +} + +func TestTable(t *testing.T) { + var mock middleware.Handler = func(context.Context, any) (any, error) { return nil, nil } + + tests := []protoVali{ + {"v1", 365, false}, + {"v2", -1, true}, + {"", 365, true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + v := Validator()(mock) + _, err := v(context.Background(), test) + if want, have := test.isErr, kratoserrors.IsBadRequest(err); want != have { + t.Errorf("fail data %v, want %v, have %v", test, want, have) + } + }) + } +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..c61fc9b --- /dev/null +++ b/options.go @@ -0,0 +1,129 @@ +package kratos + +import ( + "context" + "log/slog" + "net/url" + "os" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/transport" +) + +// Option is an application option. +type Option func(o *options) + +// options is an application options. +type options struct { + id string + name string + version string + metadata map[string]string + endpoints []*url.URL + + ctx context.Context + sigs []os.Signal + + logger *slog.Logger + registrar registry.Registrar + registrarTimeout time.Duration + stopTimeout time.Duration + servers []transport.Server + + // Before and After funcs + beforeStart []func(context.Context) error + beforeStop []func(context.Context) error + afterStart []func(context.Context) error + afterStop []func(context.Context) error +} + +// ID with service id. +func ID(id string) Option { + return func(o *options) { o.id = id } +} + +// Name with service name. +func Name(name string) Option { + return func(o *options) { o.name = name } +} + +// Version with service version. +func Version(version string) Option { + return func(o *options) { o.version = version } +} + +// Metadata with service metadata. +func Metadata(md map[string]string) Option { + return func(o *options) { o.metadata = md } +} + +// Endpoint with service endpoint. +func Endpoint(endpoints ...*url.URL) Option { + return func(o *options) { o.endpoints = endpoints } +} + +// Context with service context. +func Context(ctx context.Context) Option { + return func(o *options) { o.ctx = ctx } +} + +// Logger with service logger. +func Logger(logger *slog.Logger) Option { + return func(o *options) { o.logger = logger } +} + +// Server with transport servers. +func Server(srv ...transport.Server) Option { + return func(o *options) { o.servers = srv } +} + +// Signal with exit signals. +func Signal(sigs ...os.Signal) Option { + return func(o *options) { o.sigs = sigs } +} + +// Registrar with service registry. +func Registrar(r registry.Registrar) Option { + return func(o *options) { o.registrar = r } +} + +// RegistrarTimeout with registrar timeout. +func RegistrarTimeout(t time.Duration) Option { + return func(o *options) { o.registrarTimeout = t } +} + +// StopTimeout with app stop timeout. +func StopTimeout(t time.Duration) Option { + return func(o *options) { o.stopTimeout = t } +} + +// Before and Afters + +// BeforeStart run funcs before app starts +func BeforeStart(fn func(context.Context) error) Option { + return func(o *options) { + o.beforeStart = append(o.beforeStart, fn) + } +} + +// BeforeStop run funcs before app stops +func BeforeStop(fn func(context.Context) error) Option { + return func(o *options) { + o.beforeStop = append(o.beforeStop, fn) + } +} + +// AfterStart run funcs after app starts +func AfterStart(fn func(context.Context) error) Option { + return func(o *options) { + o.afterStart = append(o.afterStart, fn) + } +} + +// AfterStop run funcs after app stops +func AfterStop(fn func(context.Context) error) Option { + return func(o *options) { + o.afterStop = append(o.afterStop, fn) + } +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 0000000..8466854 --- /dev/null +++ b/options_test.go @@ -0,0 +1,199 @@ +package kratos + +import ( + "context" + "io" + "log/slog" + "net/url" + "os" + "reflect" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/transport" +) + +func TestID(t *testing.T) { + o := &options{} + v := "123" + ID(v)(o) + if !reflect.DeepEqual(v, o.id) { + t.Fatalf("o.id:%s is not equal to v:%s", o.id, v) + } +} + +func TestName(t *testing.T) { + o := &options{} + v := "abc" + Name(v)(o) + if !reflect.DeepEqual(v, o.name) { + t.Fatalf("o.name:%s is not equal to v:%s", o.name, v) + } +} + +func TestVersion(t *testing.T) { + o := &options{} + v := "123" + Version(v)(o) + if !reflect.DeepEqual(v, o.version) { + t.Fatalf("o.version:%s is not equal to v:%s", o.version, v) + } +} + +func TestMetadata(t *testing.T) { + o := &options{} + v := map[string]string{ + "a": "1", + "b": "2", + } + Metadata(v)(o) + if !reflect.DeepEqual(v, o.metadata) { + t.Fatalf("o.metadata:%s is not equal to v:%s", o.metadata, v) + } +} + +func TestEndpoint(t *testing.T) { + o := &options{} + v := []*url.URL{ + {Host: "example.com"}, + {Host: "foo.com"}, + } + Endpoint(v...)(o) + if !reflect.DeepEqual(v, o.endpoints) { + t.Fatalf("o.endpoints:%s is not equal to v:%s", o.endpoints, v) + } +} + +func TestContext(t *testing.T) { + type ctxKey struct { + Key string + } + o := &options{} + v := context.WithValue(context.TODO(), ctxKey{Key: "context"}, "b") + Context(v)(o) + if !reflect.DeepEqual(v, o.ctx) { + t.Fatalf("o.ctx:%s is not equal to v:%s", o.ctx, v) + } +} + +func TestLogger(t *testing.T) { + o := &options{} + v := slog.New(slog.NewTextHandler(io.Discard, nil)) + Logger(v)(o) + if !reflect.DeepEqual(v, o.logger) { + t.Fatalf("o.logger:%v is not equal to v:%v", o.logger, v) + } +} + +type mockServer struct { + stopFn func(context.Context) error +} + +func (m *mockServer) Start(_ context.Context) error { return nil } +func (m *mockServer) Stop(ctx context.Context) error { + if m.stopFn != nil { + return m.stopFn(ctx) + } + return nil +} + +func TestServer(t *testing.T) { + o := &options{} + v := []transport.Server{ + &mockServer{}, &mockServer{}, + } + Server(v...)(o) + if !reflect.DeepEqual(v, o.servers) { + t.Fatalf("o.servers:%s is not equal to v:%s", o.servers, v) + } +} + +type mockSignal struct{} + +func (m *mockSignal) String() string { return "sig" } +func (m *mockSignal) Signal() {} + +func TestSignal(t *testing.T) { + o := &options{} + v := []os.Signal{ + &mockSignal{}, &mockSignal{}, + } + Signal(v...)(o) + if !reflect.DeepEqual(v, o.sigs) { + t.Fatal("o.sigs is not equal to v") + } +} + +type mockRegistrar struct{} + +func (m *mockRegistrar) Register(_ context.Context, _ *registry.ServiceInstance) error { + return nil +} + +func (m *mockRegistrar) Deregister(_ context.Context, _ *registry.ServiceInstance) error { + return nil +} + +func TestRegistrar(t *testing.T) { + o := &options{} + v := &mockRegistrar{} + Registrar(v)(o) + if !reflect.DeepEqual(v, o.registrar) { + t.Fatal("o.registrar is not equal to v") + } +} + +func TestRegistrarTimeout(t *testing.T) { + o := &options{} + v := time.Duration(123) + RegistrarTimeout(v)(o) + if !reflect.DeepEqual(v, o.registrarTimeout) { + t.Fatal("o.registrarTimeout is not equal to v") + } +} + +func TestStopTimeout(t *testing.T) { + o := &options{} + v := time.Duration(123) + StopTimeout(v)(o) + if !reflect.DeepEqual(v, o.stopTimeout) { + t.Fatal("o.stopTimeout is not equal to v") + } +} + +func TestBeforeStart(t *testing.T) { + o := &options{} + v := func(_ context.Context) error { + t.Log("BeforeStart...") + return nil + } + BeforeStart(v)(o) +} + +func TestBeforeStop(t *testing.T) { + o := &options{} + v := func(_ context.Context) error { + t.Log("BeforeStop...") + return nil + } + BeforeStop(v)(o) +} + +func TestAfterStart(t *testing.T) { + o := &options{} + v := func(_ context.Context) error { + t.Log("AfterStart...") + return nil + } + AfterStart(v)(o) +} + +func TestAfterStop(t *testing.T) { + o := &options{} + v := func(_ context.Context) error { + t.Log("AfterStop...") + return nil + } + AfterStop(v)(o) +} diff --git a/registry/README.md b/registry/README.md new file mode 100644 index 0000000..d8a74cf --- /dev/null +++ b/registry/README.md @@ -0,0 +1,37 @@ +# Registry + +## Consul + +```shell +go get -u github.com/go-kratos/kratos/contrib/registry/consul/v3 +``` + +## Etcd + +```shell +go get -u github.com/go-kratos/kratos/contrib/registry/etcd/v3 +``` + +## zookeeper + +```shell +go get -u github.com/go-kratos/kratos/contrib/registry/zookeeper/v3 +``` + +## Nacos + +```shell +go get -u github.com/go-kratos/kratos/contrib/registry/nacos/v3 +``` + +## kubernetes + +```shell +go get -u github.com/go-kratos/kratos/contrib/registry/kubernetes/v3 +``` + +## polaris + +```shell +go get -u github.com/go-kratos/kratos/contrib/registry/polaris/v3 +``` diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 0000000..381dbfb --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,95 @@ +package registry + +import ( + "context" + "fmt" + "sort" +) + +// Registrar is service registrar. +type Registrar interface { + // Register the registration. + Register(ctx context.Context, service *ServiceInstance) error + // Deregister the registration. + Deregister(ctx context.Context, service *ServiceInstance) error +} + +// Discovery is service discovery. +type Discovery interface { + // GetService return the service instances in memory according to the service name. + GetService(ctx context.Context, serviceName string) ([]*ServiceInstance, error) + // Watch creates a watcher according to the service name. + Watch(ctx context.Context, serviceName string) (Watcher, error) +} + +// Watcher is service watcher. +type Watcher interface { + // Next returns services in the following two cases: + // 1.the first time to watch and the service instance list is not empty. + // 2.any service instance changes found. + // if the above two conditions are not met, it will block until context deadline exceeded or canceled + Next() ([]*ServiceInstance, error) + // Stop close the watcher. + Stop() error +} + +// ServiceInstance is an instance of a service in a discovery system. +type ServiceInstance struct { + // ID is the unique instance ID as registered. + ID string `json:"id"` + // Name is the service name as registered. + Name string `json:"name"` + // Version is the version of the compiled. + Version string `json:"version"` + // Metadata is the kv pair metadata associated with the service instance. + Metadata map[string]string `json:"metadata"` + // Endpoints are endpoint addresses of the service instance. + // schema: + // http://127.0.0.1:8000?isSecure=false + // grpc://127.0.0.1:9000?isSecure=false + Endpoints []string `json:"endpoints"` +} + +func (i *ServiceInstance) String() string { + return fmt.Sprintf("%s-%s", i.Name, i.ID) +} + +// Equal returns whether i and o are equivalent. +func (i *ServiceInstance) Equal(o any) bool { + if i == nil && o == nil { + return true + } + + if i == nil || o == nil { + return false + } + + t, ok := o.(*ServiceInstance) + if !ok { + return false + } + + if len(i.Endpoints) != len(t.Endpoints) { + return false + } + + sort.Strings(i.Endpoints) + sort.Strings(t.Endpoints) + for j := 0; j < len(i.Endpoints); j++ { + if i.Endpoints[j] != t.Endpoints[j] { + return false + } + } + + if len(i.Metadata) != len(t.Metadata) { + return false + } + + for k, v := range i.Metadata { + if v != t.Metadata[k] { + return false + } + } + + return i.ID == t.ID && i.Name == t.Name && i.Version == t.Version +} diff --git a/selector/balancer.go b/selector/balancer.go new file mode 100644 index 0000000..dbd64bb --- /dev/null +++ b/selector/balancer.go @@ -0,0 +1,38 @@ +package selector + +import ( + "context" + "time" +) + +// Balancer is balancer interface +type Balancer interface { + Pick(ctx context.Context, nodes []WeightedNode) (selected WeightedNode, done DoneFunc, err error) +} + +// BalancerBuilder build balancer +type BalancerBuilder interface { + Build() Balancer +} + +// WeightedNode calculates scheduling weight in real time +type WeightedNode interface { + Node + + // Raw returns the original node + Raw() Node + + // Weight is the runtime calculated weight + Weight() float64 + + // Pick the node + Pick() DoneFunc + + // PickElapsed is time elapsed since the latest pick + PickElapsed() time.Duration +} + +// WeightedNodeBuilder is WeightedNode Builder +type WeightedNodeBuilder interface { + Build(Node) WeightedNode +} diff --git a/selector/default_node.go b/selector/default_node.go new file mode 100644 index 0000000..a4db7b5 --- /dev/null +++ b/selector/default_node.go @@ -0,0 +1,68 @@ +package selector + +import ( + "strconv" + + "github.com/go-kratos/kratos/v3/registry" +) + +var _ Node = (*DefaultNode)(nil) + +// DefaultNode is selector node +type DefaultNode struct { + scheme string + addr string + weight *int64 + version string + name string + metadata map[string]string +} + +// Scheme is node scheme +func (n *DefaultNode) Scheme() string { + return n.scheme +} + +// Address is node address +func (n *DefaultNode) Address() string { + return n.addr +} + +// ServiceName is node serviceName +func (n *DefaultNode) ServiceName() string { + return n.name +} + +// InitialWeight is node initialWeight +func (n *DefaultNode) InitialWeight() *int64 { + return n.weight +} + +// Version is node version +func (n *DefaultNode) Version() string { + return n.version +} + +// Metadata is node metadata +func (n *DefaultNode) Metadata() map[string]string { + return n.metadata +} + +// NewNode new node +func NewNode(scheme, addr string, ins *registry.ServiceInstance) Node { + n := &DefaultNode{ + scheme: scheme, + addr: addr, + } + if ins != nil { + n.name = ins.Name + n.version = ins.Version + n.metadata = ins.Metadata + if str, ok := ins.Metadata["weight"]; ok { + if weight, err := strconv.ParseInt(str, 10, 64); err == nil { + n.weight = &weight + } + } + } + return n +} diff --git a/selector/default_selector.go b/selector/default_selector.go new file mode 100644 index 0000000..83b1ce9 --- /dev/null +++ b/selector/default_selector.go @@ -0,0 +1,86 @@ +package selector + +import ( + "context" + "sync/atomic" +) + +var ( + _ Rebalancer = (*Default)(nil) + _ Builder = (*DefaultBuilder)(nil) +) + +// Default is composite selector. +type Default struct { + NodeBuilder WeightedNodeBuilder + Balancer Balancer + + nodes atomic.Value +} + +// Select is select one node. +func (d *Default) Select(ctx context.Context, opts ...SelectOption) (selected Node, done DoneFunc, err error) { + var ( + options SelectOptions + candidates []WeightedNode + ) + nodes, ok := d.nodes.Load().([]WeightedNode) + if !ok { + return nil, nil, ErrNoAvailable + } + for _, o := range opts { + o(&options) + } + if len(options.NodeFilters) > 0 { + newNodes := make([]Node, len(nodes)) + for i, wc := range nodes { + newNodes[i] = wc + } + for _, filter := range options.NodeFilters { + newNodes = filter(ctx, newNodes) + } + candidates = make([]WeightedNode, len(newNodes)) + for i, n := range newNodes { + candidates[i] = n.(WeightedNode) + } + } else { + candidates = nodes + } + + if len(candidates) == 0 { + return nil, nil, ErrNoAvailable + } + wn, done, err := d.Balancer.Pick(ctx, candidates) + if err != nil { + return nil, nil, err + } + p, ok := FromPeerContext(ctx) + if ok { + p.Node = wn.Raw() + } + return wn.Raw(), done, nil +} + +// Apply update nodes info. +func (d *Default) Apply(nodes []Node) { + weightedNodes := make([]WeightedNode, 0, len(nodes)) + for _, n := range nodes { + weightedNodes = append(weightedNodes, d.NodeBuilder.Build(n)) + } + // TODO: Do not delete unchanged nodes + d.nodes.Store(weightedNodes) +} + +// DefaultBuilder is de +type DefaultBuilder struct { + Node WeightedNodeBuilder + Balancer BalancerBuilder +} + +// Build create builder +func (db *DefaultBuilder) Build() Selector { + return &Default{ + NodeBuilder: db.Node, + Balancer: db.Balancer.Build(), + } +} diff --git a/selector/filter.go b/selector/filter.go new file mode 100644 index 0000000..c8b630d --- /dev/null +++ b/selector/filter.go @@ -0,0 +1,6 @@ +package selector + +import "context" + +// NodeFilter is select filter. +type NodeFilter func(context.Context, []Node) []Node diff --git a/selector/filter/version.go b/selector/filter/version.go new file mode 100644 index 0000000..540ef42 --- /dev/null +++ b/selector/filter/version.go @@ -0,0 +1,20 @@ +package filter + +import ( + "context" + + "github.com/go-kratos/kratos/v3/selector" +) + +// Version is version filter. +func Version(version string) selector.NodeFilter { + return func(_ context.Context, nodes []selector.Node) []selector.Node { + newNodes := make([]selector.Node, 0, len(nodes)) + for _, n := range nodes { + if n.Version() == version { + newNodes = append(newNodes, n) + } + } + return newNodes + } +} diff --git a/selector/filter/version_test.go b/selector/filter/version_test.go new file mode 100644 index 0000000..201bd89 --- /dev/null +++ b/selector/filter/version_test.go @@ -0,0 +1,42 @@ +package filter + +import ( + "context" + "reflect" + "testing" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" +) + +func TestVersion(t *testing.T) { + f := Version("v2.0.0") + var nodes []selector.Node + nodes = append(nodes, selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + })) + + nodes = append(nodes, selector.NewNode( + "http", + "127.0.0.2:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.2:9090", + Name: "helloworld", + Version: "v2.0.0", + Endpoints: []string{"http://127.0.0.2:9090"}, + })) + + nodes = f(context.Background(), nodes) + if !reflect.DeepEqual(len(nodes), 1) { + t.Errorf("expect %v, got %v", 1, len(nodes)) + } + if !reflect.DeepEqual(nodes[0].Address(), "127.0.0.2:9090") { + t.Errorf("expect %v, got %v", nodes[0].Address(), "127.0.0.2:9090") + } +} diff --git a/selector/global.go b/selector/global.go new file mode 100644 index 0000000..4d6b620 --- /dev/null +++ b/selector/global.go @@ -0,0 +1,21 @@ +package selector + +var globalSelector = &wrapSelector{} + +var _ Builder = (*wrapSelector)(nil) + +// wrapSelector wrapped Selector, help override global Selector implementation. +type wrapSelector struct{ Builder } + +// GlobalSelector returns global selector builder. +func GlobalSelector() Builder { + if globalSelector.Builder != nil { + return globalSelector + } + return nil +} + +// SetGlobalSelector set global selector builder. +func SetGlobalSelector(builder Builder) { + globalSelector.Builder = builder +} diff --git a/selector/node/direct/direct.go b/selector/node/direct/direct.go new file mode 100644 index 0000000..4206a86 --- /dev/null +++ b/selector/node/direct/direct.go @@ -0,0 +1,56 @@ +package direct + +import ( + "context" + "sync/atomic" + "time" + + "github.com/go-kratos/kratos/v3/selector" +) + +const ( + defaultWeight = 100 +) + +var ( + _ selector.WeightedNode = (*Node)(nil) + _ selector.WeightedNodeBuilder = (*Builder)(nil) +) + +// Node is endpoint instance +type Node struct { + selector.Node + + // last lastPick timestamp + lastPick atomic.Int64 +} + +// Builder is direct node builder +type Builder struct{} + +// Build create node +func (*Builder) Build(n selector.Node) selector.WeightedNode { + return &Node{Node: n, lastPick: atomic.Int64{}} +} + +func (n *Node) Pick() selector.DoneFunc { + now := time.Now().UnixNano() + n.lastPick.Store(now) + return func(context.Context, selector.DoneInfo) {} +} + +// Weight is node effective weight +func (n *Node) Weight() float64 { + if n.InitialWeight() != nil { + return float64(*n.InitialWeight()) + } + return defaultWeight +} + +func (n *Node) PickElapsed() time.Duration { + return time.Duration(time.Now().UnixNano() - n.lastPick.Load()) +} + +func (n *Node) Raw() selector.Node { + return n.Node +} diff --git a/selector/node/direct/direct_test.go b/selector/node/direct/direct_test.go new file mode 100644 index 0000000..2a8dd72 --- /dev/null +++ b/selector/node/direct/direct_test.go @@ -0,0 +1,70 @@ +package direct + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" +) + +func TestDirect(t *testing.T) { + b := &Builder{} + wn := b.Build(selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + Metadata: map[string]string{"weight": "10"}, + })) + + done := wn.Pick() + if done == nil { + t.Errorf("expect %v, got %v", nil, done) + } + time.Sleep(time.Millisecond * 10) + done(context.Background(), selector.DoneInfo{}) + if !reflect.DeepEqual(float64(10), wn.Weight()) { + t.Errorf("expect %v, got %v", float64(10), wn.Weight()) + } + if time.Millisecond*20 <= wn.PickElapsed() { + t.Errorf("20ms <= wn.PickElapsed()(%s)", wn.PickElapsed()) + } + if time.Millisecond*10 >= wn.PickElapsed() { + t.Errorf("10ms >= wn.PickElapsed()(%s)", wn.PickElapsed()) + } +} + +func TestDirectDefaultWeight(t *testing.T) { + b := &Builder{} + wn := b.Build(selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + })) + + done := wn.Pick() + if done == nil { + t.Errorf("expect %v, got %v", nil, done) + } + time.Sleep(time.Millisecond * 10) + done(context.Background(), selector.DoneInfo{}) + if !reflect.DeepEqual(float64(100), wn.Weight()) { + t.Errorf("expect %v, got %v", float64(100), wn.Weight()) + } + if time.Millisecond*20 <= wn.PickElapsed() { + t.Errorf("time.Millisecond*20 <= wn.PickElapsed()(%s)", wn.PickElapsed()) + } + if time.Millisecond*5 >= wn.PickElapsed() { + t.Errorf("time.Millisecond*5 >= wn.PickElapsed()(%s)", wn.PickElapsed()) + } +} diff --git a/selector/node/ewma/node.go b/selector/node/ewma/node.go new file mode 100644 index 0000000..8354550 --- /dev/null +++ b/selector/node/ewma/node.go @@ -0,0 +1,191 @@ +package ewma + +import ( + "context" + "math" + "net" + "sync/atomic" + "time" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/selector" +) + +const ( + // The mean lifetime of `cost`, it reaches its half-life after Tau*ln(2). + tau = int64(time.Millisecond * 600) + // if statistic not collected,we add a big lag penalty to endpoint + penalty = uint64(time.Microsecond * 100) +) + +var ( + _ selector.WeightedNode = (*Node)(nil) + _ selector.WeightedNodeBuilder = (*Builder)(nil) +) + +// Node is endpoint instance +type Node struct { + selector.Node + + // client statistic data + lag atomic.Int64 + success atomic.Uint64 + inflight atomic.Int64 + inflights [200]atomic.Int64 + // last collected timestamp + stamp atomic.Int64 + // request number in a period time + reqs atomic.Int64 + // last lastPick timestamp + lastPick atomic.Int64 + + errHandler func(err error) (isErr bool) + cachedWeight *atomic.Value +} + +type nodeWeight struct { + value float64 + updateAt int64 +} + +// Builder is ewma node builder. +type Builder struct { + ErrHandler func(err error) (isErr bool) +} + +// Build create a weighted node. +func (b *Builder) Build(n selector.Node) selector.WeightedNode { + s := &Node{ + Node: n, + inflights: [200]atomic.Int64{}, + errHandler: b.ErrHandler, + cachedWeight: &atomic.Value{}, + } + s.success.Store(1000) + s.inflight.Store(1) + return s +} + +func (n *Node) health() uint64 { + return n.success.Load() +} + +func (n *Node) load() (load uint64) { + now := time.Now().UnixNano() + avgLag := n.lag.Load() + predict := n.predict(avgLag, now) + + if avgLag == 0 { + // penalty is the penalty value when there is no data when the node is just started. + load = penalty * uint64(n.inflight.Load()) + return + } + if predict > avgLag { + avgLag = predict + } + // add 5ms to eliminate the latency gap between different zones + avgLag += int64(time.Millisecond * 5) + avgLag = int64(math.Sqrt(float64(avgLag))) + load = uint64(avgLag) * uint64(n.inflight.Load()) + return load +} + +func (n *Node) predict(avgLag int64, now int64) (predict int64) { + var ( + total int64 + slowNum int + totalNum int + ) + for i := range n.inflights { + start := n.inflights[i].Load() + if start != 0 { + totalNum++ + lag := now - start + if lag > avgLag { + slowNum++ + total += lag + } + } + } + if slowNum >= (totalNum/2 + 1) { + predict = total / int64(slowNum) + } + return +} + +// Pick pick a node. +func (n *Node) Pick() selector.DoneFunc { + start := time.Now().UnixNano() + n.lastPick.Store(start) + n.inflight.Add(1) + reqs := n.reqs.Add(1) + slot := reqs % 200 + swapped := n.inflights[slot].CompareAndSwap(0, start) + return func(_ context.Context, di selector.DoneInfo) { + if swapped { + n.inflights[slot].CompareAndSwap(start, 0) + } + n.inflight.Add(-1) + + now := time.Now().UnixNano() + // get moving average ratio w + stamp := n.stamp.Swap(now) + td := now - stamp + if td < 0 { + td = 0 + } + w := math.Exp(float64(-td) / float64(tau)) + + lag := now - start + if lag < 0 { + lag = 0 + } + oldLag := n.lag.Load() + if oldLag == 0 { + w = 0.0 + } + lag = int64(float64(oldLag)*w + float64(lag)*(1.0-w)) + n.lag.Store(lag) + + success := uint64(1000) // error value ,if error set 1 + if di.Err != nil { + if n.errHandler != nil && n.errHandler(di.Err) { + success = 0 + } + var netErr net.Error + if errors.Is(context.DeadlineExceeded, di.Err) || errors.Is(context.Canceled, di.Err) || + errors.IsServiceUnavailable(di.Err) || errors.IsGatewayTimeout(di.Err) || errors.As(di.Err, &netErr) { + success = 0 + } + } + oldSuc := n.success.Load() + success = uint64(float64(oldSuc)*w + float64(success)*(1.0-w)) + n.success.Store(success) + } +} + +// Weight is node effective weight. +func (n *Node) Weight() (weight float64) { + w, ok := n.cachedWeight.Load().(*nodeWeight) + now := time.Now().UnixNano() + if !ok || time.Duration(now-w.updateAt) > (time.Millisecond*5) { + health := n.health() + load := n.load() + weight = float64(health*uint64(time.Microsecond)*10) / float64(load) + n.cachedWeight.Store(&nodeWeight{ + value: weight, + updateAt: now, + }) + } else { + weight = w.value + } + return +} + +func (n *Node) PickElapsed() time.Duration { + return time.Duration(time.Now().UnixNano() - n.lastPick.Load()) +} + +func (n *Node) Raw() selector.Node { + return n.Node +} diff --git a/selector/node/ewma/node_test.go b/selector/node/ewma/node_test.go new file mode 100644 index 0000000..f3fe3e8 --- /dev/null +++ b/selector/node/ewma/node_test.go @@ -0,0 +1,149 @@ +package ewma + +import ( + "context" + "net" + "reflect" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" +) + +func TestDirect(t *testing.T) { + b := &Builder{} + wn := b.Build(selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + Metadata: map[string]string{"weight": "10"}, + })) + + if !reflect.DeepEqual(float64(100), wn.Weight()) { + t.Errorf("expect %v, got %v", 100, wn.Weight()) + } + done := wn.Pick() + if done == nil { + t.Errorf("done is equal to nil") + } + done2 := wn.Pick() + if done2 == nil { + t.Errorf("done2 is equal to nil") + } + + time.Sleep(time.Millisecond * 15) + done(context.Background(), selector.DoneInfo{}) + if float64(70) >= wn.Weight() { + t.Errorf("float64(30000) >= wn.Weight()(%v)", wn.Weight()) + } + if float64(1200) <= wn.Weight() { + t.Errorf("float64(1000) <= wn.Weight()(%v)", wn.Weight()) + } + if time.Millisecond*30 <= wn.PickElapsed() { + t.Errorf("time.Millisecond*30 <= wn.PickElapsed()(%v)", wn.PickElapsed()) + } + if time.Millisecond*5 >= wn.PickElapsed() { + t.Errorf("time.Millisecond*5 >= wn.PickElapsed()(%v)", wn.PickElapsed()) + } +} + +func TestDirectError(t *testing.T) { + b := &Builder{} + wn := b.Build(selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + Metadata: map[string]string{"weight": "10"}, + })) + + for i := 0; i < 5; i++ { + var err error + if i != 0 { + err = context.DeadlineExceeded + } + done := wn.Pick() + if done == nil { + t.Errorf("expect not nil, got nil") + } + time.Sleep(time.Millisecond * 20) + done(context.Background(), selector.DoneInfo{Err: err}) + } + if float64(1000) >= wn.Weight() { + t.Errorf("float64(1000) >= wn.Weight()(%v)", wn.Weight()) + } + if float64(2000) <= wn.Weight() { + t.Errorf("float64(2000) <= wn.Weight()(%v)", wn.Weight()) + } +} + +func TestDirectErrorHandler(t *testing.T) { + b := &Builder{ + ErrHandler: func(err error) bool { + return err != nil + }, + } + wn := b.Build(selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + Metadata: map[string]string{"weight": "10"}, + })) + errs := []error{ + context.DeadlineExceeded, + context.Canceled, + net.ErrClosed, + } + for i := 0; i < 5; i++ { + var err error + if i != 0 { + err = errs[i%len(errs)] + } + done := wn.Pick() + if done == nil { + t.Errorf("expect not nil, got nil") + } + time.Sleep(time.Millisecond * 20) + done(context.Background(), selector.DoneInfo{Err: err}) + } + if float64(1000) >= wn.Weight() { + t.Errorf("float64(100) >= wn.Weight()(%v)", wn.Weight()) + } + if float64(2000) <= wn.Weight() { + t.Errorf("float64(200) <= wn.Weight()(%v)", wn.Weight()) + } +} + +func BenchmarkPickAndWeight(b *testing.B) { + bu := &Builder{} + node := bu.Build(selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + Metadata: map[string]string{"weight": "10"}, + })) + di := selector.DoneInfo{} + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + done := node.Pick() + node.Weight() + done(context.Background(), di) + } + }) +} diff --git a/selector/options.go b/selector/options.go new file mode 100644 index 0000000..14b441d --- /dev/null +++ b/selector/options.go @@ -0,0 +1,16 @@ +package selector + +// SelectOptions is Select Options. +type SelectOptions struct { + NodeFilters []NodeFilter +} + +// SelectOption is Selector option. +type SelectOption func(*SelectOptions) + +// WithNodeFilter with filter options +func WithNodeFilter(fn ...NodeFilter) SelectOption { + return func(opts *SelectOptions) { + opts.NodeFilters = fn + } +} diff --git a/selector/p2c/p2c.go b/selector/p2c/p2c.go new file mode 100644 index 0000000..faa783c --- /dev/null +++ b/selector/p2c/p2c.go @@ -0,0 +1,100 @@ +package p2c + +import ( + "context" + "math/rand/v2" + "sync" + "sync/atomic" + "time" + + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/node/ewma" +) + +const ( + forcePick = time.Second * 3 + // Name is p2c(Pick of 2 choices) balancer name + Name = "p2c" +) + +var _ selector.Balancer = (*Balancer)(nil) + +// Option is p2c builder option. +type Option func(o *options) + +// options is p2c builder options +type options struct{} + +// New creates a p2c selector. +func New(opts ...Option) selector.Selector { + return NewBuilder(opts...).Build() +} + +// Balancer is p2c selector. +type Balancer struct { + mu sync.Mutex + r *rand.Rand + picked atomic.Bool +} + +// choose two distinct nodes. +func (s *Balancer) prePick(nodes []selector.WeightedNode) (nodeA selector.WeightedNode, nodeB selector.WeightedNode) { + s.mu.Lock() + a := s.r.IntN(len(nodes)) + b := s.r.IntN(len(nodes) - 1) + s.mu.Unlock() + if b >= a { + b = b + 1 + } + nodeA, nodeB = nodes[a], nodes[b] + return +} + +// Pick pick a node. +func (s *Balancer) Pick(_ context.Context, nodes []selector.WeightedNode) (selector.WeightedNode, selector.DoneFunc, error) { + if len(nodes) == 0 { + return nil, nil, selector.ErrNoAvailable + } + if len(nodes) == 1 { + done := nodes[0].Pick() + return nodes[0], done, nil + } + + var pc, upc selector.WeightedNode + nodeA, nodeB := s.prePick(nodes) + // meta.Weight is the weight set by the service publisher in discovery + if nodeB.Weight() > nodeA.Weight() { + pc, upc = nodeB, nodeA + } else { + pc, upc = nodeA, nodeB + } + + // If the failed node has never been selected once during forceGap, it is forced to be selected once + // Take advantage of forced opportunities to trigger updates of success rate and delay + if upc.PickElapsed() > forcePick && s.picked.CompareAndSwap(false, true) { + defer s.picked.Store(false) + pc = upc + } + done := pc.Pick() + return pc, done, nil +} + +// NewBuilder returns a selector builder with p2c balancer +func NewBuilder(opts ...Option) selector.Builder { + var option options + for _, opt := range opts { + opt(&option) + } + return &selector.DefaultBuilder{ + Balancer: &Builder{}, + Node: &ewma.Builder{}, + } +} + +// Builder is p2c builder +type Builder struct{} + +// Build creates Balancer +func (b *Builder) Build() selector.Balancer { + return &Balancer{r: rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), 0))} +} diff --git a/selector/p2c/p2c_test.go b/selector/p2c/p2c_test.go new file mode 100644 index 0000000..588ee02 --- /dev/null +++ b/selector/p2c/p2c_test.go @@ -0,0 +1,122 @@ +package p2c + +import ( + "context" + "fmt" + "math/rand/v2" + "reflect" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/filter" +) + +func TestWrr3(t *testing.T) { + p2c := New() + var nodes []selector.Node + for i := 0; i < 3; i++ { + addr := fmt.Sprintf("127.0.0.%d:8080", i) + nodes = append(nodes, selector.NewNode( + "http", + addr, + ®istry.ServiceInstance{ + ID: addr, + Version: "v2.0.0", + Metadata: map[string]string{"weight": "10"}, + })) + } + p2c.Apply(nodes) + var count1, count2, count3 int64 + group := &sync.WaitGroup{} + var lk sync.Mutex + for i := 0; i < 9000; i++ { + group.Add(1) + go func() { + defer group.Done() + lk.Lock() + d := time.Duration(rand.IntN(500)) * time.Millisecond + lk.Unlock() + time.Sleep(d) + n, done, err := p2c.Select(context.Background(), selector.WithNodeFilter(filter.Version("v2.0.0"))) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if n == nil { + t.Errorf("expect %v, got %v", nil, n) + } + if done == nil { + t.Errorf("expect %v, got %v", nil, done) + } + time.Sleep(time.Millisecond * 10) + done(context.Background(), selector.DoneInfo{}) + if n.Address() == "127.0.0.0:8080" { + atomic.AddInt64(&count1, 1) + } else if n.Address() == "127.0.0.1:8080" { + atomic.AddInt64(&count2, 1) + } else if n.Address() == "127.0.0.2:8080" { + atomic.AddInt64(&count3, 1) + } + }() + } + group.Wait() + if count1 <= int64(1500) { + t.Errorf("count1(%v) <= int64(1500)", count1) + } + if count1 >= int64(4500) { + t.Errorf("count1(%v) >= int64(4500),", count1) + } + if count2 <= int64(1500) { + t.Errorf("count2(%v) <= int64(1500)", count2) + } + if count2 >= int64(4500) { + t.Errorf("count2(%v) >= int64(4500),", count2) + } + if count3 <= int64(1500) { + t.Errorf("count3(%v) <= int64(1500)", count3) + } + if count3 >= int64(4500) { + t.Errorf("count3(%v) >= int64(4500),", count3) + } +} + +func TestEmpty(t *testing.T) { + b := &Balancer{} + _, _, err := b.Pick(context.Background(), []selector.WeightedNode{}) + if err == nil { + t.Errorf("expect %v, got %v", nil, err) + } +} + +func TestOne(t *testing.T) { + p2c := New() + var nodes []selector.Node + for i := 0; i < 1; i++ { + addr := fmt.Sprintf("127.0.0.%d:8080", i) + nodes = append(nodes, selector.NewNode( + "http", + addr, + ®istry.ServiceInstance{ + ID: addr, + Version: "v2.0.0", + Metadata: map[string]string{"weight": "10"}, + })) + } + p2c.Apply(nodes) + n, done, err := p2c.Select(context.Background(), selector.WithNodeFilter(filter.Version("v2.0.0"))) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if n == nil { + t.Errorf("expect %v, got %v", nil, n) + } + if done == nil { + t.Errorf("expect %v, got %v", nil, done) + } + if !reflect.DeepEqual("127.0.0.0:8080", n.Address()) { + t.Errorf("expect %v, got %v", "127.0.0.0:8080", n.Address()) + } +} diff --git a/selector/peer.go b/selector/peer.go new file mode 100644 index 0000000..273ba06 --- /dev/null +++ b/selector/peer.go @@ -0,0 +1,25 @@ +package selector + +import ( + "context" +) + +type peerKey struct{} + +// Peer contains the information of the peer for an RPC, such as the address +// and authentication information. +type Peer struct { + // node is the peer node. + Node Node +} + +// NewPeerContext creates a new context with peer information attached. +func NewPeerContext(ctx context.Context, p *Peer) context.Context { + return context.WithValue(ctx, peerKey{}, p) +} + +// FromPeerContext returns the peer information in ctx if it exists. +func FromPeerContext(ctx context.Context) (p *Peer, ok bool) { + p, ok = ctx.Value(peerKey{}).(*Peer) + return +} diff --git a/selector/peer_test.go b/selector/peer_test.go new file mode 100644 index 0000000..d0eeeb8 --- /dev/null +++ b/selector/peer_test.go @@ -0,0 +1,24 @@ +package selector + +import ( + "context" + "testing" +) + +func TestPeer(t *testing.T) { + p := Peer{ + Node: mockWeightedNode{}, + } + ctx := NewPeerContext(context.Background(), &p) + p2, ok := FromPeerContext(ctx) + if !ok || p2.Node == nil { + t.Fatalf(" no peer found!") + } +} + +func TestNotPeer(t *testing.T) { + _, ok := FromPeerContext(context.Background()) + if ok { + t.Fatalf("test no peer found peer!") + } +} diff --git a/selector/random/random.go b/selector/random/random.go new file mode 100644 index 0000000..0e91af0 --- /dev/null +++ b/selector/random/random.go @@ -0,0 +1,61 @@ +package random + +import ( + "context" + "math/rand/v2" + + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/node/direct" +) + +const ( + // Name is random balancer name + Name = "random" +) + +var _ selector.Balancer = (*Balancer)(nil) + +// Option is random builder option. +type Option func(o *options) + +// options is random builder options +type options struct{} + +// Balancer is a random balancer. +type Balancer struct{} + +// New a random selector. +func New(opts ...Option) selector.Selector { + return NewBuilder(opts...).Build() +} + +// Pick is pick a weighted node. +func (p *Balancer) Pick(_ context.Context, nodes []selector.WeightedNode) (selector.WeightedNode, selector.DoneFunc, error) { + if len(nodes) == 0 { + return nil, nil, selector.ErrNoAvailable + } + cur := rand.IntN(len(nodes)) + selected := nodes[cur] + d := selected.Pick() + return selected, d, nil +} + +// NewBuilder returns a selector builder with random balancer +func NewBuilder(opts ...Option) selector.Builder { + var option options + for _, opt := range opts { + opt(&option) + } + return &selector.DefaultBuilder{ + Balancer: &Builder{}, + Node: &direct.Builder{}, + } +} + +// Builder is random builder +type Builder struct{} + +// Build creates Balancer +func (b *Builder) Build() selector.Balancer { + return &Balancer{} +} diff --git a/selector/random/random_test.go b/selector/random/random_test.go new file mode 100644 index 0000000..a272195 --- /dev/null +++ b/selector/random/random_test.go @@ -0,0 +1,71 @@ +package random + +import ( + "context" + "testing" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/filter" +) + +func TestWrr(t *testing.T) { + random := New() + nodes := make([]selector.Node, 0, 2) + nodes = append(nodes, selector.NewNode( + "http", + "127.0.0.1:8080", + ®istry.ServiceInstance{ + ID: "127.0.0.1:8080", + Version: "v2.0.0", + Metadata: map[string]string{"weight": "10"}, + })) + nodes = append(nodes, selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Version: "v2.0.0", + Metadata: map[string]string{"weight": "20"}, + })) + random.Apply(nodes) + var count1, count2 int + for i := 0; i < 1000; i++ { + n, done, err := random.Select(context.Background(), selector.WithNodeFilter(filter.Version("v2.0.0"))) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if done == nil { + t.Errorf("expect not nil, got:%v", done) + } + if n == nil { + t.Errorf("expect not nil, got:%v", n) + } + done(context.Background(), selector.DoneInfo{}) + if n.Address() == "127.0.0.1:8080" { + count1++ + } else if n.Address() == "127.0.0.1:9090" { + count2++ + } + } + if count1 <= 400 { + t.Errorf("count1(%v) <= 400", count1) + } + if count1 >= 600 { + t.Errorf("count1(%v) >= 600", count1) + } + if count2 <= 400 { + t.Errorf("count2(%v) <= 400", count2) + } + if count2 >= 600 { + t.Errorf("count2(%v) >= 600", count2) + } +} + +func TestEmpty(t *testing.T) { + b := &Balancer{} + _, _, err := b.Pick(context.Background(), []selector.WeightedNode{}) + if err == nil { + t.Errorf("expect nil, got %v", err) + } +} diff --git a/selector/selector.go b/selector/selector.go new file mode 100644 index 0000000..035db34 --- /dev/null +++ b/selector/selector.go @@ -0,0 +1,74 @@ +package selector + +import ( + "context" + + "github.com/go-kratos/kratos/v3/errors" +) + +// ErrNoAvailable is no available node. +var ErrNoAvailable = errors.ServiceUnavailable("no_available_node", "") + +// Selector is node pick balancer. +type Selector interface { + Rebalancer + + // Select nodes + // if err == nil, selected and done must not be empty. + Select(ctx context.Context, opts ...SelectOption) (selected Node, done DoneFunc, err error) +} + +// Rebalancer is nodes rebalancer. +type Rebalancer interface { + // Apply is apply all nodes when any changes happen + Apply(nodes []Node) +} + +// Builder build selector +type Builder interface { + Build() Selector +} + +// Node is node interface. +type Node interface { + // Scheme is service node scheme + Scheme() string + + // Address is the unique address under the same service + Address() string + + // ServiceName is service name + ServiceName() string + + // InitialWeight is the initial value of scheduling weight + // if not set return nil + InitialWeight() *int64 + + // Version is service node version + Version() string + + // Metadata is the kv pair metadata associated with the service instance. + // version,namespace,region,protocol etc.. + Metadata() map[string]string +} + +// DoneInfo is callback info when RPC invoke done. +type DoneInfo struct { + // Response Error + Err error + // Response Metadata + ReplyMD ReplyMD + + // BytesSent indicates if any bytes have been sent to the server. + BytesSent bool + // BytesReceived indicates if any byte has been received from the server. + BytesReceived bool +} + +// ReplyMD is Reply Metadata. +type ReplyMD interface { + Get(key string) string +} + +// DoneFunc is callback function when RPC invoke done. +type DoneFunc func(ctx context.Context, di DoneInfo) diff --git a/selector/selector_test.go b/selector/selector_test.go new file mode 100644 index 0000000..b030ee2 --- /dev/null +++ b/selector/selector_test.go @@ -0,0 +1,291 @@ +package selector + +import ( + "context" + "errors" + "math/rand/v2" + "reflect" + "sync/atomic" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" +) + +var errNodeNotMatch = errors.New("node is not match") + +type mockWeightedNode struct { + Node + + lastPick int64 +} + +// Raw returns the original node +func (n *mockWeightedNode) Raw() Node { + return n.Node +} + +// Weight is the runtime calculated weight +func (n *mockWeightedNode) Weight() float64 { + if n.InitialWeight() != nil { + return float64(*n.InitialWeight()) + } + return 100 +} + +// Pick the node +func (n *mockWeightedNode) Pick() DoneFunc { + now := time.Now().UnixNano() + atomic.StoreInt64(&n.lastPick, now) + return func(context.Context, DoneInfo) {} +} + +// PickElapsed is time elapsed since the latest pick +func (n *mockWeightedNode) PickElapsed() time.Duration { + return time.Duration(time.Now().UnixNano() - atomic.LoadInt64(&n.lastPick)) +} + +type mockWeightedNodeBuilder struct{} + +func (b *mockWeightedNodeBuilder) Build(n Node) WeightedNode { + return &mockWeightedNode{Node: n} +} + +func mockFilter(version string) NodeFilter { + return func(_ context.Context, nodes []Node) []Node { + newNodes := nodes[:0] + for _, n := range nodes { + if n.Version() == version { + newNodes = append(newNodes, n) + } + } + return newNodes + } +} + +type mockBalancerBuilder struct{} + +func (b *mockBalancerBuilder) Build() Balancer { + return &mockBalancer{} +} + +type mockBalancer struct{} + +func (b *mockBalancer) Pick(_ context.Context, nodes []WeightedNode) (selected WeightedNode, done DoneFunc, err error) { + if len(nodes) == 0 { + err = ErrNoAvailable + return + } + cur := rand.IntN(len(nodes)) + selected = nodes[cur] + done = selected.Pick() + return +} + +type mockMustErrorBalancerBuilder struct{} + +func (b *mockMustErrorBalancerBuilder) Build() Balancer { + return &mockMustErrorBalancer{} +} + +type mockMustErrorBalancer struct{} + +func (b *mockMustErrorBalancer) Pick(_ context.Context, _ []WeightedNode) (selected WeightedNode, done DoneFunc, err error) { + return nil, nil, errNodeNotMatch +} + +func TestDefault(t *testing.T) { + builder := DefaultBuilder{ + Node: &mockWeightedNodeBuilder{}, + Balancer: &mockBalancerBuilder{}, + } + selector := builder.Build() + nodes := make([]Node, 0, 2) + nodes = append(nodes, NewNode( + "http", + "127.0.0.1:8080", + ®istry.ServiceInstance{ + ID: "127.0.0.1:8080", + Name: "helloworld", + Version: "v2.0.0", + Endpoints: []string{"http://127.0.0.1:8080"}, + Metadata: map[string]string{"weight": "10"}, + })) + nodes = append(nodes, NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + Metadata: map[string]string{"weight": "10"}, + })) + + selector.Apply(nodes) + n, done, err := selector.Select(context.Background(), WithNodeFilter(mockFilter("v2.0.0"))) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if n == nil { + t.Errorf("expect %v, got %v", nil, n) + } + if done == nil { + t.Errorf("expect %v, got %v", nil, done) + } + if !reflect.DeepEqual("v2.0.0", n.Version()) { + t.Errorf("expect %v, got %v", "v2.0.0", n.Version()) + } + if n.Scheme() == "" { + t.Errorf("expect %v, got %v", "", n.Scheme()) + } + if n.Address() == "" { + t.Errorf("expect %v, got %v", "", n.Address()) + } + if !reflect.DeepEqual(int64(10), *n.InitialWeight()) { + t.Errorf("expect %v, got %v", 10, *n.InitialWeight()) + } + if n.Metadata() == nil { + t.Errorf("expect %v, got %v", nil, n.Metadata()) + } + if !reflect.DeepEqual("helloworld", n.ServiceName()) { + t.Errorf("expect %v, got %v", "helloworld", n.ServiceName()) + } + done(context.Background(), DoneInfo{}) + + // peer in ctx + ctx := NewPeerContext(context.Background(), &Peer{ + Node: mockWeightedNode{}, + }) + n, done, err = selector.Select(ctx) + if err != nil { + t.Errorf("expect %v, got %v", ErrNoAvailable, err) + } + if done == nil { + t.Errorf("expect %v, got %v", nil, done) + } + if n == nil { + t.Errorf("expect %v, got %v", nil, n) + } + + // no v3.0.0 instance + n, done, err = selector.Select(context.Background(), WithNodeFilter(mockFilter("v3.0.0"))) + if !errors.Is(ErrNoAvailable, err) { + t.Errorf("expect %v, got %v", ErrNoAvailable, err) + } + if done != nil { + t.Errorf("expect %v, got %v", nil, done) + } + if n != nil { + t.Errorf("expect %v, got %v", nil, n) + } + + // apply zero instance + selector.Apply([]Node{}) + n, done, err = selector.Select(context.Background(), WithNodeFilter(mockFilter("v2.0.0"))) + if !errors.Is(ErrNoAvailable, err) { + t.Errorf("expect %v, got %v", ErrNoAvailable, err) + } + if done != nil { + t.Errorf("expect %v, got %v", nil, done) + } + if n != nil { + t.Errorf("expect %v, got %v", nil, n) + } + + // apply zero instance + selector.Apply(nil) + n, done, err = selector.Select(context.Background(), WithNodeFilter(mockFilter("v2.0.0"))) + if !errors.Is(ErrNoAvailable, err) { + t.Errorf("expect %v, got %v", ErrNoAvailable, err) + } + if done != nil { + t.Errorf("expect %v, got %v", nil, done) + } + if n != nil { + t.Errorf("expect %v, got %v", nil, n) + } + + // without node_filters + n, done, err = selector.Select(context.Background()) + if !errors.Is(ErrNoAvailable, err) { + t.Errorf("expect %v, got %v", ErrNoAvailable, err) + } + if done != nil { + t.Errorf("expect %v, got %v", nil, done) + } + if n != nil { + t.Errorf("expect %v, got %v", nil, n) + } +} + +func TestWithoutApply(t *testing.T) { + builder := DefaultBuilder{ + Node: &mockWeightedNodeBuilder{}, + Balancer: &mockBalancerBuilder{}, + } + selector := builder.Build() + n, done, err := selector.Select(context.Background()) + if !errors.Is(ErrNoAvailable, err) { + t.Errorf("expect %v, got %v", ErrNoAvailable, err) + } + if done != nil { + t.Errorf("expect %v, got %v", nil, done) + } + if n != nil { + t.Errorf("expect %v, got %v", nil, n) + } +} + +func TestNoPick(t *testing.T) { + builder := DefaultBuilder{ + Node: &mockWeightedNodeBuilder{}, + Balancer: &mockMustErrorBalancerBuilder{}, + } + nodes := make([]Node, 0, 2) + nodes = append(nodes, NewNode( + "http", + "127.0.0.1:8080", + ®istry.ServiceInstance{ + ID: "127.0.0.1:8080", + Name: "helloworld", + Version: "v2.0.0", + Endpoints: []string{"http://127.0.0.1:8080"}, + Metadata: map[string]string{"weight": "10"}, + })) + nodes = append(nodes, NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Name: "helloworld", + Version: "v1.0.0", + Endpoints: []string{"http://127.0.0.1:9090"}, + Metadata: map[string]string{"weight": "10"}, + })) + selector := builder.Build() + selector.Apply(nodes) + n, done, err := selector.Select(context.Background()) + if !errors.Is(errNodeNotMatch, err) { + t.Errorf("expect %v, got %v", errNodeNotMatch, err) + } + if done != nil { + t.Errorf("expect %v, got %v", nil, done) + } + if n != nil { + t.Errorf("expect %v, got %v", nil, n) + } +} + +func TestGlobalSelector(t *testing.T) { + builder := DefaultBuilder{ + Node: &mockWeightedNodeBuilder{}, + Balancer: &mockBalancerBuilder{}, + } + SetGlobalSelector(&builder) + + gBuilder := GlobalSelector() + if gBuilder == nil { + t.Errorf("expect %v, got %v", nil, gBuilder) + } +} diff --git a/selector/wrr/wrr.go b/selector/wrr/wrr.go new file mode 100644 index 0000000..f8975c1 --- /dev/null +++ b/selector/wrr/wrr.go @@ -0,0 +1,127 @@ +package wrr + +import ( + "context" + "sync" + + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/node/direct" +) + +const ( + // Name is wrr(Weighted Round Robin) balancer name + Name = "wrr" +) + +var _ selector.Balancer = (*Balancer)(nil) + +// Option is wrr builder option. +type Option func(o *options) + +// options is wrr builder options +type options struct{} + +// Balancer is a wrr balancer. +type Balancer struct { + mu sync.Mutex + currentWeight map[string]float64 + lastNodes []selector.WeightedNode +} + +// equalNodes checks if two slices of WeightedNode contain the same nodes +func equalNodes(a, b []selector.WeightedNode) bool { + if len(a) != len(b) { + return false + } + + // Create a map of addresses from slice a + aMap := make(map[string]bool, len(a)) + for _, node := range a { + aMap[node.Address()] = true + } + + // Check if all nodes in slice b exist in slice a + for _, node := range b { + if !aMap[node.Address()] { + return false + } + } + + return true +} + +// New random a selector. +func New(opts ...Option) selector.Selector { + return NewBuilder(opts...).Build() +} + +// Pick is pick a weighted node. +func (p *Balancer) Pick(_ context.Context, nodes []selector.WeightedNode) (selector.WeightedNode, selector.DoneFunc, error) { + if len(nodes) == 0 { + return nil, nil, selector.ErrNoAvailable + } + + p.mu.Lock() + defer p.mu.Unlock() + + // Check if the node list has changed + if !equalNodes(p.lastNodes, nodes) { + // Update lastNodes + p.lastNodes = make([]selector.WeightedNode, len(nodes)) + copy(p.lastNodes, nodes) + + // Create a set of current node addresses for cleanup + currentNodes := make(map[string]bool, len(nodes)) + for _, node := range nodes { + currentNodes[node.Address()] = true + } + + // Clean up stale entries from currentWeight map + for address := range p.currentWeight { + if !currentNodes[address] { + delete(p.currentWeight, address) + } + } + } + + var totalWeight float64 + var selected selector.WeightedNode + var selectWeight float64 + + // nginx wrr load balancing algorithm: http://blog.csdn.net/zhangskd/article/details/50194069 + for _, node := range nodes { + totalWeight += node.Weight() + cwt := p.currentWeight[node.Address()] + // current += effectiveWeight + cwt += node.Weight() + p.currentWeight[node.Address()] = cwt + if selected == nil || selectWeight < cwt { + selectWeight = cwt + selected = node + } + } + p.currentWeight[selected.Address()] = selectWeight - totalWeight + + d := selected.Pick() + return selected, d, nil +} + +// NewBuilder returns a selector builder with wrr balancer +func NewBuilder(opts ...Option) selector.Builder { + var option options + for _, opt := range opts { + opt(&option) + } + return &selector.DefaultBuilder{ + Balancer: &Builder{}, + Node: &direct.Builder{}, + } +} + +// Builder is wrr builder +type Builder struct{} + +// Build creates Balancer +func (b *Builder) Build() selector.Balancer { + return &Balancer{currentWeight: make(map[string]float64)} +} diff --git a/selector/wrr/wrr_test.go b/selector/wrr/wrr_test.go new file mode 100644 index 0000000..f880f7b --- /dev/null +++ b/selector/wrr/wrr_test.go @@ -0,0 +1,331 @@ +package wrr + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/filter" +) + +func TestWrr(t *testing.T) { + wrr := New() + nodes := make([]selector.Node, 0, 2) + nodes = append(nodes, selector.NewNode( + "http", + "127.0.0.1:8080", + ®istry.ServiceInstance{ + ID: "127.0.0.1:8080", + Version: "v2.0.0", + Metadata: map[string]string{"weight": "10"}, + })) + nodes = append(nodes, selector.NewNode( + "http", + "127.0.0.1:9090", + ®istry.ServiceInstance{ + ID: "127.0.0.1:9090", + Version: "v2.0.0", + Metadata: map[string]string{"weight": "20"}, + })) + wrr.Apply(nodes) + var count1, count2 int + for i := 0; i < 90; i++ { + n, done, err := wrr.Select(context.Background(), selector.WithNodeFilter(filter.Version("v2.0.0"))) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + if done == nil { + t.Errorf("expect done callback, got nil") + } + if n == nil { + t.Errorf("expect node, got nil") + } + done(context.Background(), selector.DoneInfo{}) + if n.Address() == "127.0.0.1:8080" { + count1++ + } else if n.Address() == "127.0.0.1:9090" { + count2++ + } + } + if !reflect.DeepEqual(count1, 30) { + t.Errorf("expect 30, got %d", count1) + } + if !reflect.DeepEqual(count2, 60) { + t.Errorf("expect 60, got %d", count2) + } +} + +// TestCurrentWeightCleanup tests that stale entries in currentWeight map are cleaned up +func TestCurrentWeightCleanup(t *testing.T) { + balancer := &Balancer{currentWeight: make(map[string]float64)} + + // Create initial nodes + nodes1 := []selector.WeightedNode{ + &mockWeightedNode{address: "node1", weight: 10}, + &mockWeightedNode{address: "node2", weight: 20}, + &mockWeightedNode{address: "node3", weight: 30}, + } + + // Pick from initial nodes to populate currentWeight + for i := 0; i < 10; i++ { + _, _, err := balancer.Pick(context.Background(), nodes1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + + // Verify all 3 nodes are in currentWeight map + if len(balancer.currentWeight) != 3 { + t.Errorf("expected 3 entries in currentWeight, got %d", len(balancer.currentWeight)) + } + + // Change to different set of nodes (simulating service discovery update) + nodes2 := []selector.WeightedNode{ + &mockWeightedNode{address: "node2", weight: 20}, // only node2 remains + &mockWeightedNode{address: "node4", weight: 40}, // node4 is new + } + + // Pick from new nodes + _, _, err := balancer.Pick(context.Background(), nodes2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify that stale entries (node1, node3) are cleaned up + if len(balancer.currentWeight) != 2 { + t.Errorf("expected 2 entries in currentWeight after cleanup, got %d", len(balancer.currentWeight)) + } + + // Verify that node2 and node4 are present, but node1 and node3 are not + if _, exists := balancer.currentWeight["node1"]; exists { + t.Error("stale entry node1 should have been cleaned up") + } + if _, exists := balancer.currentWeight["node3"]; exists { + t.Error("stale entry node3 should have been cleaned up") + } + if _, exists := balancer.currentWeight["node2"]; !exists { + t.Error("node2 should be present in currentWeight") + } + if _, exists := balancer.currentWeight["node4"]; !exists { + t.Error("node4 should be present in currentWeight") + } +} + +// TestCleanupOnlyWhenNodesChange verifies that cleanup logic only runs when nodes actually change +func TestCleanupOnlyWhenNodesChange(t *testing.T) { + // Create a custom balancer that tracks cleanup calls + type trackingBalancer struct { + *Balancer + cleanupCount int + } + + // Override the Pick method to count cleanup operations + balancer := &trackingBalancer{ + Balancer: &Balancer{currentWeight: make(map[string]float64)}, + } + + originalPick := func(_ context.Context, nodes []selector.WeightedNode) (selector.WeightedNode, selector.DoneFunc, error) { + if len(nodes) == 0 { + return nil, nil, selector.ErrNoAvailable + } + + balancer.mu.Lock() + defer balancer.mu.Unlock() + + // Check if the node list has changed + if len(balancer.lastNodes) != len(nodes) || !equalNodes(balancer.lastNodes, nodes) { + balancer.cleanupCount++ // Count cleanup operations + + // Update lastNodes + balancer.lastNodes = make([]selector.WeightedNode, len(nodes)) + copy(balancer.lastNodes, nodes) + + // Create a set of current node addresses for cleanup + currentNodes := make(map[string]bool) + for _, node := range nodes { + currentNodes[node.Address()] = true + } + + // Clean up stale entries from currentWeight map + for address := range balancer.currentWeight { + if !currentNodes[address] { + delete(balancer.currentWeight, address) + } + } + } + + var totalWeight float64 + var selected selector.WeightedNode + var selectWeight float64 + + // nginx wrr load balancing algorithm + for _, node := range nodes { + totalWeight += node.Weight() + cwt := balancer.currentWeight[node.Address()] + cwt += node.Weight() + balancer.currentWeight[node.Address()] = cwt + if selected == nil || selectWeight < cwt { + selectWeight = cwt + selected = node + } + } + balancer.currentWeight[selected.Address()] = selectWeight - totalWeight + + d := selected.Pick() + return selected, d, nil + } + + ctx := context.Background() + nodes1 := []selector.WeightedNode{ + &mockWeightedNode{address: "node1", weight: 10}, + &mockWeightedNode{address: "node2", weight: 20}, + } + + nodes2 := []selector.WeightedNode{ + &mockWeightedNode{address: "node3", weight: 30}, + &mockWeightedNode{address: "node4", weight: 40}, + } + + var err error + + // First call with nodes1 - should trigger cleanup (initialization) + _, _, err = originalPick(ctx, nodes1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if balancer.cleanupCount != 1 { + t.Errorf("expected 1 cleanup call after first pick, got %d", balancer.cleanupCount) + } + + // Multiple calls with same nodes1 - should NOT trigger additional cleanup + for i := 0; i < 5; i++ { + _, _, err = originalPick(ctx, nodes1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + + if balancer.cleanupCount != 1 { + t.Errorf("expected still 1 cleanup call after repeated picks with same nodes, got %d", balancer.cleanupCount) + } + + // Call with different nodes2 - should trigger cleanup + _, _, err = originalPick(ctx, nodes2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if balancer.cleanupCount != 2 { + t.Errorf("expected 2 cleanup calls after node change, got %d", balancer.cleanupCount) + } + + // Multiple calls with same nodes2 - should NOT trigger additional cleanup + for i := 0; i < 3; i++ { + _, _, err = originalPick(ctx, nodes2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + + if balancer.cleanupCount != 2 { + t.Errorf("expected still 2 cleanup calls after repeated picks with same nodes, got %d", balancer.cleanupCount) + } +} + +// mockWeightedNode is a mock implementation for testing +type mockWeightedNode struct { + address string + weight float64 +} + +func (m *mockWeightedNode) Raw() selector.Node { return nil } +func (m *mockWeightedNode) Weight() float64 { return m.weight } +func (m *mockWeightedNode) Address() string { return m.address } +func (m *mockWeightedNode) Pick() selector.DoneFunc { + return func(context.Context, selector.DoneInfo) {} +} +func (m *mockWeightedNode) PickElapsed() time.Duration { return 0 } +func (m *mockWeightedNode) Scheme() string { return "http" } +func (m *mockWeightedNode) ServiceName() string { return "test" } +func (m *mockWeightedNode) InitialWeight() *int64 { return nil } +func (m *mockWeightedNode) Version() string { return "v1.0.0" } +func (m *mockWeightedNode) Metadata() map[string]string { return nil } + +func TestEmpty(t *testing.T) { + b := &Balancer{} + _, _, err := b.Pick(context.Background(), []selector.WeightedNode{}) + if err == nil { + t.Errorf("expect no error, got %v", err) + } +} + +// BenchmarkPickWithSameNodes benchmarks Pick() calls with the same node set +// This demonstrates the performance improvement where cleanup only happens on node changes +func BenchmarkPickWithSameNodes(b *testing.B) { + balancer := &Balancer{currentWeight: make(map[string]float64)} + + // Create a fixed set of nodes + nodes := []selector.WeightedNode{ + &mockWeightedNode{address: "node1", weight: 10}, + &mockWeightedNode{address: "node2", weight: 20}, + &mockWeightedNode{address: "node3", weight: 30}, + &mockWeightedNode{address: "node4", weight: 40}, + &mockWeightedNode{address: "node5", weight: 50}, + } + + ctx := context.Background() + + b.ResetTimer() + b.ReportAllocs() + + // Benchmark Pick() calls with the same nodes + // After the first call, no cleanup should occur on subsequent calls + for i := 0; i < b.N; i++ { + _, _, err := balancer.Pick(ctx, nodes) + if err != nil { + b.Fatalf("unexpected error: %v", err) + } + } +} + +// BenchmarkPickWithChangingNodes benchmarks Pick() calls with changing node sets +// This shows the overhead when nodes actually change (expected to be slower) +func BenchmarkPickWithChangingNodes(b *testing.B) { + balancer := &Balancer{currentWeight: make(map[string]float64)} + + // Create alternating sets of nodes to simulate node changes + nodes1 := []selector.WeightedNode{ + &mockWeightedNode{address: "node1", weight: 10}, + &mockWeightedNode{address: "node2", weight: 20}, + } + + nodes2 := []selector.WeightedNode{ + &mockWeightedNode{address: "node3", weight: 30}, + &mockWeightedNode{address: "node4", weight: 40}, + } + + ctx := context.Background() + + b.ResetTimer() + b.ReportAllocs() + + // Benchmark Pick() calls with alternating node sets + // This will trigger cleanup on every call + for i := 0; i < b.N; i++ { + var nodes []selector.WeightedNode + if i%2 == 0 { + nodes = nodes1 + } else { + nodes = nodes2 + } + + _, _, err := balancer.Pick(ctx, nodes) + if err != nil { + b.Fatalf("unexpected error: %v", err) + } + } +} diff --git a/third_party/README.md b/third_party/README.md new file mode 100644 index 0000000..005faa2 --- /dev/null +++ b/third_party/README.md @@ -0,0 +1 @@ +# third_party diff --git a/third_party/buf.yaml b/third_party/buf.yaml new file mode 100644 index 0000000..3af932d --- /dev/null +++ b/third_party/buf.yaml @@ -0,0 +1,12 @@ +version: v1 +name: buf.build/kratos/apis +breaking: + use: + - FILE +lint: + use: + - DEFAULT +build: + excludes: + - google + - validate diff --git a/third_party/buf/validate/README.md b/third_party/buf/validate/README.md new file mode 100644 index 0000000..3657eba --- /dev/null +++ b/third_party/buf/validate/README.md @@ -0,0 +1,3 @@ +# protovalidate + +* https://github.com/bufbuild/protovalidate \ No newline at end of file diff --git a/third_party/buf/validate/validate.proto b/third_party/buf/validate/validate.proto new file mode 100644 index 0000000..c6914f6 --- /dev/null +++ b/third_party/buf/validate/validate.proto @@ -0,0 +1,4902 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package buf.validate; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"; +option java_multiple_files = true; +option java_outer_classname = "ValidateProto"; +option java_package = "build.buf.validate"; + +// MessageOptions is an extension to google.protobuf.MessageOptions. It allows +// the addition of validation rules at the message level. These rules can be +// applied to incoming messages to ensure they meet certain criteria before +// being processed. +extend google.protobuf.MessageOptions { + // Rules specify the validations to be performed on this message. By default, + // no validation is performed against a message. + optional MessageConstraints message = 1159; +} + +// OneofOptions is an extension to google.protobuf.OneofOptions. It allows +// the addition of validation rules on a oneof. These rules can be +// applied to incoming messages to ensure they meet certain criteria before +// being processed. +extend google.protobuf.OneofOptions { + // Rules specify the validations to be performed on this oneof. By default, + // no validation is performed against a oneof. + optional OneofConstraints oneof = 1159; +} + +// FieldOptions is an extension to google.protobuf.FieldOptions. It allows +// the addition of validation rules at the field level. These rules can be +// applied to incoming messages to ensure they meet certain criteria before +// being processed. +extend google.protobuf.FieldOptions { + // Rules specify the validations to be performed on this field. By default, + // no validation is performed against a field. + optional FieldConstraints field = 1159; + + // Specifies predefined rules. When extending a standard constraint message, + // this adds additional CEL expressions that apply when the extension is used. + // + // ```proto + // extend buf.validate.Int32Rules { + // bool is_zero [(buf.validate.predefined).cel = { + // id: "int32.is_zero", + // message: "value must be zero", + // expression: "!rule || this == 0", + // }]; + // } + // + // message Foo { + // int32 reserved = 1 [(buf.validate.field).int32.(is_zero) = true]; + // } + // ``` + optional PredefinedConstraints predefined = 1160; +} + +// `Constraint` represents a validation rule written in the Common Expression +// Language (CEL) syntax. Each Constraint includes a unique identifier, an +// optional error message, and the CEL expression to evaluate. For more +// information on CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md). +// +// ```proto +// message Foo { +// option (buf.validate.message).cel = { +// id: "foo.bar" +// message: "bar must be greater than 0" +// expression: "this.bar > 0" +// }; +// int32 bar = 1; +// } +// ``` +message Constraint { + // `id` is a string that serves as a machine-readable name for this Constraint. + // It should be unique within its scope, which could be either a message or a field. + optional string id = 1; + + // `message` is an optional field that provides a human-readable error message + // for this Constraint when the CEL expression evaluates to false. If a + // non-empty message is provided, any strings resulting from the CEL + // expression evaluation are ignored. + optional string message = 2; + + // `expression` is the actual CEL expression that will be evaluated for + // validation. This string must resolve to either a boolean or a string + // value. If the expression evaluates to false or a non-empty string, the + // validation is considered failed, and the message is rejected. + optional string expression = 3; +} + +// MessageConstraints represents validation rules that are applied to the entire message. +// It includes disabling options and a list of Constraint messages representing Common Expression Language (CEL) validation rules. +message MessageConstraints { + // `disabled` is a boolean flag that, when set to true, nullifies any validation rules for this message. + // This includes any fields within the message that would otherwise support validation. + // + // ```proto + // message MyMessage { + // // validation will be bypassed for this message + // option (buf.validate.message).disabled = true; + // } + // ``` + optional bool disabled = 1; + + // `cel` is a repeated field of type Constraint. Each Constraint specifies a validation rule to be applied to this message. + // These constraints are written in Common Expression Language (CEL) syntax. For more information on + // CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md). + // + // + // ```proto + // message MyMessage { + // // The field `foo` must be greater than 42. + // option (buf.validate.message).cel = { + // id: "my_message.value", + // message: "value must be greater than 42", + // expression: "this.foo > 42", + // }; + // optional int32 foo = 1; + // } + // ``` + repeated Constraint cel = 3; +} + +// The `OneofConstraints` message type enables you to manage constraints for +// oneof fields in your protobuf messages. +message OneofConstraints { + // If `required` is true, exactly one field of the oneof must be present. A + // validation error is returned if no fields in the oneof are present. The + // field itself may still be a default value; further constraints + // should be placed on the fields themselves to ensure they are valid values, + // such as `min_len` or `gt`. + // + // ```proto + // message MyMessage { + // oneof value { + // // Either `a` or `b` must be set. If `a` is set, it must also be + // // non-empty; whereas if `b` is set, it can still be an empty string. + // option (buf.validate.oneof).required = true; + // string a = 1 [(buf.validate.field).string.min_len = 1]; + // string b = 2; + // } + // } + // ``` + optional bool required = 1; +} + +// FieldConstraints encapsulates the rules for each type of field. Depending on +// the field, the correct set should be used to ensure proper validations. +message FieldConstraints { + // `cel` is a repeated field used to represent a textual expression + // in the Common Expression Language (CEL) syntax. For more information on + // CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md). + // + // ```proto + // message MyMessage { + // // The field `value` must be greater than 42. + // optional int32 value = 1 [(buf.validate.field).cel = { + // id: "my_message.value", + // message: "value must be greater than 42", + // expression: "this > 42", + // }]; + // } + // ``` + repeated Constraint cel = 23; + // If `required` is true, the field must be populated. A populated field can be + // described as "serialized in the wire format," which includes: + // + // - the following "nullable" fields must be explicitly set to be considered populated: + // - singular message fields (whose fields may be unpopulated/default values) + // - member fields of a oneof (may be their default value) + // - proto3 optional fields (may be their default value) + // - proto2 scalar fields (both optional and required) + // - proto3 scalar fields must be non-zero to be considered populated + // - repeated and map fields must be non-empty to be considered populated + // + // ```proto + // message MyMessage { + // // The field `value` must be set to a non-null value. + // optional MyOtherMessage value = 1 [(buf.validate.field).required = true]; + // } + // ``` + optional bool required = 25; + // Skip validation on the field if its value matches the specified criteria. + // See Ignore enum for details. + // + // ```proto + // message UpdateRequest { + // // The uri rule only applies if the field is populated and not an empty + // // string. + // optional string url = 1 [ + // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE, + // (buf.validate.field).string.uri = true, + // ]; + // } + // ``` + optional Ignore ignore = 27; + + oneof type { + // Scalar Field Types + FloatRules float = 1; + DoubleRules double = 2; + Int32Rules int32 = 3; + Int64Rules int64 = 4; + UInt32Rules uint32 = 5; + UInt64Rules uint64 = 6; + SInt32Rules sint32 = 7; + SInt64Rules sint64 = 8; + Fixed32Rules fixed32 = 9; + Fixed64Rules fixed64 = 10; + SFixed32Rules sfixed32 = 11; + SFixed64Rules sfixed64 = 12; + BoolRules bool = 13; + StringRules string = 14; + BytesRules bytes = 15; + + // Complex Field Types + EnumRules enum = 16; + RepeatedRules repeated = 18; + MapRules map = 19; + + // Well-Known Field Types + AnyRules any = 20; + DurationRules duration = 21; + TimestampRules timestamp = 22; + } + + // DEPRECATED: use ignore=IGNORE_ALWAYS instead. TODO: remove this field pre-v1. + optional bool skipped = 24 [deprecated = true]; + // DEPRECATED: use ignore=IGNORE_IF_UNPOPULATED instead. TODO: remove this field pre-v1. + optional bool ignore_empty = 26 [deprecated = true]; +} + +// PredefinedConstraints are custom constraints that can be re-used with +// multiple fields. +message PredefinedConstraints { + // `cel` is a repeated field used to represent a textual expression + // in the Common Expression Language (CEL) syntax. For more information on + // CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md). + // + // ```proto + // message MyMessage { + // // The field `value` must be greater than 42. + // optional int32 value = 1 [(buf.validate.predefined).cel = { + // id: "my_message.value", + // message: "value must be greater than 42", + // expression: "this > 42", + // }]; + // } + // ``` + repeated Constraint cel = 1; +} + +// Specifies how FieldConstraints.ignore behaves. See the documentation for +// FieldConstraints.required for definitions of "populated" and "nullable". +enum Ignore { + // buf:lint:ignore ENUM_NO_ALLOW_ALIAS // allowance for deprecations. TODO: remove pre-v1. + option allow_alias = true; + // Validation is only skipped if it's an unpopulated nullable fields. + // + // ```proto + // syntax="proto3"; + // + // message Request { + // // The uri rule applies to any value, including the empty string. + // string foo = 1 [ + // (buf.validate.field).string.uri = true + // ]; + // + // // The uri rule only applies if the field is set, including if it's + // // set to the empty string. + // optional string bar = 2 [ + // (buf.validate.field).string.uri = true + // ]; + // + // // The min_items rule always applies, even if the list is empty. + // repeated string baz = 3 [ + // (buf.validate.field).repeated.min_items = 3 + // ]; + // + // // The custom CEL rule applies only if the field is set, including if + // // it's the "zero" value of that message. + // SomeMessage quux = 4 [ + // (buf.validate.field).cel = {/* ... */} + // ]; + // } + // ``` + IGNORE_UNSPECIFIED = 0; + + // Validation is skipped if the field is unpopulated. This rule is redundant + // if the field is already nullable. This value is equivalent behavior to the + // deprecated ignore_empty rule. + // + // ```proto + // syntax="proto3 + // + // message Request { + // // The uri rule applies only if the value is not the empty string. + // string foo = 1 [ + // (buf.validate.field).string.uri = true, + // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED + // ]; + // + // // IGNORE_IF_UNPOPULATED is equivalent to IGNORE_UNSPECIFIED in this + // // case: the uri rule only applies if the field is set, including if + // // it's set to the empty string. + // optional string bar = 2 [ + // (buf.validate.field).string.uri = true, + // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED + // ]; + // + // // The min_items rule only applies if the list has at least one item. + // repeated string baz = 3 [ + // (buf.validate.field).repeated.min_items = 3, + // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED + // ]; + // + // // IGNORE_IF_UNPOPULATED is equivalent to IGNORE_UNSPECIFIED in this + // // case: the custom CEL rule applies only if the field is set, including + // // if it's the "zero" value of that message. + // SomeMessage quux = 4 [ + // (buf.validate.field).cel = {/* ... */}, + // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED + // ]; + // } + // ``` + IGNORE_IF_UNPOPULATED = 1; + + // Validation is skipped if the field is unpopulated or if it is a nullable + // field populated with its default value. This is typically the zero or + // empty value, but proto2 scalars support custom defaults. For messages, the + // default is a non-null message with all its fields unpopulated. + // + // ```proto + // syntax="proto3 + // + // message Request { + // // IGNORE_IF_DEFAULT_VALUE is equivalent to IGNORE_IF_UNPOPULATED in + // // this case; the uri rule applies only if the value is not the empty + // // string. + // string foo = 1 [ + // (buf.validate.field).string.uri = true, + // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE + // ]; + // + // // The uri rule only applies if the field is set to a value other than + // // the empty string. + // optional string bar = 2 [ + // (buf.validate.field).string.uri = true, + // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE + // ]; + // + // // IGNORE_IF_DEFAULT_VALUE is equivalent to IGNORE_IF_UNPOPULATED in + // // this case; the min_items rule only applies if the list has at least + // // one item. + // repeated string baz = 3 [ + // (buf.validate.field).repeated.min_items = 3, + // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE + // ]; + // + // // The custom CEL rule only applies if the field is set to a value other + // // than an empty message (i.e., fields are unpopulated). + // SomeMessage quux = 4 [ + // (buf.validate.field).cel = {/* ... */}, + // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE + // ]; + // } + // ``` + // + // This rule is affected by proto2 custom default values: + // + // ```proto + // syntax="proto2"; + // + // message Request { + // // The gt rule only applies if the field is set and it's value is not + // the default (i.e., not -42). The rule even applies if the field is set + // to zero since the default value differs. + // optional int32 value = 1 [ + // default = -42, + // (buf.validate.field).int32.gt = 0, + // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE + // ]; + // } + IGNORE_IF_DEFAULT_VALUE = 2; + + // The validation rules of this field will be skipped and not evaluated. This + // is useful for situations that necessitate turning off the rules of a field + // containing a message that may not make sense in the current context, or to + // temporarily disable constraints during development. + // + // ```proto + // message MyMessage { + // // The field's rules will always be ignored, including any validation's + // // on value's fields. + // MyOtherMessage value = 1 [ + // (buf.validate.field).ignore = IGNORE_ALWAYS]; + // } + // ``` + IGNORE_ALWAYS = 3; + + // Deprecated: Use IGNORE_IF_UNPOPULATED instead. TODO: Remove this value pre-v1. + IGNORE_EMPTY = 1 [deprecated = true]; + // Deprecated: Use IGNORE_IF_DEFAULT_VALUE. TODO: Remove this value pre-v1. + IGNORE_DEFAULT = 2 [deprecated = true]; +} + +// FloatRules describes the constraints applied to `float` values. These +// rules may also be applied to the `google.protobuf.FloatValue` Well-Known-Type. +message FloatRules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyFloat { + // // value must equal 42.0 + // float value = 1 [(buf.validate.field).float.const = 42.0]; + // } + // ``` + optional float const = 1 [(predefined).cel = { + id: "float.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MyFloat { + // // value must be less than 10.0 + // float value = 1 [(buf.validate.field).float.lt = 10.0]; + // } + // ``` + float lt = 2 [(predefined).cel = { + id: "float.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this >= rules.lt)" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyFloat { + // // value must be less than or equal to 10.0 + // float value = 1 [(buf.validate.field).float.lte = 10.0]; + // } + // ``` + float lte = 3 [(predefined).cel = { + id: "float.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this > rules.lte)" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyFloat { + // // value must be greater than 5.0 [float.gt] + // float value = 1 [(buf.validate.field).float.gt = 5.0]; + // + // // value must be greater than 5 and less than 10.0 [float.gt_lt] + // float other_value = 2 [(buf.validate.field).float = { gt: 5.0, lt: 10.0 }]; + // + // // value must be greater than 10 or less than 5.0 [float.gt_lt_exclusive] + // float another_value = 3 [(buf.validate.field).float = { gt: 10.0, lt: 5.0 }]; + // } + // ``` + float gt = 4 [ + (predefined).cel = { + id: "float.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this <= rules.gt)" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "float.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this.isNan() || this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "float.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (this.isNan() || (rules.lt <= this && this <= rules.gt))" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "float.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this.isNan() || this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "float.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (this.isNan() || (rules.lte < this && this <= rules.gt))" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyFloat { + // // value must be greater than or equal to 5.0 [float.gte] + // float value = 1 [(buf.validate.field).float.gte = 5.0]; + // + // // value must be greater than or equal to 5.0 and less than 10.0 [float.gte_lt] + // float other_value = 2 [(buf.validate.field).float = { gte: 5.0, lt: 10.0 }]; + // + // // value must be greater than or equal to 10.0 or less than 5.0 [float.gte_lt_exclusive] + // float another_value = 3 [(buf.validate.field).float = { gte: 10.0, lt: 5.0 }]; + // } + // ``` + float gte = 5 [ + (predefined).cel = { + id: "float.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this < rules.gte)" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "float.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this.isNan() || this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "float.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (this.isNan() || (rules.lt <= this && this < rules.gte))" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "float.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this.isNan() || this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "float.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (this.isNan() || (rules.lte < this && this < rules.gte))" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message + // is generated. + // + // ```proto + // message MyFloat { + // // value must be in list [1.0, 2.0, 3.0] + // repeated float value = 1 (buf.validate.field).float = { in: [1.0, 2.0, 3.0] }; + // } + // ``` + repeated float in = 6 [(predefined).cel = { + id: "float.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MyFloat { + // // value must not be in list [1.0, 2.0, 3.0] + // repeated float value = 1 (buf.validate.field).float = { not_in: [1.0, 2.0, 3.0] }; + // } + // ``` + repeated float not_in = 7 [(predefined).cel = { + id: "float.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `finite` requires the field value to be finite. If the field value is + // infinite or NaN, an error message is generated. + optional bool finite = 8 [(predefined).cel = { + id: "float.finite" + expression: "rules.finite ? (this.isNan() || this.isInf() ? 'value must be finite' : '') : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyFloat { + // float value = 1 [ + // (buf.validate.field).float.example = 1.0, + // (buf.validate.field).float.example = "Infinity" + // ]; + // } + // ``` + repeated float example = 9 [(predefined).cel = { + id: "float.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// DoubleRules describes the constraints applied to `double` values. These +// rules may also be applied to the `google.protobuf.DoubleValue` Well-Known-Type. +message DoubleRules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyDouble { + // // value must equal 42.0 + // double value = 1 [(buf.validate.field).double.const = 42.0]; + // } + // ``` + optional double const = 1 [(predefined).cel = { + id: "double.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyDouble { + // // value must be less than 10.0 + // double value = 1 [(buf.validate.field).double.lt = 10.0]; + // } + // ``` + double lt = 2 [(predefined).cel = { + id: "double.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this >= rules.lt)" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified value + // (field <= value). If the field value is greater than the specified value, + // an error message is generated. + // + // ```proto + // message MyDouble { + // // value must be less than or equal to 10.0 + // double value = 1 [(buf.validate.field).double.lte = 10.0]; + // } + // ``` + double lte = 3 [(predefined).cel = { + id: "double.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this > rules.lte)" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or `lte`, + // the range is reversed, and the field value must be outside the specified + // range. If the field value doesn't meet the required conditions, an error + // message is generated. + // + // ```proto + // message MyDouble { + // // value must be greater than 5.0 [double.gt] + // double value = 1 [(buf.validate.field).double.gt = 5.0]; + // + // // value must be greater than 5 and less than 10.0 [double.gt_lt] + // double other_value = 2 [(buf.validate.field).double = { gt: 5.0, lt: 10.0 }]; + // + // // value must be greater than 10 or less than 5.0 [double.gt_lt_exclusive] + // double another_value = 3 [(buf.validate.field).double = { gt: 10.0, lt: 5.0 }]; + // } + // ``` + double gt = 4 [ + (predefined).cel = { + id: "double.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this <= rules.gt)" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "double.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this.isNan() || this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "double.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (this.isNan() || (rules.lt <= this && this <= rules.gt))" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "double.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this.isNan() || this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "double.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (this.isNan() || (rules.lte < this && this <= rules.gt))" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyDouble { + // // value must be greater than or equal to 5.0 [double.gte] + // double value = 1 [(buf.validate.field).double.gte = 5.0]; + // + // // value must be greater than or equal to 5.0 and less than 10.0 [double.gte_lt] + // double other_value = 2 [(buf.validate.field).double = { gte: 5.0, lt: 10.0 }]; + // + // // value must be greater than or equal to 10.0 or less than 5.0 [double.gte_lt_exclusive] + // double another_value = 3 [(buf.validate.field).double = { gte: 10.0, lt: 5.0 }]; + // } + // ``` + double gte = 5 [ + (predefined).cel = { + id: "double.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this < rules.gte)" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "double.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this.isNan() || this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "double.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (this.isNan() || (rules.lt <= this && this < rules.gte))" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "double.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this.isNan() || this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "double.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (this.isNan() || (rules.lte < this && this < rules.gte))" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MyDouble { + // // value must be in list [1.0, 2.0, 3.0] + // repeated double value = 1 (buf.validate.field).double = { in: [1.0, 2.0, 3.0] }; + // } + // ``` + repeated double in = 6 [(predefined).cel = { + id: "double.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MyDouble { + // // value must not be in list [1.0, 2.0, 3.0] + // repeated double value = 1 (buf.validate.field).double = { not_in: [1.0, 2.0, 3.0] }; + // } + // ``` + repeated double not_in = 7 [(predefined).cel = { + id: "double.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `finite` requires the field value to be finite. If the field value is + // infinite or NaN, an error message is generated. + optional bool finite = 8 [(predefined).cel = { + id: "double.finite" + expression: "rules.finite ? (this.isNan() || this.isInf() ? 'value must be finite' : '') : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyDouble { + // double value = 1 [ + // (buf.validate.field).double.example = 1.0, + // (buf.validate.field).double.example = "Infinity" + // ]; + // } + // ``` + repeated double example = 9 [(predefined).cel = { + id: "double.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// Int32Rules describes the constraints applied to `int32` values. These +// rules may also be applied to the `google.protobuf.Int32Value` Well-Known-Type. +message Int32Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyInt32 { + // // value must equal 42 + // int32 value = 1 [(buf.validate.field).int32.const = 42]; + // } + // ``` + optional int32 const = 1 [(predefined).cel = { + id: "int32.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field + // < value). If the field value is equal to or greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyInt32 { + // // value must be less than 10 + // int32 value = 1 [(buf.validate.field).int32.lt = 10]; + // } + // ``` + int32 lt = 2 [(predefined).cel = { + id: "int32.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyInt32 { + // // value must be less than or equal to 10 + // int32 value = 1 [(buf.validate.field).int32.lte = 10]; + // } + // ``` + int32 lte = 3 [(predefined).cel = { + id: "int32.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyInt32 { + // // value must be greater than 5 [int32.gt] + // int32 value = 1 [(buf.validate.field).int32.gt = 5]; + // + // // value must be greater than 5 and less than 10 [int32.gt_lt] + // int32 other_value = 2 [(buf.validate.field).int32 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [int32.gt_lt_exclusive] + // int32 another_value = 3 [(buf.validate.field).int32 = { gt: 10, lt: 5 }]; + // } + // ``` + int32 gt = 4 [ + (predefined).cel = { + id: "int32.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "int32.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int32.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int32.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "int32.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified value + // (exclusive). If the value of `gte` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyInt32 { + // // value must be greater than or equal to 5 [int32.gte] + // int32 value = 1 [(buf.validate.field).int32.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [int32.gte_lt] + // int32 other_value = 2 [(buf.validate.field).int32 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [int32.gte_lt_exclusive] + // int32 another_value = 3 [(buf.validate.field).int32 = { gte: 10, lt: 5 }]; + // } + // ``` + int32 gte = 5 [ + (predefined).cel = { + id: "int32.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "int32.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int32.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int32.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "int32.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MyInt32 { + // // value must be in list [1, 2, 3] + // repeated int32 value = 1 (buf.validate.field).int32 = { in: [1, 2, 3] }; + // } + // ``` + repeated int32 in = 6 [(predefined).cel = { + id: "int32.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error message + // is generated. + // + // ```proto + // message MyInt32 { + // // value must not be in list [1, 2, 3] + // repeated int32 value = 1 (buf.validate.field).int32 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated int32 not_in = 7 [(predefined).cel = { + id: "int32.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyInt32 { + // int32 value = 1 [ + // (buf.validate.field).int32.example = 1, + // (buf.validate.field).int32.example = -10 + // ]; + // } + // ``` + repeated int32 example = 8 [(predefined).cel = { + id: "int32.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// Int64Rules describes the constraints applied to `int64` values. These +// rules may also be applied to the `google.protobuf.Int64Value` Well-Known-Type. +message Int64Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyInt64 { + // // value must equal 42 + // int64 value = 1 [(buf.validate.field).int64.const = 42]; + // } + // ``` + optional int64 const = 1 [(predefined).cel = { + id: "int64.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MyInt64 { + // // value must be less than 10 + // int64 value = 1 [(buf.validate.field).int64.lt = 10]; + // } + // ``` + int64 lt = 2 [(predefined).cel = { + id: "int64.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyInt64 { + // // value must be less than or equal to 10 + // int64 value = 1 [(buf.validate.field).int64.lte = 10]; + // } + // ``` + int64 lte = 3 [(predefined).cel = { + id: "int64.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyInt64 { + // // value must be greater than 5 [int64.gt] + // int64 value = 1 [(buf.validate.field).int64.gt = 5]; + // + // // value must be greater than 5 and less than 10 [int64.gt_lt] + // int64 other_value = 2 [(buf.validate.field).int64 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [int64.gt_lt_exclusive] + // int64 another_value = 3 [(buf.validate.field).int64 = { gt: 10, lt: 5 }]; + // } + // ``` + int64 gt = 4 [ + (predefined).cel = { + id: "int64.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "int64.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int64.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int64.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "int64.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyInt64 { + // // value must be greater than or equal to 5 [int64.gte] + // int64 value = 1 [(buf.validate.field).int64.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [int64.gte_lt] + // int64 other_value = 2 [(buf.validate.field).int64 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [int64.gte_lt_exclusive] + // int64 another_value = 3 [(buf.validate.field).int64 = { gte: 10, lt: 5 }]; + // } + // ``` + int64 gte = 5 [ + (predefined).cel = { + id: "int64.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "int64.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int64.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "int64.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "int64.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MyInt64 { + // // value must be in list [1, 2, 3] + // repeated int64 value = 1 (buf.validate.field).int64 = { in: [1, 2, 3] }; + // } + // ``` + repeated int64 in = 6 [(predefined).cel = { + id: "int64.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MyInt64 { + // // value must not be in list [1, 2, 3] + // repeated int64 value = 1 (buf.validate.field).int64 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated int64 not_in = 7 [(predefined).cel = { + id: "int64.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyInt64 { + // int64 value = 1 [ + // (buf.validate.field).int64.example = 1, + // (buf.validate.field).int64.example = -10 + // ]; + // } + // ``` + repeated int64 example = 9 [(predefined).cel = { + id: "int64.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// UInt32Rules describes the constraints applied to `uint32` values. These +// rules may also be applied to the `google.protobuf.UInt32Value` Well-Known-Type. +message UInt32Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyUInt32 { + // // value must equal 42 + // uint32 value = 1 [(buf.validate.field).uint32.const = 42]; + // } + // ``` + optional uint32 const = 1 [(predefined).cel = { + id: "uint32.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MyUInt32 { + // // value must be less than 10 + // uint32 value = 1 [(buf.validate.field).uint32.lt = 10]; + // } + // ``` + uint32 lt = 2 [(predefined).cel = { + id: "uint32.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyUInt32 { + // // value must be less than or equal to 10 + // uint32 value = 1 [(buf.validate.field).uint32.lte = 10]; + // } + // ``` + uint32 lte = 3 [(predefined).cel = { + id: "uint32.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyUInt32 { + // // value must be greater than 5 [uint32.gt] + // uint32 value = 1 [(buf.validate.field).uint32.gt = 5]; + // + // // value must be greater than 5 and less than 10 [uint32.gt_lt] + // uint32 other_value = 2 [(buf.validate.field).uint32 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [uint32.gt_lt_exclusive] + // uint32 another_value = 3 [(buf.validate.field).uint32 = { gt: 10, lt: 5 }]; + // } + // ``` + uint32 gt = 4 [ + (predefined).cel = { + id: "uint32.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "uint32.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint32.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint32.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "uint32.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyUInt32 { + // // value must be greater than or equal to 5 [uint32.gte] + // uint32 value = 1 [(buf.validate.field).uint32.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [uint32.gte_lt] + // uint32 other_value = 2 [(buf.validate.field).uint32 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [uint32.gte_lt_exclusive] + // uint32 another_value = 3 [(buf.validate.field).uint32 = { gte: 10, lt: 5 }]; + // } + // ``` + uint32 gte = 5 [ + (predefined).cel = { + id: "uint32.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "uint32.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint32.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint32.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "uint32.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MyUInt32 { + // // value must be in list [1, 2, 3] + // repeated uint32 value = 1 (buf.validate.field).uint32 = { in: [1, 2, 3] }; + // } + // ``` + repeated uint32 in = 6 [(predefined).cel = { + id: "uint32.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MyUInt32 { + // // value must not be in list [1, 2, 3] + // repeated uint32 value = 1 (buf.validate.field).uint32 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated uint32 not_in = 7 [(predefined).cel = { + id: "uint32.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyUInt32 { + // uint32 value = 1 [ + // (buf.validate.field).uint32.example = 1, + // (buf.validate.field).uint32.example = 10 + // ]; + // } + // ``` + repeated uint32 example = 8 [(predefined).cel = { + id: "uint32.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// UInt64Rules describes the constraints applied to `uint64` values. These +// rules may also be applied to the `google.protobuf.UInt64Value` Well-Known-Type. +message UInt64Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyUInt64 { + // // value must equal 42 + // uint64 value = 1 [(buf.validate.field).uint64.const = 42]; + // } + // ``` + optional uint64 const = 1 [(predefined).cel = { + id: "uint64.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MyUInt64 { + // // value must be less than 10 + // uint64 value = 1 [(buf.validate.field).uint64.lt = 10]; + // } + // ``` + uint64 lt = 2 [(predefined).cel = { + id: "uint64.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyUInt64 { + // // value must be less than or equal to 10 + // uint64 value = 1 [(buf.validate.field).uint64.lte = 10]; + // } + // ``` + uint64 lte = 3 [(predefined).cel = { + id: "uint64.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyUInt64 { + // // value must be greater than 5 [uint64.gt] + // uint64 value = 1 [(buf.validate.field).uint64.gt = 5]; + // + // // value must be greater than 5 and less than 10 [uint64.gt_lt] + // uint64 other_value = 2 [(buf.validate.field).uint64 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [uint64.gt_lt_exclusive] + // uint64 another_value = 3 [(buf.validate.field).uint64 = { gt: 10, lt: 5 }]; + // } + // ``` + uint64 gt = 4 [ + (predefined).cel = { + id: "uint64.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "uint64.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint64.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint64.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "uint64.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyUInt64 { + // // value must be greater than or equal to 5 [uint64.gte] + // uint64 value = 1 [(buf.validate.field).uint64.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [uint64.gte_lt] + // uint64 other_value = 2 [(buf.validate.field).uint64 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [uint64.gte_lt_exclusive] + // uint64 another_value = 3 [(buf.validate.field).uint64 = { gte: 10, lt: 5 }]; + // } + // ``` + uint64 gte = 5 [ + (predefined).cel = { + id: "uint64.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "uint64.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint64.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "uint64.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "uint64.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MyUInt64 { + // // value must be in list [1, 2, 3] + // repeated uint64 value = 1 (buf.validate.field).uint64 = { in: [1, 2, 3] }; + // } + // ``` + repeated uint64 in = 6 [(predefined).cel = { + id: "uint64.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MyUInt64 { + // // value must not be in list [1, 2, 3] + // repeated uint64 value = 1 (buf.validate.field).uint64 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated uint64 not_in = 7 [(predefined).cel = { + id: "uint64.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyUInt64 { + // uint64 value = 1 [ + // (buf.validate.field).uint64.example = 1, + // (buf.validate.field).uint64.example = -10 + // ]; + // } + // ``` + repeated uint64 example = 8 [(predefined).cel = { + id: "uint64.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// SInt32Rules describes the constraints applied to `sint32` values. +message SInt32Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MySInt32 { + // // value must equal 42 + // sint32 value = 1 [(buf.validate.field).sint32.const = 42]; + // } + // ``` + optional sint32 const = 1 [(predefined).cel = { + id: "sint32.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field + // < value). If the field value is equal to or greater than the specified + // value, an error message is generated. + // + // ```proto + // message MySInt32 { + // // value must be less than 10 + // sint32 value = 1 [(buf.validate.field).sint32.lt = 10]; + // } + // ``` + sint32 lt = 2 [(predefined).cel = { + id: "sint32.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MySInt32 { + // // value must be less than or equal to 10 + // sint32 value = 1 [(buf.validate.field).sint32.lte = 10]; + // } + // ``` + sint32 lte = 3 [(predefined).cel = { + id: "sint32.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySInt32 { + // // value must be greater than 5 [sint32.gt] + // sint32 value = 1 [(buf.validate.field).sint32.gt = 5]; + // + // // value must be greater than 5 and less than 10 [sint32.gt_lt] + // sint32 other_value = 2 [(buf.validate.field).sint32 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [sint32.gt_lt_exclusive] + // sint32 another_value = 3 [(buf.validate.field).sint32 = { gt: 10, lt: 5 }]; + // } + // ``` + sint32 gt = 4 [ + (predefined).cel = { + id: "sint32.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "sint32.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint32.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint32.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sint32.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySInt32 { + // // value must be greater than or equal to 5 [sint32.gte] + // sint32 value = 1 [(buf.validate.field).sint32.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [sint32.gte_lt] + // sint32 other_value = 2 [(buf.validate.field).sint32 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [sint32.gte_lt_exclusive] + // sint32 another_value = 3 [(buf.validate.field).sint32 = { gte: 10, lt: 5 }]; + // } + // ``` + sint32 gte = 5 [ + (predefined).cel = { + id: "sint32.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "sint32.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint32.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint32.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sint32.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MySInt32 { + // // value must be in list [1, 2, 3] + // repeated sint32 value = 1 (buf.validate.field).sint32 = { in: [1, 2, 3] }; + // } + // ``` + repeated sint32 in = 6 [(predefined).cel = { + id: "sint32.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MySInt32 { + // // value must not be in list [1, 2, 3] + // repeated sint32 value = 1 (buf.validate.field).sint32 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated sint32 not_in = 7 [(predefined).cel = { + id: "sint32.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MySInt32 { + // sint32 value = 1 [ + // (buf.validate.field).sint32.example = 1, + // (buf.validate.field).sint32.example = -10 + // ]; + // } + // ``` + repeated sint32 example = 8 [(predefined).cel = { + id: "sint32.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// SInt64Rules describes the constraints applied to `sint64` values. +message SInt64Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MySInt64 { + // // value must equal 42 + // sint64 value = 1 [(buf.validate.field).sint64.const = 42]; + // } + // ``` + optional sint64 const = 1 [(predefined).cel = { + id: "sint64.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field + // < value). If the field value is equal to or greater than the specified + // value, an error message is generated. + // + // ```proto + // message MySInt64 { + // // value must be less than 10 + // sint64 value = 1 [(buf.validate.field).sint64.lt = 10]; + // } + // ``` + sint64 lt = 2 [(predefined).cel = { + id: "sint64.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MySInt64 { + // // value must be less than or equal to 10 + // sint64 value = 1 [(buf.validate.field).sint64.lte = 10]; + // } + // ``` + sint64 lte = 3 [(predefined).cel = { + id: "sint64.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySInt64 { + // // value must be greater than 5 [sint64.gt] + // sint64 value = 1 [(buf.validate.field).sint64.gt = 5]; + // + // // value must be greater than 5 and less than 10 [sint64.gt_lt] + // sint64 other_value = 2 [(buf.validate.field).sint64 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [sint64.gt_lt_exclusive] + // sint64 another_value = 3 [(buf.validate.field).sint64 = { gt: 10, lt: 5 }]; + // } + // ``` + sint64 gt = 4 [ + (predefined).cel = { + id: "sint64.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "sint64.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint64.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint64.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sint64.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySInt64 { + // // value must be greater than or equal to 5 [sint64.gte] + // sint64 value = 1 [(buf.validate.field).sint64.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [sint64.gte_lt] + // sint64 other_value = 2 [(buf.validate.field).sint64 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [sint64.gte_lt_exclusive] + // sint64 another_value = 3 [(buf.validate.field).sint64 = { gte: 10, lt: 5 }]; + // } + // ``` + sint64 gte = 5 [ + (predefined).cel = { + id: "sint64.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "sint64.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint64.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sint64.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sint64.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message + // is generated. + // + // ```proto + // message MySInt64 { + // // value must be in list [1, 2, 3] + // repeated sint64 value = 1 (buf.validate.field).sint64 = { in: [1, 2, 3] }; + // } + // ``` + repeated sint64 in = 6 [(predefined).cel = { + id: "sint64.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MySInt64 { + // // value must not be in list [1, 2, 3] + // repeated sint64 value = 1 (buf.validate.field).sint64 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated sint64 not_in = 7 [(predefined).cel = { + id: "sint64.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MySInt64 { + // sint64 value = 1 [ + // (buf.validate.field).sint64.example = 1, + // (buf.validate.field).sint64.example = -10 + // ]; + // } + // ``` + repeated sint64 example = 8 [(predefined).cel = { + id: "sint64.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// Fixed32Rules describes the constraints applied to `fixed32` values. +message Fixed32Rules { + // `const` requires the field value to exactly match the specified value. + // If the field value doesn't match, an error message is generated. + // + // ```proto + // message MyFixed32 { + // // value must equal 42 + // fixed32 value = 1 [(buf.validate.field).fixed32.const = 42]; + // } + // ``` + optional fixed32 const = 1 [(predefined).cel = { + id: "fixed32.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MyFixed32 { + // // value must be less than 10 + // fixed32 value = 1 [(buf.validate.field).fixed32.lt = 10]; + // } + // ``` + fixed32 lt = 2 [(predefined).cel = { + id: "fixed32.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyFixed32 { + // // value must be less than or equal to 10 + // fixed32 value = 1 [(buf.validate.field).fixed32.lte = 10]; + // } + // ``` + fixed32 lte = 3 [(predefined).cel = { + id: "fixed32.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyFixed32 { + // // value must be greater than 5 [fixed32.gt] + // fixed32 value = 1 [(buf.validate.field).fixed32.gt = 5]; + // + // // value must be greater than 5 and less than 10 [fixed32.gt_lt] + // fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [fixed32.gt_lt_exclusive] + // fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gt: 10, lt: 5 }]; + // } + // ``` + fixed32 gt = 4 [ + (predefined).cel = { + id: "fixed32.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "fixed32.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed32.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed32.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "fixed32.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyFixed32 { + // // value must be greater than or equal to 5 [fixed32.gte] + // fixed32 value = 1 [(buf.validate.field).fixed32.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [fixed32.gte_lt] + // fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [fixed32.gte_lt_exclusive] + // fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gte: 10, lt: 5 }]; + // } + // ``` + fixed32 gte = 5 [ + (predefined).cel = { + id: "fixed32.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "fixed32.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed32.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed32.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "fixed32.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message + // is generated. + // + // ```proto + // message MyFixed32 { + // // value must be in list [1, 2, 3] + // repeated fixed32 value = 1 (buf.validate.field).fixed32 = { in: [1, 2, 3] }; + // } + // ``` + repeated fixed32 in = 6 [(predefined).cel = { + id: "fixed32.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MyFixed32 { + // // value must not be in list [1, 2, 3] + // repeated fixed32 value = 1 (buf.validate.field).fixed32 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated fixed32 not_in = 7 [(predefined).cel = { + id: "fixed32.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyFixed32 { + // fixed32 value = 1 [ + // (buf.validate.field).fixed32.example = 1, + // (buf.validate.field).fixed32.example = 2 + // ]; + // } + // ``` + repeated fixed32 example = 8 [(predefined).cel = { + id: "fixed32.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// Fixed64Rules describes the constraints applied to `fixed64` values. +message Fixed64Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyFixed64 { + // // value must equal 42 + // fixed64 value = 1 [(buf.validate.field).fixed64.const = 42]; + // } + // ``` + optional fixed64 const = 1 [(predefined).cel = { + id: "fixed64.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MyFixed64 { + // // value must be less than 10 + // fixed64 value = 1 [(buf.validate.field).fixed64.lt = 10]; + // } + // ``` + fixed64 lt = 2 [(predefined).cel = { + id: "fixed64.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MyFixed64 { + // // value must be less than or equal to 10 + // fixed64 value = 1 [(buf.validate.field).fixed64.lte = 10]; + // } + // ``` + fixed64 lte = 3 [(predefined).cel = { + id: "fixed64.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyFixed64 { + // // value must be greater than 5 [fixed64.gt] + // fixed64 value = 1 [(buf.validate.field).fixed64.gt = 5]; + // + // // value must be greater than 5 and less than 10 [fixed64.gt_lt] + // fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [fixed64.gt_lt_exclusive] + // fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gt: 10, lt: 5 }]; + // } + // ``` + fixed64 gt = 4 [ + (predefined).cel = { + id: "fixed64.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "fixed64.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed64.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed64.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "fixed64.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyFixed64 { + // // value must be greater than or equal to 5 [fixed64.gte] + // fixed64 value = 1 [(buf.validate.field).fixed64.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [fixed64.gte_lt] + // fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [fixed64.gte_lt_exclusive] + // fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gte: 10, lt: 5 }]; + // } + // ``` + fixed64 gte = 5 [ + (predefined).cel = { + id: "fixed64.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "fixed64.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed64.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "fixed64.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "fixed64.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MyFixed64 { + // // value must be in list [1, 2, 3] + // repeated fixed64 value = 1 (buf.validate.field).fixed64 = { in: [1, 2, 3] }; + // } + // ``` + repeated fixed64 in = 6 [(predefined).cel = { + id: "fixed64.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MyFixed64 { + // // value must not be in list [1, 2, 3] + // repeated fixed64 value = 1 (buf.validate.field).fixed64 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated fixed64 not_in = 7 [(predefined).cel = { + id: "fixed64.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyFixed64 { + // fixed64 value = 1 [ + // (buf.validate.field).fixed64.example = 1, + // (buf.validate.field).fixed64.example = 2 + // ]; + // } + // ``` + repeated fixed64 example = 8 [(predefined).cel = { + id: "fixed64.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// SFixed32Rules describes the constraints applied to `fixed32` values. +message SFixed32Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MySFixed32 { + // // value must equal 42 + // sfixed32 value = 1 [(buf.validate.field).sfixed32.const = 42]; + // } + // ``` + optional sfixed32 const = 1 [(predefined).cel = { + id: "sfixed32.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MySFixed32 { + // // value must be less than 10 + // sfixed32 value = 1 [(buf.validate.field).sfixed32.lt = 10]; + // } + // ``` + sfixed32 lt = 2 [(predefined).cel = { + id: "sfixed32.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MySFixed32 { + // // value must be less than or equal to 10 + // sfixed32 value = 1 [(buf.validate.field).sfixed32.lte = 10]; + // } + // ``` + sfixed32 lte = 3 [(predefined).cel = { + id: "sfixed32.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySFixed32 { + // // value must be greater than 5 [sfixed32.gt] + // sfixed32 value = 1 [(buf.validate.field).sfixed32.gt = 5]; + // + // // value must be greater than 5 and less than 10 [sfixed32.gt_lt] + // sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [sfixed32.gt_lt_exclusive] + // sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gt: 10, lt: 5 }]; + // } + // ``` + sfixed32 gt = 4 [ + (predefined).cel = { + id: "sfixed32.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySFixed32 { + // // value must be greater than or equal to 5 [sfixed32.gte] + // sfixed32 value = 1 [(buf.validate.field).sfixed32.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [sfixed32.gte_lt] + // sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [sfixed32.gte_lt_exclusive] + // sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gte: 10, lt: 5 }]; + // } + // ``` + sfixed32 gte = 5 [ + (predefined).cel = { + id: "sfixed32.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sfixed32.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MySFixed32 { + // // value must be in list [1, 2, 3] + // repeated sfixed32 value = 1 (buf.validate.field).sfixed32 = { in: [1, 2, 3] }; + // } + // ``` + repeated sfixed32 in = 6 [(predefined).cel = { + id: "sfixed32.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MySFixed32 { + // // value must not be in list [1, 2, 3] + // repeated sfixed32 value = 1 (buf.validate.field).sfixed32 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated sfixed32 not_in = 7 [(predefined).cel = { + id: "sfixed32.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MySFixed32 { + // sfixed32 value = 1 [ + // (buf.validate.field).sfixed32.example = 1, + // (buf.validate.field).sfixed32.example = 2 + // ]; + // } + // ``` + repeated sfixed32 example = 8 [(predefined).cel = { + id: "sfixed32.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// SFixed64Rules describes the constraints applied to `fixed64` values. +message SFixed64Rules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MySFixed64 { + // // value must equal 42 + // sfixed64 value = 1 [(buf.validate.field).sfixed64.const = 42]; + // } + // ``` + optional sfixed64 const = 1 [(predefined).cel = { + id: "sfixed64.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` requires the field value to be less than the specified value (field < + // value). If the field value is equal to or greater than the specified value, + // an error message is generated. + // + // ```proto + // message MySFixed64 { + // // value must be less than 10 + // sfixed64 value = 1 [(buf.validate.field).sfixed64.lt = 10]; + // } + // ``` + sfixed64 lt = 2 [(predefined).cel = { + id: "sfixed64.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` requires the field value to be less than or equal to the specified + // value (field <= value). If the field value is greater than the specified + // value, an error message is generated. + // + // ```proto + // message MySFixed64 { + // // value must be less than or equal to 10 + // sfixed64 value = 1 [(buf.validate.field).sfixed64.lte = 10]; + // } + // ``` + sfixed64 lte = 3 [(predefined).cel = { + id: "sfixed64.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the field value to be greater than the specified value + // (exclusive). If the value of `gt` is larger than a specified `lt` or + // `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySFixed64 { + // // value must be greater than 5 [sfixed64.gt] + // sfixed64 value = 1 [(buf.validate.field).sfixed64.gt = 5]; + // + // // value must be greater than 5 and less than 10 [sfixed64.gt_lt] + // sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gt: 5, lt: 10 }]; + // + // // value must be greater than 10 or less than 5 [sfixed64.gt_lt_exclusive] + // sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gt: 10, lt: 5 }]; + // } + // ``` + sfixed64 gt = 4 [ + (predefined).cel = { + id: "sfixed64.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the field value to be greater than or equal to the specified + // value (exclusive). If the value of `gte` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MySFixed64 { + // // value must be greater than or equal to 5 [sfixed64.gte] + // sfixed64 value = 1 [(buf.validate.field).sfixed64.gte = 5]; + // + // // value must be greater than or equal to 5 and less than 10 [sfixed64.gte_lt] + // sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gte: 5, lt: 10 }]; + // + // // value must be greater than or equal to 10 or less than 5 [sfixed64.gte_lt_exclusive] + // sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gte: 10, lt: 5 }]; + // } + // ``` + sfixed64 gte = 5 [ + (predefined).cel = { + id: "sfixed64.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "sfixed64.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` requires the field value to be equal to one of the specified values. + // If the field value isn't one of the specified values, an error message is + // generated. + // + // ```proto + // message MySFixed64 { + // // value must be in list [1, 2, 3] + // repeated sfixed64 value = 1 (buf.validate.field).sfixed64 = { in: [1, 2, 3] }; + // } + // ``` + repeated sfixed64 in = 6 [(predefined).cel = { + id: "sfixed64.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to not be equal to any of the specified + // values. If the field value is one of the specified values, an error + // message is generated. + // + // ```proto + // message MySFixed64 { + // // value must not be in list [1, 2, 3] + // repeated sfixed64 value = 1 (buf.validate.field).sfixed64 = { not_in: [1, 2, 3] }; + // } + // ``` + repeated sfixed64 not_in = 7 [(predefined).cel = { + id: "sfixed64.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MySFixed64 { + // sfixed64 value = 1 [ + // (buf.validate.field).sfixed64.example = 1, + // (buf.validate.field).sfixed64.example = 2 + // ]; + // } + // ``` + repeated sfixed64 example = 8 [(predefined).cel = { + id: "sfixed64.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// BoolRules describes the constraints applied to `bool` values. These rules +// may also be applied to the `google.protobuf.BoolValue` Well-Known-Type. +message BoolRules { + // `const` requires the field value to exactly match the specified boolean value. + // If the field value doesn't match, an error message is generated. + // + // ```proto + // message MyBool { + // // value must equal true + // bool value = 1 [(buf.validate.field).bool.const = true]; + // } + // ``` + optional bool const = 1 [(predefined).cel = { + id: "bool.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyBool { + // bool value = 1 [ + // (buf.validate.field).bool.example = 1, + // (buf.validate.field).bool.example = 2 + // ]; + // } + // ``` + repeated bool example = 2 [(predefined).cel = { + id: "bool.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// StringRules describes the constraints applied to `string` values These +// rules may also be applied to the `google.protobuf.StringValue` Well-Known-Type. +message StringRules { + // `const` requires the field value to exactly match the specified value. If + // the field value doesn't match, an error message is generated. + // + // ```proto + // message MyString { + // // value must equal `hello` + // string value = 1 [(buf.validate.field).string.const = "hello"]; + // } + // ``` + optional string const = 1 [(predefined).cel = { + id: "string.const" + expression: "this != rules.const ? 'value must equal `%s`'.format([rules.const]) : ''" + }]; + + // `len` dictates that the field value must have the specified + // number of characters (Unicode code points), which may differ from the number + // of bytes in the string. If the field value does not meet the specified + // length, an error message will be generated. + // + // ```proto + // message MyString { + // // value length must be 5 characters + // string value = 1 [(buf.validate.field).string.len = 5]; + // } + // ``` + optional uint64 len = 19 [(predefined).cel = { + id: "string.len" + expression: "uint(this.size()) != rules.len ? 'value length must be %s characters'.format([rules.len]) : ''" + }]; + + // `min_len` specifies that the field value must have at least the specified + // number of characters (Unicode code points), which may differ from the number + // of bytes in the string. If the field value contains fewer characters, an error + // message will be generated. + // + // ```proto + // message MyString { + // // value length must be at least 3 characters + // string value = 1 [(buf.validate.field).string.min_len = 3]; + // } + // ``` + optional uint64 min_len = 2 [(predefined).cel = { + id: "string.min_len" + expression: "uint(this.size()) < rules.min_len ? 'value length must be at least %s characters'.format([rules.min_len]) : ''" + }]; + + // `max_len` specifies that the field value must have no more than the specified + // number of characters (Unicode code points), which may differ from the + // number of bytes in the string. If the field value contains more characters, + // an error message will be generated. + // + // ```proto + // message MyString { + // // value length must be at most 10 characters + // string value = 1 [(buf.validate.field).string.max_len = 10]; + // } + // ``` + optional uint64 max_len = 3 [(predefined).cel = { + id: "string.max_len" + expression: "uint(this.size()) > rules.max_len ? 'value length must be at most %s characters'.format([rules.max_len]) : ''" + }]; + + // `len_bytes` dictates that the field value must have the specified number of + // bytes. If the field value does not match the specified length in bytes, + // an error message will be generated. + // + // ```proto + // message MyString { + // // value length must be 6 bytes + // string value = 1 [(buf.validate.field).string.len_bytes = 6]; + // } + // ``` + optional uint64 len_bytes = 20 [(predefined).cel = { + id: "string.len_bytes" + expression: "uint(bytes(this).size()) != rules.len_bytes ? 'value length must be %s bytes'.format([rules.len_bytes]) : ''" + }]; + + // `min_bytes` specifies that the field value must have at least the specified + // number of bytes. If the field value contains fewer bytes, an error message + // will be generated. + // + // ```proto + // message MyString { + // // value length must be at least 4 bytes + // string value = 1 [(buf.validate.field).string.min_bytes = 4]; + // } + // + // ``` + optional uint64 min_bytes = 4 [(predefined).cel = { + id: "string.min_bytes" + expression: "uint(bytes(this).size()) < rules.min_bytes ? 'value length must be at least %s bytes'.format([rules.min_bytes]) : ''" + }]; + + // `max_bytes` specifies that the field value must have no more than the + //specified number of bytes. If the field value contains more bytes, an + // error message will be generated. + // + // ```proto + // message MyString { + // // value length must be at most 8 bytes + // string value = 1 [(buf.validate.field).string.max_bytes = 8]; + // } + // ``` + optional uint64 max_bytes = 5 [(predefined).cel = { + id: "string.max_bytes" + expression: "uint(bytes(this).size()) > rules.max_bytes ? 'value length must be at most %s bytes'.format([rules.max_bytes]) : ''" + }]; + + // `pattern` specifies that the field value must match the specified + // regular expression (RE2 syntax), with the expression provided without any + // delimiters. If the field value doesn't match the regular expression, an + // error message will be generated. + // + // ```proto + // message MyString { + // // value does not match regex pattern `^[a-zA-Z]//$` + // string value = 1 [(buf.validate.field).string.pattern = "^[a-zA-Z]//$"]; + // } + // ``` + optional string pattern = 6 [(predefined).cel = { + id: "string.pattern" + expression: "!this.matches(rules.pattern) ? 'value does not match regex pattern `%s`'.format([rules.pattern]) : ''" + }]; + + // `prefix` specifies that the field value must have the + //specified substring at the beginning of the string. If the field value + // doesn't start with the specified prefix, an error message will be + // generated. + // + // ```proto + // message MyString { + // // value does not have prefix `pre` + // string value = 1 [(buf.validate.field).string.prefix = "pre"]; + // } + // ``` + optional string prefix = 7 [(predefined).cel = { + id: "string.prefix" + expression: "!this.startsWith(rules.prefix) ? 'value does not have prefix `%s`'.format([rules.prefix]) : ''" + }]; + + // `suffix` specifies that the field value must have the + //specified substring at the end of the string. If the field value doesn't + // end with the specified suffix, an error message will be generated. + // + // ```proto + // message MyString { + // // value does not have suffix `post` + // string value = 1 [(buf.validate.field).string.suffix = "post"]; + // } + // ``` + optional string suffix = 8 [(predefined).cel = { + id: "string.suffix" + expression: "!this.endsWith(rules.suffix) ? 'value does not have suffix `%s`'.format([rules.suffix]) : ''" + }]; + + // `contains` specifies that the field value must have the + //specified substring anywhere in the string. If the field value doesn't + // contain the specified substring, an error message will be generated. + // + // ```proto + // message MyString { + // // value does not contain substring `inside`. + // string value = 1 [(buf.validate.field).string.contains = "inside"]; + // } + // ``` + optional string contains = 9 [(predefined).cel = { + id: "string.contains" + expression: "!this.contains(rules.contains) ? 'value does not contain substring `%s`'.format([rules.contains]) : ''" + }]; + + // `not_contains` specifies that the field value must not have the + //specified substring anywhere in the string. If the field value contains + // the specified substring, an error message will be generated. + // + // ```proto + // message MyString { + // // value contains substring `inside`. + // string value = 1 [(buf.validate.field).string.not_contains = "inside"]; + // } + // ``` + optional string not_contains = 23 [(predefined).cel = { + id: "string.not_contains" + expression: "this.contains(rules.not_contains) ? 'value contains substring `%s`'.format([rules.not_contains]) : ''" + }]; + + // `in` specifies that the field value must be equal to one of the specified + // values. If the field value isn't one of the specified values, an error + // message will be generated. + // + // ```proto + // message MyString { + // // value must be in list ["apple", "banana"] + // repeated string value = 1 [(buf.validate.field).string.in = "apple", (buf.validate.field).string.in = "banana"]; + // } + // ``` + repeated string in = 10 [(predefined).cel = { + id: "string.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` specifies that the field value cannot be equal to any + // of the specified values. If the field value is one of the specified values, + // an error message will be generated. + // ```proto + // message MyString { + // // value must not be in list ["orange", "grape"] + // repeated string value = 1 [(buf.validate.field).string.not_in = "orange", (buf.validate.field).string.not_in = "grape"]; + // } + // ``` + repeated string not_in = 11 [(predefined).cel = { + id: "string.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `WellKnown` rules provide advanced constraints against common string + // patterns + oneof well_known { + // `email` specifies that the field value must be a valid email address + // (addr-spec only) as defined by [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.4.1). + // If the field value isn't a valid email address, an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid email address + // string value = 1 [(buf.validate.field).string.email = true]; + // } + // ``` + bool email = 12 [ + (predefined).cel = { + id: "string.email" + message: "value must be a valid email address" + expression: "!rules.email || this == '' || this.isEmail()" + }, + (predefined).cel = { + id: "string.email_empty" + message: "value is empty, which is not a valid email address" + expression: "!rules.email || this != ''" + } + ]; + + // `hostname` specifies that the field value must be a valid + // hostname as defined by [RFC 1034](https://tools.ietf.org/html/rfc1034#section-3.5). This constraint doesn't support + // internationalized domain names (IDNs). If the field value isn't a + // valid hostname, an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid hostname + // string value = 1 [(buf.validate.field).string.hostname = true]; + // } + // ``` + bool hostname = 13 [ + (predefined).cel = { + id: "string.hostname" + message: "value must be a valid hostname" + expression: "!rules.hostname || this == '' || this.isHostname()" + }, + (predefined).cel = { + id: "string.hostname_empty" + message: "value is empty, which is not a valid hostname" + expression: "!rules.hostname || this != ''" + } + ]; + + // `ip` specifies that the field value must be a valid IP + // (v4 or v6) address, without surrounding square brackets for IPv6 addresses. + // If the field value isn't a valid IP address, an error message will be + // generated. + // + // ```proto + // message MyString { + // // value must be a valid IP address + // string value = 1 [(buf.validate.field).string.ip = true]; + // } + // ``` + bool ip = 14 [ + (predefined).cel = { + id: "string.ip" + message: "value must be a valid IP address" + expression: "!rules.ip || this == '' || this.isIp()" + }, + (predefined).cel = { + id: "string.ip_empty" + message: "value is empty, which is not a valid IP address" + expression: "!rules.ip || this != ''" + } + ]; + + // `ipv4` specifies that the field value must be a valid IPv4 + // address. If the field value isn't a valid IPv4 address, an error message + // will be generated. + // + // ```proto + // message MyString { + // // value must be a valid IPv4 address + // string value = 1 [(buf.validate.field).string.ipv4 = true]; + // } + // ``` + bool ipv4 = 15 [ + (predefined).cel = { + id: "string.ipv4" + message: "value must be a valid IPv4 address" + expression: "!rules.ipv4 || this == '' || this.isIp(4)" + }, + (predefined).cel = { + id: "string.ipv4_empty" + message: "value is empty, which is not a valid IPv4 address" + expression: "!rules.ipv4 || this != ''" + } + ]; + + // `ipv6` specifies that the field value must be a valid + // IPv6 address, without surrounding square brackets. If the field value is + // not a valid IPv6 address, an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid IPv6 address + // string value = 1 [(buf.validate.field).string.ipv6 = true]; + // } + // ``` + bool ipv6 = 16 [ + (predefined).cel = { + id: "string.ipv6" + message: "value must be a valid IPv6 address" + expression: "!rules.ipv6 || this == '' || this.isIp(6)" + }, + (predefined).cel = { + id: "string.ipv6_empty" + message: "value is empty, which is not a valid IPv6 address" + expression: "!rules.ipv6 || this != ''" + } + ]; + + // `uri` specifies that the field value must be a valid, + // absolute URI as defined by [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3). If the field value isn't a valid, + // absolute URI, an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid URI + // string value = 1 [(buf.validate.field).string.uri = true]; + // } + // ``` + bool uri = 17 [ + (predefined).cel = { + id: "string.uri" + message: "value must be a valid URI" + expression: "!rules.uri || this == '' || this.isUri()" + }, + (predefined).cel = { + id: "string.uri_empty" + message: "value is empty, which is not a valid URI" + expression: "!rules.uri || this != ''" + } + ]; + + // `uri_ref` specifies that the field value must be a valid URI + // as defined by [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3) and may be either relative or absolute. If the + // field value isn't a valid URI, an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid URI + // string value = 1 [(buf.validate.field).string.uri_ref = true]; + // } + // ``` + bool uri_ref = 18 [(predefined).cel = { + id: "string.uri_ref" + message: "value must be a valid URI" + expression: "!rules.uri_ref || this.isUriRef()" + }]; + + // `address` specifies that the field value must be either a valid hostname + // as defined by [RFC 1034](https://tools.ietf.org/html/rfc1034#section-3.5) + // (which doesn't support internationalized domain names or IDNs) or a valid + // IP (v4 or v6). If the field value isn't a valid hostname or IP, an error + // message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid hostname, or ip address + // string value = 1 [(buf.validate.field).string.address = true]; + // } + // ``` + bool address = 21 [ + (predefined).cel = { + id: "string.address" + message: "value must be a valid hostname, or ip address" + expression: "!rules.address || this == '' || this.isHostname() || this.isIp()" + }, + (predefined).cel = { + id: "string.address_empty" + message: "value is empty, which is not a valid hostname, or ip address" + expression: "!rules.address || this != ''" + } + ]; + + // `uuid` specifies that the field value must be a valid UUID as defined by + // [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.2). If the + // field value isn't a valid UUID, an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid UUID + // string value = 1 [(buf.validate.field).string.uuid = true]; + // } + // ``` + bool uuid = 22 [ + (predefined).cel = { + id: "string.uuid" + message: "value must be a valid UUID" + expression: "!rules.uuid || this == '' || this.matches('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')" + }, + (predefined).cel = { + id: "string.uuid_empty" + message: "value is empty, which is not a valid UUID" + expression: "!rules.uuid || this != ''" + } + ]; + + // `tuuid` (trimmed UUID) specifies that the field value must be a valid UUID as + // defined by [RFC 4122](https://tools.ietf.org/html/rfc4122#section-4.1.2) with all dashes + // omitted. If the field value isn't a valid UUID without dashes, an error message + // will be generated. + // + // ```proto + // message MyString { + // // value must be a valid trimmed UUID + // string value = 1 [(buf.validate.field).string.tuuid = true]; + // } + // ``` + bool tuuid = 33 [ + (predefined).cel = { + id: "string.tuuid" + message: "value must be a valid trimmed UUID" + expression: "!rules.tuuid || this == '' || this.matches('^[0-9a-fA-F]{32}$')" + }, + (predefined).cel = { + id: "string.tuuid_empty" + message: "value is empty, which is not a valid trimmed UUID" + expression: "!rules.tuuid || this != ''" + } + ]; + + // `ip_with_prefixlen` specifies that the field value must be a valid IP (v4 or v6) + // address with prefix length. If the field value isn't a valid IP with prefix + // length, an error message will be generated. + // + // + // ```proto + // message MyString { + // // value must be a valid IP with prefix length + // string value = 1 [(buf.validate.field).string.ip_with_prefixlen = true]; + // } + // ``` + bool ip_with_prefixlen = 26 [ + (predefined).cel = { + id: "string.ip_with_prefixlen" + message: "value must be a valid IP prefix" + expression: "!rules.ip_with_prefixlen || this == '' || this.isIpPrefix()" + }, + (predefined).cel = { + id: "string.ip_with_prefixlen_empty" + message: "value is empty, which is not a valid IP prefix" + expression: "!rules.ip_with_prefixlen || this != ''" + } + ]; + + // `ipv4_with_prefixlen` specifies that the field value must be a valid + // IPv4 address with prefix. + // If the field value isn't a valid IPv4 address with prefix length, + // an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid IPv4 address with prefix length + // string value = 1 [(buf.validate.field).string.ipv4_with_prefixlen = true]; + // } + // ``` + bool ipv4_with_prefixlen = 27 [ + (predefined).cel = { + id: "string.ipv4_with_prefixlen" + message: "value must be a valid IPv4 address with prefix length" + expression: "!rules.ipv4_with_prefixlen || this == '' || this.isIpPrefix(4)" + }, + (predefined).cel = { + id: "string.ipv4_with_prefixlen_empty" + message: "value is empty, which is not a valid IPv4 address with prefix length" + expression: "!rules.ipv4_with_prefixlen || this != ''" + } + ]; + + // `ipv6_with_prefixlen` specifies that the field value must be a valid + // IPv6 address with prefix length. + // If the field value is not a valid IPv6 address with prefix length, + // an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid IPv6 address prefix length + // string value = 1 [(buf.validate.field).string.ipv6_with_prefixlen = true]; + // } + // ``` + bool ipv6_with_prefixlen = 28 [ + (predefined).cel = { + id: "string.ipv6_with_prefixlen" + message: "value must be a valid IPv6 address with prefix length" + expression: "!rules.ipv6_with_prefixlen || this == '' || this.isIpPrefix(6)" + }, + (predefined).cel = { + id: "string.ipv6_with_prefixlen_empty" + message: "value is empty, which is not a valid IPv6 address with prefix length" + expression: "!rules.ipv6_with_prefixlen || this != ''" + } + ]; + + // `ip_prefix` specifies that the field value must be a valid IP (v4 or v6) prefix. + // If the field value isn't a valid IP prefix, an error message will be + // generated. The prefix must have all zeros for the masked bits of the prefix (e.g., + // `127.0.0.0/16`, not `127.0.0.1/16`). + // + // ```proto + // message MyString { + // // value must be a valid IP prefix + // string value = 1 [(buf.validate.field).string.ip_prefix = true]; + // } + // ``` + bool ip_prefix = 29 [ + (predefined).cel = { + id: "string.ip_prefix" + message: "value must be a valid IP prefix" + expression: "!rules.ip_prefix || this == '' || this.isIpPrefix(true)" + }, + (predefined).cel = { + id: "string.ip_prefix_empty" + message: "value is empty, which is not a valid IP prefix" + expression: "!rules.ip_prefix || this != ''" + } + ]; + + // `ipv4_prefix` specifies that the field value must be a valid IPv4 + // prefix. If the field value isn't a valid IPv4 prefix, an error message + // will be generated. The prefix must have all zeros for the masked bits of + // the prefix (e.g., `127.0.0.0/16`, not `127.0.0.1/16`). + // + // ```proto + // message MyString { + // // value must be a valid IPv4 prefix + // string value = 1 [(buf.validate.field).string.ipv4_prefix = true]; + // } + // ``` + bool ipv4_prefix = 30 [ + (predefined).cel = { + id: "string.ipv4_prefix" + message: "value must be a valid IPv4 prefix" + expression: "!rules.ipv4_prefix || this == '' || this.isIpPrefix(4, true)" + }, + (predefined).cel = { + id: "string.ipv4_prefix_empty" + message: "value is empty, which is not a valid IPv4 prefix" + expression: "!rules.ipv4_prefix || this != ''" + } + ]; + + // `ipv6_prefix` specifies that the field value must be a valid IPv6 prefix. + // If the field value is not a valid IPv6 prefix, an error message will be + // generated. The prefix must have all zeros for the masked bits of the prefix + // (e.g., `2001:db8::/48`, not `2001:db8::1/48`). + // + // ```proto + // message MyString { + // // value must be a valid IPv6 prefix + // string value = 1 [(buf.validate.field).string.ipv6_prefix = true]; + // } + // ``` + bool ipv6_prefix = 31 [ + (predefined).cel = { + id: "string.ipv6_prefix" + message: "value must be a valid IPv6 prefix" + expression: "!rules.ipv6_prefix || this == '' || this.isIpPrefix(6, true)" + }, + (predefined).cel = { + id: "string.ipv6_prefix_empty" + message: "value is empty, which is not a valid IPv6 prefix" + expression: "!rules.ipv6_prefix || this != ''" + } + ]; + + // `host_and_port` specifies the field value must be a valid host and port + // pair. The host must be a valid hostname or IP address while the port + // must be in the range of 0-65535, inclusive. IPv6 addresses must be delimited + // with square brackets (e.g., `[::1]:1234`). + bool host_and_port = 32 [ + (predefined).cel = { + id: "string.host_and_port" + message: "value must be a valid host (hostname or IP address) and port pair" + expression: "!rules.host_and_port || this == '' || this.isHostAndPort(true)" + }, + (predefined).cel = { + id: "string.host_and_port_empty" + message: "value is empty, which is not a valid host and port pair" + expression: "!rules.host_and_port || this != ''" + } + ]; + + // `well_known_regex` specifies a common well-known pattern + // defined as a regex. If the field value doesn't match the well-known + // regex, an error message will be generated. + // + // ```proto + // message MyString { + // // value must be a valid HTTP header value + // string value = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE]; + // } + // ``` + // + // #### KnownRegex + // + // `well_known_regex` contains some well-known patterns. + // + // | Name | Number | Description | + // |-------------------------------|--------|-------------------------------------------| + // | KNOWN_REGEX_UNSPECIFIED | 0 | | + // | KNOWN_REGEX_HTTP_HEADER_NAME | 1 | HTTP header name as defined by [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2) | + // | KNOWN_REGEX_HTTP_HEADER_VALUE | 2 | HTTP header value as defined by [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2.4) | + KnownRegex well_known_regex = 24 [ + (predefined).cel = { + id: "string.well_known_regex.header_name" + message: "value must be a valid HTTP header name" + expression: + "rules.well_known_regex != 1 || this == '' || this.matches(!has(rules.strict) || rules.strict ?" + "'^:?[0-9a-zA-Z!#$%&\\'*+-.^_|~\\x60]+$' :" + "'^[^\\u0000\\u000A\\u000D]+$')" + }, + (predefined).cel = { + id: "string.well_known_regex.header_name_empty" + message: "value is empty, which is not a valid HTTP header name" + expression: "rules.well_known_regex != 1 || this != ''" + }, + (predefined).cel = { + id: "string.well_known_regex.header_value" + message: "value must be a valid HTTP header value" + expression: + "rules.well_known_regex != 2 || this.matches(!has(rules.strict) || rules.strict ?" + "'^[^\\u0000-\\u0008\\u000A-\\u001F\\u007F]*$' :" + "'^[^\\u0000\\u000A\\u000D]*$')" + } + ]; + } + + // This applies to regexes `HTTP_HEADER_NAME` and `HTTP_HEADER_VALUE` to + // enable strict header validation. By default, this is true, and HTTP header + // validations are [RFC-compliant](https://tools.ietf.org/html/rfc7230#section-3). Setting to false will enable looser + // validations that only disallow `\r\n\0` characters, which can be used to + // bypass header matching rules. + // + // ```proto + // message MyString { + // // The field `value` must have be a valid HTTP headers, but not enforced with strict rules. + // string value = 1 [(buf.validate.field).string.strict = false]; + // } + // ``` + optional bool strict = 25; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyString { + // string value = 1 [ + // (buf.validate.field).string.example = "hello", + // (buf.validate.field).string.example = "world" + // ]; + // } + // ``` + repeated string example = 34 [(predefined).cel = { + id: "string.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// WellKnownRegex contain some well-known patterns. +enum KnownRegex { + KNOWN_REGEX_UNSPECIFIED = 0; + + // HTTP header name as defined by [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2). + KNOWN_REGEX_HTTP_HEADER_NAME = 1; + + // HTTP header value as defined by [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2.4). + KNOWN_REGEX_HTTP_HEADER_VALUE = 2; +} + +// BytesRules describe the constraints applied to `bytes` values. These rules +// may also be applied to the `google.protobuf.BytesValue` Well-Known-Type. +message BytesRules { + // `const` requires the field value to exactly match the specified bytes + // value. If the field value doesn't match, an error message is generated. + // + // ```proto + // message MyBytes { + // // value must be "\x01\x02\x03\x04" + // bytes value = 1 [(buf.validate.field).bytes.const = "\x01\x02\x03\x04"]; + // } + // ``` + optional bytes const = 1 [(predefined).cel = { + id: "bytes.const" + expression: "this != rules.const ? 'value must be %x'.format([rules.const]) : ''" + }]; + + // `len` requires the field value to have the specified length in bytes. + // If the field value doesn't match, an error message is generated. + // + // ```proto + // message MyBytes { + // // value length must be 4 bytes. + // optional bytes value = 1 [(buf.validate.field).bytes.len = 4]; + // } + // ``` + optional uint64 len = 13 [(predefined).cel = { + id: "bytes.len" + expression: "uint(this.size()) != rules.len ? 'value length must be %s bytes'.format([rules.len]) : ''" + }]; + + // `min_len` requires the field value to have at least the specified minimum + // length in bytes. + // If the field value doesn't meet the requirement, an error message is generated. + // + // ```proto + // message MyBytes { + // // value length must be at least 2 bytes. + // optional bytes value = 1 [(buf.validate.field).bytes.min_len = 2]; + // } + // ``` + optional uint64 min_len = 2 [(predefined).cel = { + id: "bytes.min_len" + expression: "uint(this.size()) < rules.min_len ? 'value length must be at least %s bytes'.format([rules.min_len]) : ''" + }]; + + // `max_len` requires the field value to have at most the specified maximum + // length in bytes. + // If the field value exceeds the requirement, an error message is generated. + // + // ```proto + // message MyBytes { + // // value must be at most 6 bytes. + // optional bytes value = 1 [(buf.validate.field).bytes.max_len = 6]; + // } + // ``` + optional uint64 max_len = 3 [(predefined).cel = { + id: "bytes.max_len" + expression: "uint(this.size()) > rules.max_len ? 'value must be at most %s bytes'.format([rules.max_len]) : ''" + }]; + + // `pattern` requires the field value to match the specified regular + // expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)). + // The value of the field must be valid UTF-8 or validation will fail with a + // runtime error. + // If the field value doesn't match the pattern, an error message is generated. + // + // ```proto + // message MyBytes { + // // value must match regex pattern "^[a-zA-Z0-9]+$". + // optional bytes value = 1 [(buf.validate.field).bytes.pattern = "^[a-zA-Z0-9]+$"]; + // } + // ``` + optional string pattern = 4 [(predefined).cel = { + id: "bytes.pattern" + expression: "!string(this).matches(rules.pattern) ? 'value must match regex pattern `%s`'.format([rules.pattern]) : ''" + }]; + + // `prefix` requires the field value to have the specified bytes at the + // beginning of the string. + // If the field value doesn't meet the requirement, an error message is generated. + // + // ```proto + // message MyBytes { + // // value does not have prefix \x01\x02 + // optional bytes value = 1 [(buf.validate.field).bytes.prefix = "\x01\x02"]; + // } + // ``` + optional bytes prefix = 5 [(predefined).cel = { + id: "bytes.prefix" + expression: "!this.startsWith(rules.prefix) ? 'value does not have prefix %x'.format([rules.prefix]) : ''" + }]; + + // `suffix` requires the field value to have the specified bytes at the end + // of the string. + // If the field value doesn't meet the requirement, an error message is generated. + // + // ```proto + // message MyBytes { + // // value does not have suffix \x03\x04 + // optional bytes value = 1 [(buf.validate.field).bytes.suffix = "\x03\x04"]; + // } + // ``` + optional bytes suffix = 6 [(predefined).cel = { + id: "bytes.suffix" + expression: "!this.endsWith(rules.suffix) ? 'value does not have suffix %x'.format([rules.suffix]) : ''" + }]; + + // `contains` requires the field value to have the specified bytes anywhere in + // the string. + // If the field value doesn't meet the requirement, an error message is generated. + // + // ```protobuf + // message MyBytes { + // // value does not contain \x02\x03 + // optional bytes value = 1 [(buf.validate.field).bytes.contains = "\x02\x03"]; + // } + // ``` + optional bytes contains = 7 [(predefined).cel = { + id: "bytes.contains" + expression: "!this.contains(rules.contains) ? 'value does not contain %x'.format([rules.contains]) : ''" + }]; + + // `in` requires the field value to be equal to one of the specified + // values. If the field value doesn't match any of the specified values, an + // error message is generated. + // + // ```protobuf + // message MyBytes { + // // value must in ["\x01\x02", "\x02\x03", "\x03\x04"] + // optional bytes value = 1 [(buf.validate.field).bytes.in = {"\x01\x02", "\x02\x03", "\x03\x04"}]; + // } + // ``` + repeated bytes in = 8 [(predefined).cel = { + id: "bytes.in" + expression: "dyn(rules)['in'].size() > 0 && !(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to be not equal to any of the specified + // values. + // If the field value matches any of the specified values, an error message is + // generated. + // + // ```proto + // message MyBytes { + // // value must not in ["\x01\x02", "\x02\x03", "\x03\x04"] + // optional bytes value = 1 [(buf.validate.field).bytes.not_in = {"\x01\x02", "\x02\x03", "\x03\x04"}]; + // } + // ``` + repeated bytes not_in = 9 [(predefined).cel = { + id: "bytes.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // WellKnown rules provide advanced constraints against common byte + // patterns + oneof well_known { + // `ip` ensures that the field `value` is a valid IP address (v4 or v6) in byte format. + // If the field value doesn't meet this constraint, an error message is generated. + // + // ```proto + // message MyBytes { + // // value must be a valid IP address + // optional bytes value = 1 [(buf.validate.field).bytes.ip = true]; + // } + // ``` + bool ip = 10 [ + (predefined).cel = { + id: "bytes.ip" + message: "value must be a valid IP address" + expression: "!rules.ip || this.size() == 0 || this.size() == 4 || this.size() == 16" + }, + (predefined).cel = { + id: "bytes.ip_empty" + message: "value is empty, which is not a valid IP address" + expression: "!rules.ip || this.size() != 0" + } + ]; + + // `ipv4` ensures that the field `value` is a valid IPv4 address in byte format. + // If the field value doesn't meet this constraint, an error message is generated. + // + // ```proto + // message MyBytes { + // // value must be a valid IPv4 address + // optional bytes value = 1 [(buf.validate.field).bytes.ipv4 = true]; + // } + // ``` + bool ipv4 = 11 [ + (predefined).cel = { + id: "bytes.ipv4" + message: "value must be a valid IPv4 address" + expression: "!rules.ipv4 || this.size() == 0 || this.size() == 4" + }, + (predefined).cel = { + id: "bytes.ipv4_empty" + message: "value is empty, which is not a valid IPv4 address" + expression: "!rules.ipv4 || this.size() != 0" + } + ]; + + // `ipv6` ensures that the field `value` is a valid IPv6 address in byte format. + // If the field value doesn't meet this constraint, an error message is generated. + // ```proto + // message MyBytes { + // // value must be a valid IPv6 address + // optional bytes value = 1 [(buf.validate.field).bytes.ipv6 = true]; + // } + // ``` + bool ipv6 = 12 [ + (predefined).cel = { + id: "bytes.ipv6" + message: "value must be a valid IPv6 address" + expression: "!rules.ipv6 || this.size() == 0 || this.size() == 16" + }, + (predefined).cel = { + id: "bytes.ipv6_empty" + message: "value is empty, which is not a valid IPv6 address" + expression: "!rules.ipv6 || this.size() != 0" + } + ]; + } + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyBytes { + // bytes value = 1 [ + // (buf.validate.field).bytes.example = "\x01\x02", + // (buf.validate.field).bytes.example = "\x02\x03" + // ]; + // } + // ``` + repeated bytes example = 14 [(predefined).cel = { + id: "bytes.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// EnumRules describe the constraints applied to `enum` values. +message EnumRules { + // `const` requires the field value to exactly match the specified enum value. + // If the field value doesn't match, an error message is generated. + // + // ```proto + // enum MyEnum { + // MY_ENUM_UNSPECIFIED = 0; + // MY_ENUM_VALUE1 = 1; + // MY_ENUM_VALUE2 = 2; + // } + // + // message MyMessage { + // // The field `value` must be exactly MY_ENUM_VALUE1. + // MyEnum value = 1 [(buf.validate.field).enum.const = 1]; + // } + // ``` + optional int32 const = 1 [(predefined).cel = { + id: "enum.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + + // `defined_only` requires the field value to be one of the defined values for + // this enum, failing on any undefined value. + // + // ```proto + // enum MyEnum { + // MY_ENUM_UNSPECIFIED = 0; + // MY_ENUM_VALUE1 = 1; + // MY_ENUM_VALUE2 = 2; + // } + // + // message MyMessage { + // // The field `value` must be a defined value of MyEnum. + // MyEnum value = 1 [(buf.validate.field).enum.defined_only = true]; + // } + // ``` + optional bool defined_only = 2; + + // `in` requires the field value to be equal to one of the + //specified enum values. If the field value doesn't match any of the + //specified values, an error message is generated. + // + // ```proto + // enum MyEnum { + // MY_ENUM_UNSPECIFIED = 0; + // MY_ENUM_VALUE1 = 1; + // MY_ENUM_VALUE2 = 2; + // } + // + // message MyMessage { + // // The field `value` must be equal to one of the specified values. + // MyEnum value = 1 [(buf.validate.field).enum = { in: [1, 2]}]; + // } + // ``` + repeated int32 in = 3 [(predefined).cel = { + id: "enum.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` requires the field value to be not equal to any of the + //specified enum values. If the field value matches one of the specified + // values, an error message is generated. + // + // ```proto + // enum MyEnum { + // MY_ENUM_UNSPECIFIED = 0; + // MY_ENUM_VALUE1 = 1; + // MY_ENUM_VALUE2 = 2; + // } + // + // message MyMessage { + // // The field `value` must not be equal to any of the specified values. + // MyEnum value = 1 [(buf.validate.field).enum = { not_in: [1, 2]}]; + // } + // ``` + repeated int32 not_in = 4 [(predefined).cel = { + id: "enum.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // enum MyEnum { + // MY_ENUM_UNSPECIFIED = 0; + // MY_ENUM_VALUE1 = 1; + // MY_ENUM_VALUE2 = 2; + // } + // + // message MyMessage { + // (buf.validate.field).enum.example = 1, + // (buf.validate.field).enum.example = 2 + // } + // ``` + repeated int32 example = 5 [(predefined).cel = { + id: "enum.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// RepeatedRules describe the constraints applied to `repeated` values. +message RepeatedRules { + // `min_items` requires that this field must contain at least the specified + // minimum number of items. + // + // Note that `min_items = 1` is equivalent to setting a field as `required`. + // + // ```proto + // message MyRepeated { + // // value must contain at least 2 items + // repeated string value = 1 [(buf.validate.field).repeated.min_items = 2]; + // } + // ``` + optional uint64 min_items = 1 [(predefined).cel = { + id: "repeated.min_items" + expression: "uint(this.size()) < rules.min_items ? 'value must contain at least %d item(s)'.format([rules.min_items]) : ''" + }]; + + // `max_items` denotes that this field must not exceed a + // certain number of items as the upper limit. If the field contains more + // items than specified, an error message will be generated, requiring the + // field to maintain no more than the specified number of items. + // + // ```proto + // message MyRepeated { + // // value must contain no more than 3 item(s) + // repeated string value = 1 [(buf.validate.field).repeated.max_items = 3]; + // } + // ``` + optional uint64 max_items = 2 [(predefined).cel = { + id: "repeated.max_items" + expression: "uint(this.size()) > rules.max_items ? 'value must contain no more than %s item(s)'.format([rules.max_items]) : ''" + }]; + + // `unique` indicates that all elements in this field must + // be unique. This constraint is strictly applicable to scalar and enum + // types, with message types not being supported. + // + // ```proto + // message MyRepeated { + // // repeated value must contain unique items + // repeated string value = 1 [(buf.validate.field).repeated.unique = true]; + // } + // ``` + optional bool unique = 3 [(predefined).cel = { + id: "repeated.unique" + message: "repeated value must contain unique items" + expression: "!rules.unique || this.unique()" + }]; + + // `items` details the constraints to be applied to each item + // in the field. Even for repeated message fields, validation is executed + // against each item unless skip is explicitly specified. + // + // ```proto + // message MyRepeated { + // // The items in the field `value` must follow the specified constraints. + // repeated string value = 1 [(buf.validate.field).repeated.items = { + // string: { + // min_len: 3 + // max_len: 10 + // } + // }]; + // } + // ``` + optional FieldConstraints items = 4; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// MapRules describe the constraints applied to `map` values. +message MapRules { + //Specifies the minimum number of key-value pairs allowed. If the field has + // fewer key-value pairs than specified, an error message is generated. + // + // ```proto + // message MyMap { + // // The field `value` must have at least 2 key-value pairs. + // map value = 1 [(buf.validate.field).map.min_pairs = 2]; + // } + // ``` + optional uint64 min_pairs = 1 [(predefined).cel = { + id: "map.min_pairs" + expression: "uint(this.size()) < rules.min_pairs ? 'map must be at least %d entries'.format([rules.min_pairs]) : ''" + }]; + + //Specifies the maximum number of key-value pairs allowed. If the field has + // more key-value pairs than specified, an error message is generated. + // + // ```proto + // message MyMap { + // // The field `value` must have at most 3 key-value pairs. + // map value = 1 [(buf.validate.field).map.max_pairs = 3]; + // } + // ``` + optional uint64 max_pairs = 2 [(predefined).cel = { + id: "map.max_pairs" + expression: "uint(this.size()) > rules.max_pairs ? 'map must be at most %d entries'.format([rules.max_pairs]) : ''" + }]; + + //Specifies the constraints to be applied to each key in the field. + // + // ```proto + // message MyMap { + // // The keys in the field `value` must follow the specified constraints. + // map value = 1 [(buf.validate.field).map.keys = { + // string: { + // min_len: 3 + // max_len: 10 + // } + // }]; + // } + // ``` + optional FieldConstraints keys = 4; + + //Specifies the constraints to be applied to the value of each key in the + // field. Message values will still have their validations evaluated unless + //skip is specified here. + // + // ```proto + // message MyMap { + // // The values in the field `value` must follow the specified constraints. + // map value = 1 [(buf.validate.field).map.values = { + // string: { + // min_len: 5 + // max_len: 20 + // } + // }]; + // } + // ``` + optional FieldConstraints values = 5; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// AnyRules describe constraints applied exclusively to the `google.protobuf.Any` well-known type. +message AnyRules { + // `in` requires the field's `type_url` to be equal to one of the + //specified values. If it doesn't match any of the specified values, an error + // message is generated. + // + // ```proto + // message MyAny { + // // The `value` field must have a `type_url` equal to one of the specified values. + // google.protobuf.Any value = 1 [(buf.validate.field).any.in = ["type.googleapis.com/MyType1", "type.googleapis.com/MyType2"]]; + // } + // ``` + repeated string in = 2; + + // requires the field's type_url to be not equal to any of the specified values. If it matches any of the specified values, an error message is generated. + // + // ```proto + // message MyAny { + // // The field `value` must not have a `type_url` equal to any of the specified values. + // google.protobuf.Any value = 1 [(buf.validate.field).any.not_in = ["type.googleapis.com/ForbiddenType1", "type.googleapis.com/ForbiddenType2"]]; + // } + // ``` + repeated string not_in = 3; +} + +// DurationRules describe the constraints applied exclusively to the `google.protobuf.Duration` well-known type. +message DurationRules { + // `const` dictates that the field must match the specified value of the `google.protobuf.Duration` type exactly. + // If the field's value deviates from the specified value, an error message + // will be generated. + // + // ```proto + // message MyDuration { + // // value must equal 5s + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.const = "5s"]; + // } + // ``` + optional google.protobuf.Duration const = 2 [(predefined).cel = { + id: "duration.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // `lt` stipulates that the field must be less than the specified value of the `google.protobuf.Duration` type, + // exclusive. If the field's value is greater than or equal to the specified + // value, an error message will be generated. + // + // ```proto + // message MyDuration { + // // value must be less than 5s + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = "5s"]; + // } + // ``` + google.protobuf.Duration lt = 3 [(predefined).cel = { + id: "duration.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // `lte` indicates that the field must be less than or equal to the specified + // value of the `google.protobuf.Duration` type, inclusive. If the field's value is greater than the specified value, + // an error message will be generated. + // + // ```proto + // message MyDuration { + // // value must be less than or equal to 10s + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.lte = "10s"]; + // } + // ``` + google.protobuf.Duration lte = 4 [(predefined).cel = { + id: "duration.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + } + oneof greater_than { + // `gt` requires the duration field value to be greater than the specified + // value (exclusive). If the value of `gt` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyDuration { + // // duration must be greater than 5s [duration.gt] + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.gt = { seconds: 5 }]; + // + // // duration must be greater than 5s and less than 10s [duration.gt_lt] + // google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gt: { seconds: 5 }, lt: { seconds: 10 } }]; + // + // // duration must be greater than 10s or less than 5s [duration.gt_lt_exclusive] + // google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gt: { seconds: 10 }, lt: { seconds: 5 } }]; + // } + // ``` + google.protobuf.Duration gt = 5 [ + (predefined).cel = { + id: "duration.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "duration.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "duration.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "duration.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "duration.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the duration field value to be greater than or equal to the + // specified value (exclusive). If the value of `gte` is larger than a + // specified `lt` or `lte`, the range is reversed, and the field value must + // be outside the specified range. If the field value doesn't meet the + // required conditions, an error message is generated. + // + // ```proto + // message MyDuration { + // // duration must be greater than or equal to 5s [duration.gte] + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.gte = { seconds: 5 }]; + // + // // duration must be greater than or equal to 5s and less than 10s [duration.gte_lt] + // google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gte: { seconds: 5 }, lt: { seconds: 10 } }]; + // + // // duration must be greater than or equal to 10s or less than 5s [duration.gte_lt_exclusive] + // google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gte: { seconds: 10 }, lt: { seconds: 5 } }]; + // } + // ``` + google.protobuf.Duration gte = 6 [ + (predefined).cel = { + id: "duration.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "duration.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "duration.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "duration.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "duration.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + } + + // `in` asserts that the field must be equal to one of the specified values of the `google.protobuf.Duration` type. + // If the field's value doesn't correspond to any of the specified values, + // an error message will be generated. + // + // ```proto + // message MyDuration { + // // value must be in list [1s, 2s, 3s] + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.in = ["1s", "2s", "3s"]]; + // } + // ``` + repeated google.protobuf.Duration in = 7 [(predefined).cel = { + id: "duration.in" + expression: "!(this in dyn(rules)['in']) ? 'value must be in list %s'.format([dyn(rules)['in']]) : ''" + }]; + + // `not_in` denotes that the field must not be equal to + // any of the specified values of the `google.protobuf.Duration` type. + // If the field's value matches any of these values, an error message will be + // generated. + // + // ```proto + // message MyDuration { + // // value must not be in list [1s, 2s, 3s] + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.not_in = ["1s", "2s", "3s"]]; + // } + // ``` + repeated google.protobuf.Duration not_in = 8 [(predefined).cel = { + id: "duration.not_in" + expression: "this in rules.not_in ? 'value must not be in list %s'.format([rules.not_in]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyDuration { + // google.protobuf.Duration value = 1 [ + // (buf.validate.field).duration.example = { seconds: 1 }, + // (buf.validate.field).duration.example = { seconds: 2 }, + // ]; + // } + // ``` + repeated google.protobuf.Duration example = 9 [(predefined).cel = { + id: "duration.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// TimestampRules describe the constraints applied exclusively to the `google.protobuf.Timestamp` well-known type. +message TimestampRules { + // `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated. + // + // ```proto + // message MyTimestamp { + // // value must equal 2023-05-03T10:00:00Z + // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.const = {seconds: 1727998800}]; + // } + // ``` + optional google.protobuf.Timestamp const = 2 [(predefined).cel = { + id: "timestamp.const" + expression: "this != rules.const ? 'value must equal %s'.format([rules.const]) : ''" + }]; + oneof less_than { + // requires the duration field value to be less than the specified value (field < value). If the field value doesn't meet the required conditions, an error message is generated. + // + // ```proto + // message MyDuration { + // // duration must be less than 'P3D' [duration.lt] + // google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = { seconds: 259200 }]; + // } + // ``` + google.protobuf.Timestamp lt = 3 [(predefined).cel = { + id: "timestamp.lt" + expression: + "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" + "? 'value must be less than %s'.format([rules.lt]) : ''" + }]; + + // requires the timestamp field value to be less than or equal to the specified value (field <= value). If the field value doesn't meet the required conditions, an error message is generated. + // + // ```proto + // message MyTimestamp { + // // timestamp must be less than or equal to '2023-05-14T00:00:00Z' [timestamp.lte] + // google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lte = { seconds: 1678867200 }]; + // } + // ``` + google.protobuf.Timestamp lte = 4 [(predefined).cel = { + id: "timestamp.lte" + expression: + "!has(rules.gte) && !has(rules.gt) && this > rules.lte" + "? 'value must be less than or equal to %s'.format([rules.lte]) : ''" + }]; + + // `lt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be less than the current time. `lt_now` can only be used with the `within` rule. + // + // ```proto + // message MyTimestamp { + // // value must be less than now + // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true]; + // } + // ``` + bool lt_now = 7 [(predefined).cel = { + id: "timestamp.lt_now" + expression: "(rules.lt_now && this > now) ? 'value must be less than now' : ''" + }]; + } + oneof greater_than { + // `gt` requires the timestamp field value to be greater than the specified + // value (exclusive). If the value of `gt` is larger than a specified `lt` + // or `lte`, the range is reversed, and the field value must be outside the + // specified range. If the field value doesn't meet the required conditions, + // an error message is generated. + // + // ```proto + // message MyTimestamp { + // // timestamp must be greater than '2023-01-01T00:00:00Z' [timestamp.gt] + // google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt = { seconds: 1672444800 }]; + // + // // timestamp must be greater than '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gt_lt] + // google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gt: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }]; + // + // // timestamp must be greater than '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gt_lt_exclusive] + // google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gt: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }]; + // } + // ``` + google.protobuf.Timestamp gt = 5 [ + (predefined).cel = { + id: "timestamp.gt" + expression: + "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" + "? 'value must be greater than %s'.format([rules.gt]) : ''" + }, + (predefined).cel = { + id: "timestamp.gt_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" + "? 'value must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "timestamp.gt_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" + "? 'value must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" + }, + (predefined).cel = { + id: "timestamp.gt_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" + "? 'value must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + }, + (predefined).cel = { + id: "timestamp.gt_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" + "? 'value must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" + } + ]; + + // `gte` requires the timestamp field value to be greater than or equal to the + // specified value (exclusive). If the value of `gte` is larger than a + // specified `lt` or `lte`, the range is reversed, and the field value + // must be outside the specified range. If the field value doesn't meet + // the required conditions, an error message is generated. + // + // ```proto + // message MyTimestamp { + // // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' [timestamp.gte] + // google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gte = { seconds: 1672444800 }]; + // + // // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gte_lt] + // google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gte: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }]; + // + // // timestamp must be greater than or equal to '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gte_lt_exclusive] + // google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gte: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }]; + // } + // ``` + google.protobuf.Timestamp gte = 6 [ + (predefined).cel = { + id: "timestamp.gte" + expression: + "!has(rules.lt) && !has(rules.lte) && this < rules.gte" + "? 'value must be greater than or equal to %s'.format([rules.gte]) : ''" + }, + (predefined).cel = { + id: "timestamp.gte_lt" + expression: + "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "timestamp.gte_lt_exclusive" + expression: + "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" + }, + (predefined).cel = { + id: "timestamp.gte_lte" + expression: + "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" + "? 'value must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + }, + (predefined).cel = { + id: "timestamp.gte_lte_exclusive" + expression: + "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" + "? 'value must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" + } + ]; + + // `gt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be greater than the current time. `gt_now` can only be used with the `within` rule. + // + // ```proto + // message MyTimestamp { + // // value must be greater than now + // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.gt_now = true]; + // } + // ``` + bool gt_now = 8 [(predefined).cel = { + id: "timestamp.gt_now" + expression: "(rules.gt_now && this < now) ? 'value must be greater than now' : ''" + }]; + } + + // `within` specifies that this field, of the `google.protobuf.Timestamp` type, must be within the specified duration of the current time. If the field value isn't within the duration, an error message is generated. + // + // ```proto + // message MyTimestamp { + // // value must be within 1 hour of now + // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.within = {seconds: 3600}]; + // } + // ``` + optional google.protobuf.Duration within = 9 [(predefined).cel = { + id: "timestamp.within" + expression: "this < now-rules.within || this > now+rules.within ? 'value must be within %s of now'.format([rules.within]) : ''" + }]; + + // `example` specifies values that the field may have. These values SHOULD + // conform to other constraints. `example` values will not impact validation + // but may be used as helpful guidance on how to populate the given field. + // + // ```proto + // message MyTimestamp { + // google.protobuf.Timestamp value = 1 [ + // (buf.validate.field).timestamp.example = { seconds: 1672444800 }, + // (buf.validate.field).timestamp.example = { seconds: 1672531200 }, + // ]; + // } + // ``` + + repeated google.protobuf.Timestamp example = 10 [(predefined).cel = { + id: "timestamp.example" + expression: "true" + }]; + + // Extension fields in this range that have the (buf.validate.predefined) + // option set will be treated as predefined field constraints that can then be + // set on the field options of other fields to apply field constraints. + // Extension numbers 1000 to 99999 are reserved for extension numbers that are + // defined in the [Protobuf Global Extension Registry][1]. Extension numbers + // above this range are reserved for extension numbers that are not explicitly + // assigned. For rules defined in publicly-consumed schemas, use of extensions + // above 99999 is discouraged due to the risk of conflicts. + // + // [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md + extensions 1000 to max; +} + +// `Violations` is a collection of `Violation` messages. This message type is returned by +// protovalidate when a proto message fails to meet the requirements set by the `Constraint` validation rules. +// Each individual violation is represented by a `Violation` message. +message Violations { + // `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected. + repeated Violation violations = 1; +} + +// `Violation` represents a single instance where a validation rule, expressed +// as a `Constraint`, was not met. It provides information about the field that +// caused the violation, the specific constraint that wasn't fulfilled, and a +// human-readable error message. +// +// ```json +// { +// "fieldPath": "bar", +// "constraintId": "foo.bar", +// "message": "bar must be greater than 0" +// } +// ``` +message Violation { + // `field` is a machine-readable path to the field that failed validation. + // This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation. + // + // For example, consider the following message: + // + // ```proto + // message Message { + // bool a = 1 [(buf.validate.field).required = true]; + // } + // ``` + // + // It could produce the following violation: + // + // ```textproto + // violation { + // field { element { field_number: 1, field_name: "a", field_type: 8 } } + // ... + // } + // ``` + optional FieldPath field = 5; + + // `rule` is a machine-readable path that points to the specific constraint rule that failed validation. + // This will be a nested field starting from the FieldConstraints of the field that failed validation. + // For custom constraints, this will provide the path of the constraint, e.g. `cel[0]`. + // + // For example, consider the following message: + // + // ```proto + // message Message { + // bool a = 1 [(buf.validate.field).required = true]; + // bool b = 2 [(buf.validate.field).cel = { + // id: "custom_constraint", + // expression: "!this ? 'b must be true': ''" + // }] + // } + // ``` + // + // It could produce the following violations: + // + // ```textproto + // violation { + // rule { element { field_number: 25, field_name: "required", field_type: 8 } } + // ... + // } + // violation { + // rule { element { field_number: 23, field_name: "cel", field_type: 11, index: 0 } } + // ... + // } + // ``` + optional FieldPath rule = 6; + + // `field_path` is a human-readable identifier that points to the specific field that failed the validation. + // This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation. + // + // Deprecated: use the `field` instead. + optional string field_path = 1 [deprecated = true]; + + // `constraint_id` is the unique identifier of the `Constraint` that was not fulfilled. + // This is the same `id` that was specified in the `Constraint` message, allowing easy tracing of which rule was violated. + optional string constraint_id = 2; + + // `message` is a human-readable error message that describes the nature of the violation. + // This can be the default error message from the violated `Constraint`, or it can be a custom message that gives more context about the violation. + optional string message = 3; + + // `for_key` indicates whether the violation was caused by a map key, rather than a value. + optional bool for_key = 4; +} + +// `FieldPath` provides a path to a nested protobuf field. +// +// This message provides enough information to render a dotted field path even without protobuf descriptors. +// It also provides enough information to resolve a nested field through unknown wire data. +message FieldPath { + // `elements` contains each element of the path, starting from the root and recursing downward. + repeated FieldPathElement elements = 1; +} + +// `FieldPathElement` provides enough information to nest through a single protobuf field. +// +// If the selected field is a map or repeated field, the `subscript` value selects a specific element from it. +// A path that refers to a value nested under a map key or repeated field index will have a `subscript` value. +// The `field_type` field allows unambiguous resolution of a field even if descriptors are not available. +message FieldPathElement { + // `field_number` is the field number this path element refers to. + optional int32 field_number = 1; + + // `field_name` contains the field name this path element refers to. + // This can be used to display a human-readable path even if the field number is unknown. + optional string field_name = 2; + + // `field_type` specifies the type of this field. When using reflection, this value is not needed. + // + // This value is provided to make it possible to traverse unknown fields through wire data. + // When traversing wire data, be mindful of both packed[1] and delimited[2] encoding schemes. + // + // [1]: https://protobuf.dev/programming-guides/encoding/#packed + // [2]: https://protobuf.dev/programming-guides/encoding/#groups + // + // N.B.: Although groups are deprecated, the corresponding delimited encoding scheme is not, and + // can be explicitly used in Protocol Buffers 2023 Edition. + optional google.protobuf.FieldDescriptorProto.Type field_type = 3; + + // `key_type` specifies the map key type of this field. This value is useful when traversing + // unknown fields through wire data: specifically, it allows handling the differences between + // different integer encodings. + optional google.protobuf.FieldDescriptorProto.Type key_type = 4; + + // `value_type` specifies map value type of this field. This is useful if you want to display a + // value inside unknown fields through wire data. + optional google.protobuf.FieldDescriptorProto.Type value_type = 5; + + // `subscript` contains a repeated index or map key, if this path element nests into a repeated or map field. + oneof subscript { + // `index` specifies a 0-based index into a repeated field. + uint64 index = 6; + + // `bool_key` specifies a map key of type bool. + bool bool_key = 7; + + // `int_key` specifies a map key of type int32, int64, sint32, sint64, sfixed32 or sfixed64. + int64 int_key = 8; + + // `uint_key` specifies a map key of type uint32, uint64, fixed32 or fixed64. + uint64 uint_key = 9; + + // `string_key` specifies a map key of type string. + string string_key = 10; + } +} \ No newline at end of file diff --git a/third_party/errors/errors.proto b/third_party/errors/errors.proto new file mode 100644 index 0000000..2fb33d2 --- /dev/null +++ b/third_party/errors/errors.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package errors; + +option go_package = "github.com/go-kratos/kratos/v3/errors;errors"; +option java_multiple_files = true; +option java_package = "com.github.kratos.errors"; +option objc_class_prefix = "KratosErrors"; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.EnumOptions { + int32 default_code = 1108; +} + +extend google.protobuf.EnumValueOptions { + int32 code = 1109; +} diff --git a/third_party/google/api/annotations.proto b/third_party/google/api/annotations.proto new file mode 100644 index 0000000..85c361b --- /dev/null +++ b/third_party/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/third_party/google/api/client.proto b/third_party/google/api/client.proto new file mode 100644 index 0000000..d3d1ead --- /dev/null +++ b/third_party/google/api/client.proto @@ -0,0 +1,101 @@ +// Copyright 2019 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "ClientProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + + +extend google.protobuf.ServiceOptions { + // The hostname for this service. + // This should be specified with no prefix or protocol. + // + // Example: + // + // service Foo { + // option (google.api.default_host) = "foo.googleapi.com"; + // ... + // } + string default_host = 1049; + + // OAuth scopes needed for the client. + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform"; + // ... + // } + // + // If there is more than one scope, use a comma-separated string: + // + // Example: + // + // service Foo { + // option (google.api.oauth_scopes) = \ + // "https://www.googleapis.com/auth/cloud-platform," + // "https://www.googleapis.com/auth/monitoring"; + // ... + // } + string oauth_scopes = 1050; +} + + +extend google.protobuf.MethodOptions { + // A definition of a client library method signature. + // + // In client libraries, each proto RPC corresponds to one or more methods + // which the end user is able to call, and calls the underlying RPC. + // Normally, this method receives a single argument (a struct or instance + // corresponding to the RPC request object). Defining this field will + // add one or more overloads providing flattened or simpler method signatures + // in some languages. + // + // The fields on the method signature are provided as a comma-separated + // string. + // + // For example, the proto RPC and annotation: + // + // rpc CreateSubscription(CreateSubscriptionRequest) + // returns (Subscription) { + // option (google.api.method_signature) = "name,topic"; + // } + // + // Would add the following Java overload (in addition to the method accepting + // the request object): + // + // public final Subscription createSubscription(String name, String topic) + // + // The following backwards-compatibility guidelines apply: + // + // * Adding this annotation to an unannotated method is backwards + // compatible. + // * Adding this annotation to a method which already has existing + // method signature annotations is backwards compatible if and only if + // the new method signature annotation is last in the sequence. + // * Modifying or removing an existing method signature annotation is + // a breaking change. + // * Re-ordering existing method signature annotations is a breaking + // change. + repeated string method_signature = 1051; +} \ No newline at end of file diff --git a/third_party/google/api/field_behavior.proto b/third_party/google/api/field_behavior.proto new file mode 100644 index 0000000..e9716a2 --- /dev/null +++ b/third_party/google/api/field_behavior.proto @@ -0,0 +1,80 @@ +// Copyright 2019 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; +} + + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated FieldBehavior field_behavior = 1052; +} \ No newline at end of file diff --git a/third_party/google/api/http.proto b/third_party/google/api/http.proto new file mode 100644 index 0000000..69460cf --- /dev/null +++ b/third_party/google/api/http.proto @@ -0,0 +1,375 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/third_party/google/api/httpbody.proto b/third_party/google/api/httpbody.proto new file mode 100644 index 0000000..1a5bb78 --- /dev/null +++ b/third_party/google/api/httpbody.proto @@ -0,0 +1,77 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody"; +option java_multiple_files = true; +option java_outer_classname = "HttpBodyProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Message that represents an arbitrary HTTP body. It should only be used for +// payload formats that can't be represented as JSON, such as raw binary or +// an HTML page. +// +// +// This message can be used both in streaming and non-streaming API methods in +// the request as well as the response. +// +// It can be used as a top-level request field, which is convenient if one +// wants to extract parameters from either the URL or HTTP template into the +// request fields and also want access to the raw HTTP body. +// +// Example: +// +// message GetResourceRequest { +// // A unique request id. +// string request_id = 1; +// +// // The raw HTTP body is bound to this field. +// google.api.HttpBody http_body = 2; +// } +// +// service ResourceService { +// rpc GetResource(GetResourceRequest) returns (google.api.HttpBody); +// rpc UpdateResource(google.api.HttpBody) returns +// (google.protobuf.Empty); +// } +// +// Example with streaming methods: +// +// service CaldavService { +// rpc GetCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// rpc UpdateCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// } +// +// Use of this type only changes how the request and response bodies are +// handled, all other features will continue to work unchanged. +message HttpBody { + // The HTTP Content-Type header value specifying the content type of the body. + string content_type = 1; + + // The HTTP request/response body as raw binary. + bytes data = 2; + + // Application specific response metadata. Must be set in the first response + // for streaming APIs. + repeated google.protobuf.Any extensions = 3; +} diff --git a/third_party/google/protobuf/descriptor.proto b/third_party/google/protobuf/descriptor.proto new file mode 100644 index 0000000..ec568c3 --- /dev/null +++ b/third_party/google/protobuf/descriptor.proto @@ -0,0 +1,911 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + + +syntax = "proto2"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/descriptorpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + // Indexes of the public imported files in the dependency list above. + repeated int32 public_dependency = 10; + // Indexes of the weak imported files in the dependency list. + // For Google-internal migration only. Do not use. + repeated int32 weak_dependency = 11; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field without harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; + + // The syntax of the proto file. + // The supported values are "proto2" and "proto3". + optional string syntax = 12; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + + optional ExtensionRangeOptions options = 3; + } + repeated ExtensionRange extension_range = 5; + + repeated OneofDescriptorProto oneof_decl = 8; + + optional MessageOptions options = 7; + + // Range of reserved tag numbers. Reserved tag numbers may not be used by + // fields or extension ranges in the same message. Reserved ranges may + // not overlap. + message ReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + } + repeated ReservedRange reserved_range = 9; + // Reserved field names, which may not be used by fields in the same message. + // A given name may only be reserved once. + repeated string reserved_name = 10; +} + +message ExtensionRangeOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + // negative values are likely. + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + // negative values are likely. + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + // Tag-delimited aggregate. + // Group type is deprecated and not supported in proto3. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + } + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REQUIRED = 2; + LABEL_REPEATED = 3; + } + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + // TODO(kenton): Base-64 encode? + optional string default_value = 7; + + // If set, gives the index of a oneof in the containing type's oneof_decl + // list. This field is a member of that oneof. + optional int32 oneof_index = 9; + + // JSON name of this field. The value is set by protocol compiler. If the + // user has set a "json_name" option on this field, that option's value + // will be used. Otherwise, it's deduced from the field's name by converting + // it to camelCase. + optional string json_name = 10; + + optional FieldOptions options = 8; + + // If true, this is a proto3 "optional". When a proto3 field is optional, it + // tracks presence regardless of field type. + // + // When proto3_optional is true, this field must be belong to a oneof to + // signal to old proto3 clients that presence is tracked for this field. This + // oneof is known as a "synthetic" oneof, and this field must be its sole + // member (each proto3 optional field gets its own synthetic oneof). Synthetic + // oneofs exist in the descriptor only, and do not generate any API. Synthetic + // oneofs must be ordered after all "real" oneofs. + // + // For message fields, proto3_optional doesn't create any semantic change, + // since non-repeated message fields always track presence. However it still + // indicates the semantic detail of whether the user wrote "optional" or not. + // This can be useful for round-tripping the .proto file. For consistency we + // give message fields a synthetic oneof also, even though it is not required + // to track presence. This is especially important because the parser can't + // tell if a field is a message or an enum, so it must always create a + // synthetic oneof. + // + // Proto2 optional fields do not set this flag, because they already indicate + // optional with `LABEL_OPTIONAL`. + optional bool proto3_optional = 17; +} + +// Describes a oneof. +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; + + // Range of reserved numeric values. Reserved values may not be used by + // entries in the same enum. Reserved ranges may not overlap. + // + // Note that this is distinct from DescriptorProto.ReservedRange in that it + // is inclusive such that it can appropriately represent the entire int32 + // domain. + message EnumReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Inclusive. + } + + // Range of reserved numeric values. Reserved numeric values may not be used + // by enum values in the same enum declaration. Reserved ranges may not + // overlap. + repeated EnumReservedRange reserved_range = 4; + + // Reserved enum value names, which may not be reused. A given name may only + // be reserved once. + repeated string reserved_name = 5; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; + + // Identifies if client streams multiple client messages + optional bool client_streaming = 5 [default = false]; + // Identifies if server streams multiple server messages + optional bool server_streaming = 6 [default = false]; +} + + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail protobuf-global-extension-registry@google.com +// to reserve extension numbers. Simply provide your project name (e.g. +// Objective-C plugin) and your project website (if available) -- there's no +// need to explain how you intend to use them. Usually you only need one +// extension number. You can declare multiple options with only one extension +// number by putting them in a sub-message. See the Custom Options section of +// the docs for examples: +// https://developers.google.com/protocol-buffers/docs/proto#options +// If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + + // Controls the name of the wrapper Java class generated for the .proto file. + // That class will always contain the .proto file's getDescriptor() method as + // well as any top-level extensions defined in the .proto file. + // If java_multiple_files is disabled, then all the other classes from the + // .proto file will be nested inside the single wrapper outer class. + optional string java_outer_classname = 8; + + // If enabled, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the wrapper class + // named by java_outer_classname. However, the wrapper class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [default = false]; + + // This option does nothing. + optional bool java_generate_equals_and_hash = 20 [deprecated=true]; + + // If set true, then the Java2 code generator will generate code that + // throws an exception whenever an attempt is made to assign a non-UTF-8 + // byte sequence to a string field. + // Message reflection will do the same. + // However, an extension field still accepts non-UTF-8 byte sequences. + // This option has no effect on when used with the lite runtime. + optional bool java_string_check_utf8 = 27 [default = false]; + + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default = SPEED]; + + // Sets the Go package where structs generated from this .proto will be + // placed. If omitted, the Go package will be derived from the following: + // - The basename of the package import path, if provided. + // - Otherwise, the package statement in the .proto file, if present. + // - Otherwise, the basename of the .proto file, without extension. + optional string go_package = 11; + + + + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of google.protobuf. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default = false]; + optional bool java_generic_services = 17 [default = false]; + optional bool py_generic_services = 18 [default = false]; + optional bool php_generic_services = 42 [default = false]; + + // Is this file deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for everything in the file, or it will be completely ignored; in the very + // least, this is a formalization for deprecating files. + optional bool deprecated = 23 [default = false]; + + // Enables the use of arenas for the proto messages in this file. This applies + // only to generated classes for C++. + optional bool cc_enable_arenas = 31 [default = true]; + + + // Sets the objective c class prefix which is prepended to all objective c + // generated classes from this .proto. There is no default. + optional string objc_class_prefix = 36; + + // Namespace for generated classes; defaults to the package. + optional string csharp_namespace = 37; + + // By default Swift generators will take the proto package and CamelCase it + // replacing '.' with underscore and use that to prefix the types/symbols + // defined. When this options is provided, they will use this value instead + // to prefix the types/symbols defined. + optional string swift_prefix = 39; + + // Sets the php class prefix which is prepended to all php generated classes + // from this .proto. Default is empty. + optional string php_class_prefix = 40; + + // Use this option to change the namespace of php generated classes. Default + // is empty. When this option is empty, the package name will be used for + // determining the namespace. + optional string php_namespace = 41; + + // Use this option to change the namespace of php generated metadata classes. + // Default is empty. When this option is empty, the proto file name will be + // used for determining the namespace. + optional string php_metadata_namespace = 44; + + // Use this option to change the package of ruby generated classes. Default + // is empty. When this option is not set, the package name will be used for + // determining the ruby package. + optional string ruby_package = 45; + + + // The parser stores options it doesn't recognize here. + // See the documentation for the "Options" section above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. + // See the documentation for the "Options" section above. + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default = false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default = false]; + + // Is this message deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the message, or it will be completely ignored; in the very least, + // this is a formalization for deprecating messages. + optional bool deprecated = 3 [default = false]; + + reserved 4, 5, 6; + + // Whether the message is an automatically generated map entry type for the + // maps field. + // + // For maps fields: + // map map_field = 1; + // The parsed descriptor looks like: + // message MapFieldEntry { + // option map_entry = true; + // optional KeyType key = 1; + // optional ValueType value = 2; + // } + // repeated MapFieldEntry map_field = 1; + // + // Implementations may choose not to generate the map_entry=true message, but + // use a native map in the target language to hold the keys and values. + // The reflection APIs in such implementations still need to work as + // if the field is a repeated message field. + // + // NOTE: Do not set the option in .proto files. Always use the maps syntax + // instead. The option should only be implicitly set by the proto compiler + // parser. + optional bool map_entry = 7; + + reserved 8; // javalite_serializable + reserved 9; // javanano_as_lite + + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is not yet implemented in the open source + // release -- sorry, we'll try to include it in a future version! + optional CType ctype = 1 [default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. In proto3, only explicit setting it to + // false will avoid using packed encoding. + optional bool packed = 2; + + // The jstype option determines the JavaScript type used for values of the + // field. The option is permitted only for 64 bit integral and fixed types + // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + // is represented as JavaScript string, which avoids loss of precision that + // can happen when a large value is converted to a floating point JavaScript. + // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + // use the JavaScript "number" type. The behavior of the default option + // JS_NORMAL is implementation dependent. + // + // This option is an enum to permit additional types to be added, e.g. + // goog.math.Integer. + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + // Use the default type. + JS_NORMAL = 0; + + // Use JavaScript strings. + JS_STRING = 1; + + // Use JavaScript numbers. + JS_NUMBER = 2; + } + + // Should this field be parsed lazily? Lazy applies only to message-type + // fields. It means that when the outer message is initially parsed, the + // inner message's contents will not be parsed but instead stored in encoded + // form. The inner message will actually be parsed when it is first accessed. + // + // This is only a hint. Implementations are free to choose whether to use + // eager or lazy parsing regardless of the value of this option. However, + // setting this option true suggests that the protocol author believes that + // using lazy parsing on this field is worth the additional bookkeeping + // overhead typically needed to implement it. + // + // This option does not affect the public interface of any generated code; + // all method signatures remain the same. Furthermore, thread-safety of the + // interface is not affected by this option; const methods remain safe to + // call from multiple threads concurrently, while non-const methods continue + // to require exclusive access. + // + // + // Note that implementations may choose not to check required fields within + // a lazy sub-message. That is, calling IsInitialized() on the outer message + // may return true even if the inner message has missing required fields. + // This is necessary because otherwise the inner message would have to be + // parsed in order to perform the check, defeating the purpose of lazy + // parsing. An implementation which chooses not to check required fields + // must be consistent about it. That is, for any particular sub-message, the + // implementation must either *always* check its required fields, or *never* + // check its required fields, regardless of whether or not the message has + // been parsed. + optional bool lazy = 5 [default = false]; + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default = false]; + + // For Google-internal migration only. Do not use. + optional bool weak = 10 [default = false]; + + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; + + reserved 4; // removed jtype +} + +message OneofOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // Set this option to true to allow mapping different tag names to the same + // value. + optional bool allow_alias = 2; + + // Is this enum deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum, or it will be completely ignored; in the very least, this + // is a formalization for deprecating enums. + optional bool deprecated = 3 [default = false]; + + reserved 5; // javanano_as_lite + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // Is this enum value deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum value, or it will be completely ignored; in the very least, + // this is a formalization for deprecating enum values. + optional bool deprecated = 1 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this service deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the service, or it will be completely ignored; in the very least, + // this is a formalization for deprecating services. + optional bool deprecated = 33 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this method deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the method, or it will be completely ignored; in the very least, + // this is a formalization for deprecating methods. + optional bool deprecated = 33 [default = false]; + + // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + // or neither? HTTP based RPC implementation may choose GET verb for safe + // methods, and PUT verb for idempotent methods instead of the default POST. + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; // implies idempotent + IDEMPOTENT = 2; // idempotent, but may have side effects + } + optional IdempotencyLevel idempotency_level = 34 + [default = IDEMPOTENCY_UNKNOWN]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents + // "foo.(bar.baz).qux". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendant. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition occurs. For + // example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed = true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed = true]; + + // If this SourceCodeInfo represents a complete declaration, these are any + // comments appearing before and after the declaration which appear to be + // attached to the declaration. + // + // A series of line comments appearing on consecutive lines, with no other + // tokens appearing on those lines, will be treated as a single comment. + // + // leading_detached_comments will keep paragraphs of comments that appear + // before (but not connected to) the current element. Each paragraph, + // separated by empty lines, will be one comment element in the repeated + // field. + // + // Only the comment content is provided; comment markers (e.g. //) are + // stripped out. For block comments, leading whitespace and an asterisk + // will be stripped from the beginning of each line other than the first. + // Newlines are included in the output. + // + // Examples: + // + // optional int32 foo = 1; // Comment attached to foo. + // // Comment attached to bar. + // optional int32 bar = 2; + // + // optional string baz = 3; + // // Comment attached to baz. + // // Another line attached to baz. + // + // // Comment attached to qux. + // // + // // Another line attached to qux. + // optional double qux = 4; + // + // // Detached comment for corge. This is not leading or trailing comments + // // to qux or corge because there are blank lines separating it from + // // both. + // + // // Detached comment for corge paragraph 2. + // + // optional string corge = 5; + // /* Block comment attached + // * to corge. Leading asterisks + // * will be removed. */ + // /* Block comment attached to + // * grault. */ + // optional int32 grault = 6; + // + // // ignored detached comments. + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } +} + +// Describes the relationship between generated code and its original source +// file. A GeneratedCodeInfo message is associated with only one generated +// source file, but may contain references to different source .proto files. +message GeneratedCodeInfo { + // An Annotation connects some span of text in generated code to an element + // of its generating .proto file. + repeated Annotation annotation = 1; + message Annotation { + // Identifies the element in the original source .proto file. This field + // is formatted the same as SourceCodeInfo.Location.path. + repeated int32 path = 1 [packed = true]; + + // Identifies the filesystem path to the original source .proto. + optional string source_file = 2; + + // Identifies the starting offset in bytes in the generated code + // that relates to the identified object. + optional int32 begin = 3; + + // Identifies the ending offset in bytes in the generated code that + // relates to the identified offset. The end offset should be one past + // the last relevant byte (so the length of the text = end - begin). + optional int32 end = 4; + } +} \ No newline at end of file diff --git a/third_party/validate/README.md b/third_party/validate/README.md new file mode 100644 index 0000000..56698db --- /dev/null +++ b/third_party/validate/README.md @@ -0,0 +1,3 @@ +# protoc-gen-validate (PGV) + +* https://github.com/envoyproxy/protoc-gen-validate diff --git a/third_party/validate/validate.proto b/third_party/validate/validate.proto new file mode 100644 index 0000000..6b65ce3 --- /dev/null +++ b/third_party/validate/validate.proto @@ -0,0 +1,863 @@ +syntax = "proto2"; +package validate; + +option go_package = "github.com/envoyproxy/protoc-gen-validate/validate"; +option java_package = "io.envoyproxy.pgv.validate"; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +// Validation rules applied at the message level +extend google.protobuf.MessageOptions { + // Disabled nullifies any validation rules for this message, including any + // message fields associated with it that do support validation. + optional bool disabled = 1071; + // Ignore skips generation of validation methods for this message. + optional bool ignored = 1072; +} + +// Validation rules applied at the oneof level +extend google.protobuf.OneofOptions { + // Required ensures that exactly one the field options in a oneof is set; + // validation fails if no fields in the oneof are set. + optional bool required = 1071; +} + +// Validation rules applied at the field level +extend google.protobuf.FieldOptions { + // Rules specify the validations to be performed on this field. By default, + // no validation is performed against a field. + optional FieldRules rules = 1071; +} + +// FieldRules encapsulates the rules for each type of field. Depending on the +// field, the correct set should be used to ensure proper validations. +message FieldRules { + optional MessageRules message = 17; + oneof type { + // Scalar Field Types + FloatRules float = 1; + DoubleRules double = 2; + Int32Rules int32 = 3; + Int64Rules int64 = 4; + UInt32Rules uint32 = 5; + UInt64Rules uint64 = 6; + SInt32Rules sint32 = 7; + SInt64Rules sint64 = 8; + Fixed32Rules fixed32 = 9; + Fixed64Rules fixed64 = 10; + SFixed32Rules sfixed32 = 11; + SFixed64Rules sfixed64 = 12; + BoolRules bool = 13; + StringRules string = 14; + BytesRules bytes = 15; + + // Complex Field Types + EnumRules enum = 16; + RepeatedRules repeated = 18; + MapRules map = 19; + + // Well-Known Field Types + AnyRules any = 20; + DurationRules duration = 21; + TimestampRules timestamp = 22; + } +} + +// FloatRules describes the constraints applied to `float` values +message FloatRules { + // Const specifies that this field must be exactly the specified value + optional float const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional float lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional float lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional float gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional float gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated float in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated float not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// DoubleRules describes the constraints applied to `double` values +message DoubleRules { + // Const specifies that this field must be exactly the specified value + optional double const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional double lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional double lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional double gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional double gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated double in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated double not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// Int32Rules describes the constraints applied to `int32` values +message Int32Rules { + // Const specifies that this field must be exactly the specified value + optional int32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional int32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional int32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional int32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional int32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated int32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// Int64Rules describes the constraints applied to `int64` values +message Int64Rules { + // Const specifies that this field must be exactly the specified value + optional int64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional int64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional int64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional int64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional int64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated int64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// UInt32Rules describes the constraints applied to `uint32` values +message UInt32Rules { + // Const specifies that this field must be exactly the specified value + optional uint32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional uint32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional uint32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional uint32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional uint32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated uint32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated uint32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// UInt64Rules describes the constraints applied to `uint64` values +message UInt64Rules { + // Const specifies that this field must be exactly the specified value + optional uint64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional uint64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional uint64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional uint64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional uint64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated uint64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated uint64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// SInt32Rules describes the constraints applied to `sint32` values +message SInt32Rules { + // Const specifies that this field must be exactly the specified value + optional sint32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sint32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sint32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sint32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sint32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sint32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sint32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// SInt64Rules describes the constraints applied to `sint64` values +message SInt64Rules { + // Const specifies that this field must be exactly the specified value + optional sint64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sint64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sint64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sint64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sint64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sint64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sint64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// Fixed32Rules describes the constraints applied to `fixed32` values +message Fixed32Rules { + // Const specifies that this field must be exactly the specified value + optional fixed32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional fixed32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional fixed32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional fixed32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional fixed32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated fixed32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated fixed32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// Fixed64Rules describes the constraints applied to `fixed64` values +message Fixed64Rules { + // Const specifies that this field must be exactly the specified value + optional fixed64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional fixed64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional fixed64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional fixed64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional fixed64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated fixed64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated fixed64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// SFixed32Rules describes the constraints applied to `sfixed32` values +message SFixed32Rules { + // Const specifies that this field must be exactly the specified value + optional sfixed32 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sfixed32 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sfixed32 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sfixed32 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sfixed32 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sfixed32 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sfixed32 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// SFixed64Rules describes the constraints applied to `sfixed64` values +message SFixed64Rules { + // Const specifies that this field must be exactly the specified value + optional sfixed64 const = 1; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional sfixed64 lt = 2; + + // Lte specifies that this field must be less than or equal to the + // specified value, inclusive + optional sfixed64 lte = 3; + + // Gt specifies that this field must be greater than the specified value, + // exclusive. If the value of Gt is larger than a specified Lt or Lte, the + // range is reversed. + optional sfixed64 gt = 4; + + // Gte specifies that this field must be greater than or equal to the + // specified value, inclusive. If the value of Gte is larger than a + // specified Lt or Lte, the range is reversed. + optional sfixed64 gte = 5; + + // In specifies that this field must be equal to one of the specified + // values + repeated sfixed64 in = 6; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated sfixed64 not_in = 7; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 8; +} + +// BoolRules describes the constraints applied to `bool` values +message BoolRules { + // Const specifies that this field must be exactly the specified value + optional bool const = 1; +} + +// StringRules describe the constraints applied to `string` values +message StringRules { + // Const specifies that this field must be exactly the specified value + optional string const = 1; + + // Len specifies that this field must be the specified number of + // characters (Unicode code points). Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 len = 19; + + // MinLen specifies that this field must be the specified number of + // characters (Unicode code points) at a minimum. Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 min_len = 2; + + // MaxLen specifies that this field must be the specified number of + // characters (Unicode code points) at a maximum. Note that the number of + // characters may differ from the number of bytes in the string. + optional uint64 max_len = 3; + + // LenBytes specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 len_bytes = 20; + + // MinBytes specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 min_bytes = 4; + + // MaxBytes specifies that this field must be the specified number of bytes + // at a maximum + optional uint64 max_bytes = 5; + + // Pattern specifies that this field must match against the specified + // regular expression (RE2 syntax). The included expression should elide + // any delimiters. + optional string pattern = 6; + + // Prefix specifies that this field must have the specified substring at + // the beginning of the string. + optional string prefix = 7; + + // Suffix specifies that this field must have the specified substring at + // the end of the string. + optional string suffix = 8; + + // Contains specifies that this field must have the specified substring + // anywhere in the string. + optional string contains = 9; + + // NotContains specifies that this field cannot have the specified substring + // anywhere in the string. + optional string not_contains = 23; + + // In specifies that this field must be equal to one of the specified + // values + repeated string in = 10; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated string not_in = 11; + + // WellKnown rules provide advanced constraints against common string + // patterns + oneof well_known { + // Email specifies that the field must be a valid email address as + // defined by RFC 5322 + bool email = 12; + + // Hostname specifies that the field must be a valid hostname as + // defined by RFC 1034. This constraint does not support + // internationalized domain names (IDNs). + bool hostname = 13; + + // Ip specifies that the field must be a valid IP (v4 or v6) address. + // Valid IPv6 addresses should not include surrounding square brackets. + bool ip = 14; + + // Ipv4 specifies that the field must be a valid IPv4 address. + bool ipv4 = 15; + + // Ipv6 specifies that the field must be a valid IPv6 address. Valid + // IPv6 addresses should not include surrounding square brackets. + bool ipv6 = 16; + + // Uri specifies that the field must be a valid, absolute URI as defined + // by RFC 3986 + bool uri = 17; + + // UriRef specifies that the field must be a valid URI as defined by RFC + // 3986 and may be relative or absolute. + bool uri_ref = 18; + + // Address specifies that the field must be either a valid hostname as + // defined by RFC 1034 (which does not support internationalized domain + // names or IDNs), or it can be a valid IP (v4 or v6). + bool address = 21; + + // Uuid specifies that the field must be a valid UUID as defined by + // RFC 4122 + bool uuid = 22; + + // WellKnownRegex specifies a common well known pattern defined as a regex. + KnownRegex well_known_regex = 24; + } + + // This applies to regexes HTTP_HEADER_NAME and HTTP_HEADER_VALUE to enable + // strict header validation. + // By default, this is true, and HTTP header validations are RFC-compliant. + // Setting to false will enable a looser validations that only disallows + // \r\n\0 characters, which can be used to bypass header matching rules. + optional bool strict = 25 [default = true]; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 26; +} + +// WellKnownRegex contain some well-known patterns. +enum KnownRegex { + UNKNOWN = 0; + + // HTTP header name as defined by RFC 7230. + HTTP_HEADER_NAME = 1; + + // HTTP header value as defined by RFC 7230. + HTTP_HEADER_VALUE = 2; +} + +// BytesRules describe the constraints applied to `bytes` values +message BytesRules { + // Const specifies that this field must be exactly the specified value + optional bytes const = 1; + + // Len specifies that this field must be the specified number of bytes + optional uint64 len = 13; + + // MinLen specifies that this field must be the specified number of bytes + // at a minimum + optional uint64 min_len = 2; + + // MaxLen specifies that this field must be the specified number of bytes + // at a maximum + optional uint64 max_len = 3; + + // Pattern specifies that this field must match against the specified + // regular expression (RE2 syntax). The included expression should elide + // any delimiters. + optional string pattern = 4; + + // Prefix specifies that this field must have the specified bytes at the + // beginning of the string. + optional bytes prefix = 5; + + // Suffix specifies that this field must have the specified bytes at the + // end of the string. + optional bytes suffix = 6; + + // Contains specifies that this field must have the specified bytes + // anywhere in the string. + optional bytes contains = 7; + + // In specifies that this field must be equal to one of the specified + // values + repeated bytes in = 8; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated bytes not_in = 9; + + // WellKnown rules provide advanced constraints against common byte + // patterns + oneof well_known { + // Ip specifies that the field must be a valid IP (v4 or v6) address in + // byte format + bool ip = 10; + + // Ipv4 specifies that the field must be a valid IPv4 address in byte + // format + bool ipv4 = 11; + + // Ipv6 specifies that the field must be a valid IPv6 address in byte + // format + bool ipv6 = 12; + } + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 14; +} + +// EnumRules describe the constraints applied to enum values +message EnumRules { + // Const specifies that this field must be exactly the specified value + optional int32 const = 1; + + // DefinedOnly specifies that this field must be only one of the defined + // values for this enum, failing on any undefined value. + optional bool defined_only = 2; + + // In specifies that this field must be equal to one of the specified + // values + repeated int32 in = 3; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated int32 not_in = 4; +} + +// MessageRules describe the constraints applied to embedded message values. +// For message-type fields, validation is performed recursively. +message MessageRules { + // Skip specifies that the validation rules of this field should not be + // evaluated + optional bool skip = 1; + + // Required specifies that this field must be set + optional bool required = 2; +} + +// RepeatedRules describe the constraints applied to `repeated` values +message RepeatedRules { + // MinItems specifies that this field must have the specified number of + // items at a minimum + optional uint64 min_items = 1; + + // MaxItems specifies that this field must have the specified number of + // items at a maximum + optional uint64 max_items = 2; + + // Unique specifies that all elements in this field must be unique. This + // constraint is only applicable to scalar and enum types (messages are not + // supported). + optional bool unique = 3; + + // Items specifies the constraints to be applied to each item in the field. + // Repeated message fields will still execute validation against each item + // unless skip is specified here. + optional FieldRules items = 4; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 5; +} + +// MapRules describe the constraints applied to `map` values +message MapRules { + // MinPairs specifies that this field must have the specified number of + // KVs at a minimum + optional uint64 min_pairs = 1; + + // MaxPairs specifies that this field must have the specified number of + // KVs at a maximum + optional uint64 max_pairs = 2; + + // NoSparse specifies values in this field cannot be unset. This only + // applies to map's with message value types. + optional bool no_sparse = 3; + + // Keys specifies the constraints to be applied to each key in the field. + optional FieldRules keys = 4; + + // Values specifies the constraints to be applied to the value of each key + // in the field. Message values will still have their validations evaluated + // unless skip is specified here. + optional FieldRules values = 5; + + // IgnoreEmpty specifies that the validation rules of this field should be + // evaluated only if the field is not empty + optional bool ignore_empty = 6; +} + +// AnyRules describe constraints applied exclusively to the +// `google.protobuf.Any` well-known type +message AnyRules { + // Required specifies that this field must be set + optional bool required = 1; + + // In specifies that this field's `type_url` must be equal to one of the + // specified values. + repeated string in = 2; + + // NotIn specifies that this field's `type_url` must not be equal to any of + // the specified values. + repeated string not_in = 3; +} + +// DurationRules describe the constraints applied exclusively to the +// `google.protobuf.Duration` well-known type +message DurationRules { + // Required specifies that this field must be set + optional bool required = 1; + + // Const specifies that this field must be exactly the specified value + optional google.protobuf.Duration const = 2; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional google.protobuf.Duration lt = 3; + + // Lt specifies that this field must be less than the specified value, + // inclusive + optional google.protobuf.Duration lte = 4; + + // Gt specifies that this field must be greater than the specified value, + // exclusive + optional google.protobuf.Duration gt = 5; + + // Gte specifies that this field must be greater than the specified value, + // inclusive + optional google.protobuf.Duration gte = 6; + + // In specifies that this field must be equal to one of the specified + // values + repeated google.protobuf.Duration in = 7; + + // NotIn specifies that this field cannot be equal to one of the specified + // values + repeated google.protobuf.Duration not_in = 8; +} + +// TimestampRules describe the constraints applied exclusively to the +// `google.protobuf.Timestamp` well-known type +message TimestampRules { + // Required specifies that this field must be set + optional bool required = 1; + + // Const specifies that this field must be exactly the specified value + optional google.protobuf.Timestamp const = 2; + + // Lt specifies that this field must be less than the specified value, + // exclusive + optional google.protobuf.Timestamp lt = 3; + + // Lte specifies that this field must be less than the specified value, + // inclusive + optional google.protobuf.Timestamp lte = 4; + + // Gt specifies that this field must be greater than the specified value, + // exclusive + optional google.protobuf.Timestamp gt = 5; + + // Gte specifies that this field must be greater than the specified value, + // inclusive + optional google.protobuf.Timestamp gte = 6; + + // LtNow specifies that this must be less than the current time. LtNow + // can only be used with the Within rule. + optional bool lt_now = 7; + + // GtNow specifies that this must be greater than the current time. GtNow + // can only be used with the Within rule. + optional bool gt_now = 8; + + // Within specifies that this field must be within this duration of the + // current time. This constraint can be used alone or with the LtNow and + // GtNow rules. + optional google.protobuf.Duration within = 9; +} diff --git a/transport/grpc/balancer.go b/transport/grpc/balancer.go new file mode 100644 index 0000000..bb54ffb --- /dev/null +++ b/transport/grpc/balancer.go @@ -0,0 +1,105 @@ +package grpc + +import ( + "google.golang.org/grpc/balancer" + "google.golang.org/grpc/balancer/base" + "google.golang.org/grpc/metadata" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/transport" +) + +const ( + balancerName = "selector" +) + +var ( + _ base.PickerBuilder = (*balancerBuilder)(nil) + _ balancer.Picker = (*balancerPicker)(nil) +) + +func init() { + b := base.NewBalancerBuilder( + balancerName, + &balancerBuilder{ + builder: selector.GlobalSelector(), + }, + base.Config{HealthCheck: true}, + ) + balancer.Register(b) +} + +type balancerBuilder struct { + builder selector.Builder +} + +// Build creates a grpc Picker. +func (b *balancerBuilder) Build(info base.PickerBuildInfo) balancer.Picker { + if len(info.ReadySCs) == 0 { + // Block the RPC until a new picker is available via UpdateState(). + return base.NewErrPicker(balancer.ErrNoSubConnAvailable) + } + nodes := make([]selector.Node, 0, len(info.ReadySCs)) + for conn, info := range info.ReadySCs { + ins, _ := info.Address.Attributes.Value("rawServiceInstance").(*registry.ServiceInstance) + nodes = append(nodes, &grpcNode{ + Node: selector.NewNode("grpc", info.Address.Addr, ins), + subConn: conn, + }) + } + p := &balancerPicker{ + selector: b.builder.Build(), + } + p.selector.Apply(nodes) + return p +} + +// balancerPicker is a grpc picker. +type balancerPicker struct { + selector selector.Selector +} + +// Pick pick instances. +func (p *balancerPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { + var filters []selector.NodeFilter + if tr, ok := transport.FromClientContext(info.Ctx); ok { + if gtr, ok := tr.(*Transport); ok { + filters = gtr.NodeFilters() + } + } + + n, done, err := p.selector.Select(info.Ctx, selector.WithNodeFilter(filters...)) + if err != nil { + return balancer.PickResult{}, err + } + + return balancer.PickResult{ + SubConn: n.(*grpcNode).subConn, + Done: func(di balancer.DoneInfo) { + done(info.Ctx, selector.DoneInfo{ + Err: di.Err, + BytesSent: di.BytesSent, + BytesReceived: di.BytesReceived, + ReplyMD: Trailer(di.Trailer), + }) + }, + }, nil +} + +// Trailer is a grpc trailer MD. +type Trailer metadata.MD + +// Get get a grpc trailer value. +func (t Trailer) Get(k string) string { + v := metadata.MD(t).Get(k) + if len(v) > 0 { + return v[0] + } + return "" +} + +type grpcNode struct { + selector.Node + subConn balancer.SubConn +} diff --git a/transport/grpc/balancer_test.go b/transport/grpc/balancer_test.go new file mode 100644 index 0000000..4d66d0b --- /dev/null +++ b/transport/grpc/balancer_test.go @@ -0,0 +1,32 @@ +package grpc + +import ( + "context" + "reflect" + "testing" + + "google.golang.org/grpc/metadata" + + "github.com/go-kratos/kratos/v3/selector" +) + +func TestTrailer(t *testing.T) { + trailer := Trailer(metadata.New(map[string]string{"a": "b"})) + if !reflect.DeepEqual("b", trailer.Get("a")) { + t.Errorf("expect %v, got %v", "b", trailer.Get("a")) + } + if !reflect.DeepEqual("", trailer.Get("notfound")) { + t.Errorf("expect %v, got %v", "", trailer.Get("notfound")) + } +} + +func TestFilters(t *testing.T) { + o := &clientOptions{} + + WithNodeFilter(func(_ context.Context, nodes []selector.Node) []selector.Node { + return nodes + })(o) + if !reflect.DeepEqual(1, len(o.filters)) { + t.Errorf("expect %v, got %v", 1, len(o.filters)) + } +} diff --git a/transport/grpc/client.go b/transport/grpc/client.go new file mode 100644 index 0000000..ea9f61b --- /dev/null +++ b/transport/grpc/client.go @@ -0,0 +1,316 @@ +package grpc + +import ( + "context" + "crypto/tls" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + grpcinsecure "google.golang.org/grpc/credentials/insecure" + grpcmd "google.golang.org/grpc/metadata" + + "github.com/go-kratos/kratos/v3/internal/matcher" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/wrr" + "github.com/go-kratos/kratos/v3/transport" + "github.com/go-kratos/kratos/v3/transport/grpc/resolver/discovery" + + // init resolver + _ "github.com/go-kratos/kratos/v3/transport/grpc/resolver/direct" +) + +func init() { + if selector.GlobalSelector() == nil { + selector.SetGlobalSelector(wrr.NewBuilder()) + } +} + +// ClientOption is gRPC client option. +type ClientOption func(o *clientOptions) + +// WithEndpoint with client endpoint. +func WithEndpoint(endpoint string) ClientOption { + return func(o *clientOptions) { + o.endpoint = endpoint + } +} + +// WithSubset with client discovery subset size. +// zero value means subset filter disabled +func WithSubset(size int) ClientOption { + return func(o *clientOptions) { + o.subsetSize = size + } +} + +// WithTimeout with client timeout. +func WithTimeout(timeout time.Duration) ClientOption { + return func(o *clientOptions) { + o.timeout = timeout + } +} + +// WithMiddleware with client middleware. +func WithMiddleware(m ...middleware.Middleware) ClientOption { + return func(o *clientOptions) { + o.middleware = m + } +} + +// WithStreamMiddleware with client stream middleware. +func WithStreamMiddleware(m ...middleware.Middleware) ClientOption { + return func(o *clientOptions) { + o.streamMiddleware = m + } +} + +// WithDiscovery with client discovery. +func WithDiscovery(d registry.Discovery) ClientOption { + return func(o *clientOptions) { + o.discovery = d + } +} + +// WithTLSConfig with TLS config. +func WithTLSConfig(c *tls.Config) ClientOption { + return func(o *clientOptions) { + o.tlsConf = c + } +} + +// WithUnaryInterceptor returns a ClientOption that specifies the interceptor for unary RPCs. +func WithUnaryInterceptor(in ...grpc.UnaryClientInterceptor) ClientOption { + return func(o *clientOptions) { + o.ints = in + } +} + +// WithStreamInterceptor returns a ClientOption that specifies the interceptor for streaming RPCs. +func WithStreamInterceptor(in ...grpc.StreamClientInterceptor) ClientOption { + return func(o *clientOptions) { + o.streamInts = in + } +} + +// WithOptions with gRPC options. +func WithOptions(opts ...grpc.DialOption) ClientOption { + return func(o *clientOptions) { + o.grpcOpts = opts + } +} + +// WithNodeFilter with select filters +func WithNodeFilter(filters ...selector.NodeFilter) ClientOption { + return func(o *clientOptions) { + o.filters = filters + } +} + +// WithHealthCheck with health check +func WithHealthCheck(healthCheck bool) ClientOption { + return func(o *clientOptions) { + if !healthCheck { + o.healthCheckConfig = "" + } + } +} + +// clientOptions is gRPC Client +type clientOptions struct { + endpoint string + subsetSize int + tlsConf *tls.Config + timeout time.Duration + discovery registry.Discovery + middleware []middleware.Middleware + streamMiddleware []middleware.Middleware + ints []grpc.UnaryClientInterceptor + streamInts []grpc.StreamClientInterceptor + grpcOpts []grpc.DialOption + balancerName string + filters []selector.NodeFilter + healthCheckConfig string +} + +// NewClient returns a gRPC client connection. +func NewClient(ctx context.Context, opts ...ClientOption) (*grpc.ClientConn, error) { + options := clientOptions{ + timeout: 2000 * time.Millisecond, + balancerName: balancerName, + subsetSize: 25, + healthCheckConfig: `,"healthCheckConfig":{"serviceName":""}`, + } + for _, o := range opts { + o(&options) + } + if err := ctx.Err(); err != nil { + return nil, err + } + isInsecure := options.tlsConf == nil + ints := []grpc.UnaryClientInterceptor{ + unaryClientInterceptor(options.middleware, options.timeout, options.filters), + } + sints := []grpc.StreamClientInterceptor{ + streamClientInterceptor(options.streamMiddleware, options.filters), + } + + if len(options.ints) > 0 { + ints = append(ints, options.ints...) + } + if len(options.streamInts) > 0 { + sints = append(sints, options.streamInts...) + } + grpcOpts := []grpc.DialOption{ + grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]%s}`, + options.balancerName, options.healthCheckConfig)), + grpc.WithChainUnaryInterceptor(ints...), + grpc.WithChainStreamInterceptor(sints...), + } + + if options.discovery != nil { + grpcOpts = append(grpcOpts, + grpc.WithResolvers( + discovery.NewBuilder( + options.discovery, + discovery.WithInsecure(isInsecure), + discovery.WithTimeout(options.timeout), + discovery.WithSubset(options.subsetSize), + ))) + } + if isInsecure { + grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(grpcinsecure.NewCredentials())) + } else { + grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(credentials.NewTLS(options.tlsConf))) + } + if len(options.grpcOpts) > 0 { + grpcOpts = append(grpcOpts, options.grpcOpts...) + } + conn, err := grpc.NewClient(options.endpoint, grpcOpts...) + if err != nil { + return nil, err + } + conn.Connect() + return conn, nil +} + +func unaryClientInterceptor(ms []middleware.Middleware, timeout time.Duration, filters []selector.NodeFilter) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = transport.NewClientContext(ctx, &Transport{ + endpoint: cc.Target(), + operation: method, + reqHeader: headerCarrier{}, + nodeFilters: filters, + }) + if timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + h := func(ctx context.Context, req any) (any, error) { + if tr, ok := transport.FromClientContext(ctx); ok { + header := tr.RequestHeader() + keys := header.Keys() + keyvals := make([]string, 0, len(keys)) + for _, k := range keys { + keyvals = append(keyvals, k, header.Get(k)) + } + ctx = grpcmd.AppendToOutgoingContext(ctx, keyvals...) + } + return reply, invoker(ctx, method, req, reply, cc, opts...) + } + if len(ms) > 0 { + h = middleware.Chain(ms...)(h) + } + var p selector.Peer + ctx = selector.NewPeerContext(ctx, &p) + _, err := h(ctx, req) + return err + } +} + +// wrappedClientStream wraps the grpc.ClientStream and applies middleware +type wrappedClientStream struct { + grpc.ClientStream + ctx context.Context + middleware matcher.Matcher +} + +func (w *wrappedClientStream) Context() context.Context { + return w.ctx +} + +func (w *wrappedClientStream) SendMsg(m any) error { + h := func(_ context.Context, req any) (any, error) { + return req, w.ClientStream.SendMsg(m) + } + + info, ok := transport.FromClientContext(w.ctx) + if !ok { + return fmt.Errorf("transport value stored in ctx returns: %v", ok) + } + + if next := w.middleware.Match(info.Operation()); len(next) > 0 { + h = middleware.Chain(next...)(h) + } + + _, err := h(w.ctx, m) + return err +} + +func (w *wrappedClientStream) RecvMsg(m any) error { + h := func(_ context.Context, req any) (any, error) { + return req, w.ClientStream.RecvMsg(m) + } + + info, ok := transport.FromClientContext(w.ctx) + if !ok { + return fmt.Errorf("transport value stored in ctx returns: %v", ok) + } + + if next := w.middleware.Match(info.Operation()); len(next) > 0 { + h = middleware.Chain(next...)(h) + } + + _, err := h(w.ctx, m) + return err +} + +func streamClientInterceptor(ms []middleware.Middleware, filters []selector.NodeFilter) grpc.StreamClientInterceptor { + return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { // nolint + ctx = transport.NewClientContext(ctx, &Transport{ + endpoint: cc.Target(), + operation: method, + reqHeader: headerCarrier{}, + nodeFilters: filters, + }) + var p selector.Peer + ctx = selector.NewPeerContext(ctx, &p) + + clientStream, err := streamer(ctx, desc, cc, method, opts...) + if err != nil { + return nil, err + } + + h := func(_ context.Context, _ any) (any, error) { + return streamer, nil + } + + m := matcher.New() + if len(ms) > 0 { + m.Use(ms...) + middleware.Chain(ms...)(h) + } + + wrappedStream := &wrappedClientStream{ + ClientStream: clientStream, + ctx: ctx, + middleware: m, + } + + return wrappedStream, nil + } +} diff --git a/transport/grpc/client_test.go b/transport/grpc/client_test.go new file mode 100644 index 0000000..c3930b1 --- /dev/null +++ b/transport/grpc/client_test.go @@ -0,0 +1,167 @@ +package grpc + +import ( + "context" + "crypto/tls" + "reflect" + "testing" + "time" + + "google.golang.org/grpc" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/registry" +) + +func TestWithEndpoint(t *testing.T) { + o := &clientOptions{} + v := "abc" + WithEndpoint(v)(o) + if !reflect.DeepEqual(v, o.endpoint) { + t.Errorf("expect %v but got %v", v, o.endpoint) + } +} + +func TestWithTimeout(t *testing.T) { + o := &clientOptions{} + v := time.Duration(123) + WithTimeout(v)(o) + if !reflect.DeepEqual(v, o.timeout) { + t.Errorf("expect %v but got %v", v, o.timeout) + } +} + +func TestWithMiddleware(t *testing.T) { + o := &clientOptions{} + v := []middleware.Middleware{ + func(middleware.Handler) middleware.Handler { return nil }, + } + WithMiddleware(v...)(o) + if !reflect.DeepEqual(v, o.middleware) { + t.Errorf("expect %v but got %v", v, o.middleware) + } +} + +func TestWithStreamMiddleware(t *testing.T) { + o := &clientOptions{} + v := []middleware.Middleware{ + func(middleware.Handler) middleware.Handler { return nil }, + } + WithStreamMiddleware(v...)(o) + if !reflect.DeepEqual(v, o.streamMiddleware) { + t.Errorf("expect %v but got %v", v, o.streamInts) + } +} + +type mockRegistry struct{} + +func (m *mockRegistry) GetService(_ context.Context, _ string) ([]*registry.ServiceInstance, error) { + return nil, nil +} + +func (m *mockRegistry) Watch(_ context.Context, _ string) (registry.Watcher, error) { + return nil, nil +} + +func TestWithDiscovery(t *testing.T) { + o := &clientOptions{} + v := &mockRegistry{} + WithDiscovery(v)(o) + if !reflect.DeepEqual(v, o.discovery) { + t.Errorf("expect %v but got %v", v, o.discovery) + } +} + +func TestWithTLSConfig(t *testing.T) { + o := &clientOptions{} + v := &tls.Config{} + WithTLSConfig(v)(o) + if !reflect.DeepEqual(v, o.tlsConf) { + t.Errorf("expect %v but got %v", v, o.tlsConf) + } +} + +func EmptyMiddleware() middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + return handler(ctx, req) + } + } +} + +func TestUnaryClientInterceptor(t *testing.T) { + f := unaryClientInterceptor([]middleware.Middleware{EmptyMiddleware()}, time.Duration(100), nil) + req := &struct{}{} + resp := &struct{}{} + + err := f(context.TODO(), "hello", req, resp, &grpc.ClientConn{}, + func(context.Context, string, any, any, *grpc.ClientConn, ...grpc.CallOption) error { + return nil + }) + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} + +func TestWithUnaryInterceptor(t *testing.T) { + o := &clientOptions{} + v := []grpc.UnaryClientInterceptor{ + func(context.Context, string, any, any, *grpc.ClientConn, grpc.UnaryInvoker, ...grpc.CallOption) error { + return nil + }, + func(context.Context, string, any, any, *grpc.ClientConn, grpc.UnaryInvoker, ...grpc.CallOption) error { + return nil + }, + } + WithUnaryInterceptor(v...)(o) + if !reflect.DeepEqual(v, o.ints) { + t.Errorf("expect %v but got %v", v, o.ints) + } +} + +func TestWithOptions(t *testing.T) { + o := &clientOptions{} + v := []grpc.DialOption{ + grpc.EmptyDialOption{}, + } + WithOptions(v...)(o) + if !reflect.DeepEqual(v, o.grpcOpts) { + t.Errorf("expect %v but got %v", v, o.grpcOpts) + } +} + +func TestWithHealthCheck(t *testing.T) { + o := &clientOptions{ + healthCheckConfig: `,"healthCheckConfig":{"serviceName":""}`, + } + WithHealthCheck(false)(o) + if !reflect.DeepEqual("", o.healthCheckConfig) { + t.Errorf("expect %v but got %v", "", o.healthCheckConfig) + } +} + +func TestNewClientOptions(t *testing.T) { + o := &clientOptions{} + v := []grpc.DialOption{ + grpc.EmptyDialOption{}, + } + WithOptions(v...)(o) + if !reflect.DeepEqual(v, o.grpcOpts) { + t.Errorf("expect %v but got %v", v, o.grpcOpts) + } +} + +func TestNewClient(t *testing.T) { + conn, err := NewClient( + context.Background(), + WithDiscovery(&mockRegistry{}), + WithTimeout(10*time.Second), + WithEndpoint("abc"), + WithMiddleware(EmptyMiddleware()), + WithStreamMiddleware(EmptyMiddleware()), + ) + if err != nil { + t.Fatal(err) + } + defer conn.Close() +} diff --git a/transport/grpc/codec.go b/transport/grpc/codec.go new file mode 100644 index 0000000..891a13c --- /dev/null +++ b/transport/grpc/codec.go @@ -0,0 +1,40 @@ +package grpc + +import ( + "fmt" + + "google.golang.org/grpc/encoding" + "google.golang.org/protobuf/proto" + + enc "github.com/go-kratos/kratos/v3/encoding" + "github.com/go-kratos/kratos/v3/encoding/protojson" +) + +const jsonName = "json" + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with protobuf. It is the default codec for gRPC. +type codec struct{} + +func (codec) Marshal(v any) ([]byte, error) { + vv, ok := v.(proto.Message) + if !ok { + return nil, fmt.Errorf("failed to marshal, message is %T, want proto.Message", v) + } + return enc.GetCodec(protojson.Name).Marshal(vv) +} + +func (codec) Unmarshal(data []byte, v any) error { + vv, ok := v.(proto.Message) + if !ok { + return fmt.Errorf("failed to unmarshal, message is %T, want proto.Message", v) + } + return enc.GetCodec(protojson.Name).Unmarshal(data, vv) +} + +func (codec) Name() string { + return jsonName +} diff --git a/transport/grpc/codec_test.go b/transport/grpc/codec_test.go new file mode 100644 index 0000000..3bcf0e0 --- /dev/null +++ b/transport/grpc/codec_test.go @@ -0,0 +1,34 @@ +package grpc + +import ( + "reflect" + "testing" + + "google.golang.org/protobuf/types/known/structpb" +) + +func TestCodec(t *testing.T) { + in, err := structpb.NewStruct(map[string]any{"Golang": "Kratos"}) + if err != nil { + t.Errorf("grpc codec create input data error:%v", err) + } + c := codec{} + if c.Name() != "json" { + t.Errorf("grpc codec name want %v, got %v", "json", c.Name()) + } + data, err := c.Marshal(in) + if err != nil { + t.Errorf("grpc codec marshal error:%v", err) + } + if string(data) != `{"Golang":"Kratos"}` { + t.Errorf("grpc codec marshal want %v, got %v", `{"Golang":"Kratos"}`, string(data)) + } + out := &structpb.Struct{} + err = c.Unmarshal(data, out) + if err != nil { + t.Errorf("grpc codec unmarshal error:%v", err) + } + if !reflect.DeepEqual(in, out) { + t.Errorf("grpc codec want %v, got %v", in, out) + } +} diff --git a/transport/grpc/interceptor.go b/transport/grpc/interceptor.go new file mode 100644 index 0000000..010df4d --- /dev/null +++ b/transport/grpc/interceptor.go @@ -0,0 +1,148 @@ +package grpc + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + grpcmd "google.golang.org/grpc/metadata" + + ic "github.com/go-kratos/kratos/v3/internal/context" + "github.com/go-kratos/kratos/v3/internal/matcher" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +// unaryServerInterceptor is a gRPC unary server interceptor +func (s *Server) unaryServerInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + ctx, cancel := ic.Merge(ctx, s.baseCtx) + defer cancel() + md, _ := grpcmd.FromIncomingContext(ctx) + replyHeader := grpcmd.MD{} + tr := &Transport{ + operation: info.FullMethod, + reqHeader: headerCarrier(md), + replyHeader: headerCarrier(replyHeader), + } + if s.endpoint != nil { + tr.endpoint = s.endpoint.String() + } + ctx = transport.NewServerContext(ctx, tr) + if s.timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, s.timeout) + defer cancel() + } + h := func(ctx context.Context, req any) (any, error) { + return handler(ctx, req) + } + if next := s.middleware.Match(tr.Operation()); len(next) > 0 { + h = middleware.Chain(next...)(h) + } + reply, err := h(ctx, req) + if len(replyHeader) > 0 { + _ = grpc.SetHeader(ctx, replyHeader) + } + return reply, err + } +} + +// wrappedStream is rewrite grpc stream's context +type wrappedStream struct { + grpc.ServerStream + ctx context.Context + middleware matcher.Matcher +} + +func NewWrappedStream(ctx context.Context, stream grpc.ServerStream, m matcher.Matcher) grpc.ServerStream { + return &wrappedStream{ + ServerStream: stream, + ctx: ctx, + middleware: m, + } +} + +func (w *wrappedStream) Context() context.Context { + return w.ctx +} + +// streamServerInterceptor is a gRPC stream server interceptor +func (s *Server) streamServerInterceptor() grpc.StreamServerInterceptor { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx, cancel := ic.Merge(ss.Context(), s.baseCtx) + defer cancel() + md, _ := grpcmd.FromIncomingContext(ctx) + replyHeader := grpcmd.MD{} + ctx = transport.NewServerContext(ctx, &Transport{ + endpoint: s.endpoint.String(), + operation: info.FullMethod, + reqHeader: headerCarrier(md), + replyHeader: headerCarrier(replyHeader), + }) + + h := func(_ context.Context, _ any) (any, error) { + return handler(srv, ss), nil + } + + if next := s.streamMiddleware.Match(info.FullMethod); len(next) > 0 { + middleware.Chain(next...)(h) + } + + ctx = context.WithValue(ctx, stream{ + ServerStream: ss, + streamMiddleware: s.streamMiddleware, + }, ss) + ws := NewWrappedStream(ctx, ss, s.streamMiddleware) + + err := handler(srv, ws) + if len(replyHeader) > 0 { + _ = grpc.SetHeader(ctx, replyHeader) + } + return err + } +} + +type stream struct { + grpc.ServerStream + streamMiddleware matcher.Matcher +} + +func GetStream(ctx context.Context) grpc.ServerStream { + return ctx.Value(stream{}).(grpc.ServerStream) +} + +func (w *wrappedStream) SendMsg(m any) error { + h := func(_ context.Context, req any) (any, error) { + return req, w.ServerStream.SendMsg(m) + } + + info, ok := transport.FromServerContext(w.ctx) + if !ok { + return fmt.Errorf("transport value stored in ctx returns: %v", ok) + } + + if next := w.middleware.Match(info.Operation()); len(next) > 0 { + h = middleware.Chain(next...)(h) + } + + _, err := h(w.ctx, m) + return err +} + +func (w *wrappedStream) RecvMsg(m any) error { + h := func(_ context.Context, req any) (any, error) { + return req, w.ServerStream.RecvMsg(m) + } + + info, ok := transport.FromServerContext(w.ctx) + if !ok { + return fmt.Errorf("transport value stored in ctx returns: %v", ok) + } + + if next := w.middleware.Match(info.Operation()); len(next) > 0 { + h = middleware.Chain(next...)(h) + } + + _, err := h(w.ctx, m) + return err +} diff --git a/transport/grpc/resolver/direct/builder.go b/transport/grpc/resolver/direct/builder.go new file mode 100644 index 0000000..84856a3 --- /dev/null +++ b/transport/grpc/resolver/direct/builder.go @@ -0,0 +1,42 @@ +package direct + +import ( + "strings" + + "google.golang.org/grpc/resolver" +) + +const name = "direct" + +func init() { + resolver.Register(NewBuilder()) +} + +type directBuilder struct{} + +// NewBuilder creates a directBuilder which is used to factory direct resolvers. +// example: +// +// direct:///127.0.0.1:9000,127.0.0.2:9000 +func NewBuilder() resolver.Builder { + return &directBuilder{} +} + +func (d *directBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) { + parts := strings.Split(strings.TrimPrefix(target.URL.Path, "/"), ",") + addrs := make([]resolver.Address, 0, len(parts)) + for _, addr := range parts { + addrs = append(addrs, resolver.Address{Addr: addr}) + } + err := cc.UpdateState(resolver.State{ + Addresses: addrs, + }) + if err != nil { + return nil, err + } + return newDirectResolver(), nil +} + +func (d *directBuilder) Scheme() string { + return name +} diff --git a/transport/grpc/resolver/direct/builder_test.go b/transport/grpc/resolver/direct/builder_test.go new file mode 100644 index 0000000..d0a347d --- /dev/null +++ b/transport/grpc/resolver/direct/builder_test.go @@ -0,0 +1,54 @@ +package direct + +import ( + "errors" + "reflect" + "testing" + + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/serviceconfig" +) + +func TestDirectBuilder_Scheme(t *testing.T) { + b := NewBuilder() + if !reflect.DeepEqual(b.Scheme(), "direct") { + t.Errorf("expect %v, got %v", "direct", b.Scheme()) + } +} + +type mockConn struct { + needUpdateStateErr bool +} + +func (m *mockConn) UpdateState(resolver.State) error { + if m.needUpdateStateErr { + return errors.New("mock test needUpdateStateErr") + } + return nil +} + +func (m *mockConn) ReportError(error) {} + +func (m *mockConn) NewAddress(_ []resolver.Address) {} + +func (m *mockConn) NewServiceConfig(_ string) {} + +func (m *mockConn) ParseServiceConfig(_ string) *serviceconfig.ParseResult { + return nil +} + +func TestDirectBuilder_Build(t *testing.T) { + b := NewBuilder() + r, err := b.Build(resolver.Target{}, &mockConn{}, resolver.BuildOptions{}) + if err != nil { + t.Errorf("expect no error, got %v", err) + } + r.ResolveNow(resolver.ResolveNowOptions{}) + r.Close() + + // need update state err + _, err = b.Build(resolver.Target{}, &mockConn{needUpdateStateErr: true}, resolver.BuildOptions{}) + if err == nil { + t.Errorf("expect needUpdateStateErr, got nil") + } +} diff --git a/transport/grpc/resolver/direct/resolver.go b/transport/grpc/resolver/direct/resolver.go new file mode 100644 index 0000000..e2dce61 --- /dev/null +++ b/transport/grpc/resolver/direct/resolver.go @@ -0,0 +1,15 @@ +package direct + +import "google.golang.org/grpc/resolver" + +type directResolver struct{} + +func newDirectResolver() resolver.Resolver { + return &directResolver{} +} + +func (r *directResolver) Close() { +} + +func (r *directResolver) ResolveNow(_ resolver.ResolveNowOptions) { +} diff --git a/transport/grpc/resolver/direct/resolver_test.go b/transport/grpc/resolver/direct/resolver_test.go new file mode 100644 index 0000000..4771773 --- /dev/null +++ b/transport/grpc/resolver/direct/resolver_test.go @@ -0,0 +1 @@ +package direct diff --git a/transport/grpc/resolver/discovery/builder.go b/transport/grpc/resolver/discovery/builder.go new file mode 100644 index 0000000..14d3a3d --- /dev/null +++ b/transport/grpc/resolver/discovery/builder.go @@ -0,0 +1,112 @@ +package discovery + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/google/uuid" + "google.golang.org/grpc/resolver" + + "github.com/go-kratos/kratos/v3/registry" +) + +const name = "discovery" + +var ErrWatcherCreateTimeout = errors.New("discovery create watcher overtime") + +// Option is builder option. +type Option func(o *builder) + +// WithTimeout with timeout option. +func WithTimeout(timeout time.Duration) Option { + return func(b *builder) { + b.timeout = timeout + } +} + +// WithInsecure with isSecure option. +func WithInsecure(insecure bool) Option { + return func(b *builder) { + b.insecure = insecure + } +} + +// WithSubset with subset size. +func WithSubset(size int) Option { + return func(b *builder) { + b.subsetSize = size + } +} + +type builder struct { + discoverer registry.Discovery + timeout time.Duration + insecure bool + subsetSize int +} + +// NewBuilder creates a builder which is used to factory registry resolvers. +func NewBuilder(d registry.Discovery, opts ...Option) resolver.Builder { + b := &builder{ + discoverer: d, + timeout: time.Second * 10, + insecure: false, + subsetSize: 25, + } + for _, o := range opts { + o(b) + } + return b +} + +func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) { + watchRes := &struct { + err error + w registry.Watcher + }{} + + done := make(chan struct{}, 1) + ctx, cancel := context.WithCancel(context.Background()) + go func() { + w, err := b.discoverer.Watch(ctx, strings.TrimPrefix(target.URL.Path, "/")) + watchRes.w = w + watchRes.err = err + close(done) + }() + + var err error + if b.timeout > 0 { + select { + case <-done: + err = watchRes.err + case <-time.After(b.timeout): + err = ErrWatcherCreateTimeout + } + } else { + <-done + err = watchRes.err + } + if err != nil { + cancel() + return nil, err + } + + r := &discoveryResolver{ + w: watchRes.w, + cc: cc, + ctx: ctx, + cancel: cancel, + insecure: b.insecure, + subsetSize: b.subsetSize, + selectorKey: uuid.New().String(), + } + go r.watch() + return r, nil +} + +// Scheme return scheme of discovery +func (*builder) Scheme() string { + return name +} diff --git a/transport/grpc/resolver/discovery/builder_test.go b/transport/grpc/resolver/discovery/builder_test.go new file mode 100644 index 0000000..d476938 --- /dev/null +++ b/transport/grpc/resolver/discovery/builder_test.go @@ -0,0 +1,97 @@ +package discovery + +import ( + "context" + "net/url" + "reflect" + "testing" + "time" + + "google.golang.org/grpc/resolver" + "google.golang.org/grpc/serviceconfig" + + "github.com/go-kratos/kratos/v3/registry" +) + +func TestWithInsecure(t *testing.T) { + b := &builder{} + WithInsecure(true)(b) + if !b.insecure { + t.Errorf("expected insecure to be true") + } +} + +func TestWithTimeout(t *testing.T) { + o := &builder{} + v := time.Duration(123) + WithTimeout(v)(o) + if !reflect.DeepEqual(v, o.timeout) { + t.Errorf("expected %v, got %v", v, o.timeout) + } +} + +type mockDiscovery struct{} + +func (m *mockDiscovery) GetService(_ context.Context, _ string) ([]*registry.ServiceInstance, error) { + return nil, nil +} + +func (m *mockDiscovery) Watch(_ context.Context, _ string) (registry.Watcher, error) { + time.Sleep(time.Microsecond * 500) + return &testWatch{}, nil +} + +func TestBuilder_Scheme(t *testing.T) { + b := NewBuilder(&mockDiscovery{}) + if !reflect.DeepEqual("discovery", b.Scheme()) { + t.Errorf("expected %v, got %v", "discovery", b.Scheme()) + } +} + +type mockConn struct{} + +func (m *mockConn) UpdateState(resolver.State) error { + return nil +} + +func (m *mockConn) ReportError(error) {} + +func (m *mockConn) NewAddress(_ []resolver.Address) {} + +func (m *mockConn) NewServiceConfig(_ string) {} + +func (m *mockConn) ParseServiceConfig(_ string) *serviceconfig.ParseResult { + return nil +} + +func TestBuilder_Build(t *testing.T) { + b := NewBuilder(&mockDiscovery{}) + _, err := b.Build( + resolver.Target{ + URL: url.URL{ + Scheme: resolver.GetDefaultScheme(), + Path: "grpc://authority/endpoint", + }, + }, + &mockConn{}, + resolver.BuildOptions{}, + ) + if err != nil { + t.Errorf("expected no error, got %v", err) + return + } + timeoutBuilder := NewBuilder(&mockDiscovery{}, WithTimeout(0)) + _, err = timeoutBuilder.Build( + resolver.Target{ + URL: url.URL{ + Scheme: resolver.GetDefaultScheme(), + Path: "grpc://authority/endpoint", + }, + }, + &mockConn{}, + resolver.BuildOptions{}, + ) + if err != nil { + t.Errorf("expected no error, got %v", err) + } +} diff --git a/transport/grpc/resolver/discovery/resolver.go b/transport/grpc/resolver/discovery/resolver.go new file mode 100644 index 0000000..a98a1da --- /dev/null +++ b/transport/grpc/resolver/discovery/resolver.go @@ -0,0 +1,113 @@ +package discovery + +import ( + "context" + "encoding/json" + "errors" + "time" + + "google.golang.org/grpc/attributes" + "google.golang.org/grpc/resolver" + + "github.com/go-kratos/kratos/v3/internal/endpoint" + "github.com/go-kratos/kratos/v3/internal/subset" + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/registry" +) + +type discoveryResolver struct { + w registry.Watcher + cc resolver.ClientConn + + ctx context.Context + cancel context.CancelFunc + + insecure bool + selectorKey string + subsetSize int +} + +func (r *discoveryResolver) watch() { + for { + select { + case <-r.ctx.Done(): + return + default: + } + ins, err := r.w.Next() + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + log.Error("[resolver] failed to watch discovery endpoint", "error", err) + time.Sleep(time.Second) + continue + } + r.update(ins) + } +} + +func (r *discoveryResolver) update(ins []*registry.ServiceInstance) { + var ( + endpoints = make(map[string]struct{}) + filtered = make([]*registry.ServiceInstance, 0, len(ins)) + ) + for _, in := range ins { + ept, err := endpoint.ParseEndpoint(in.Endpoints, endpoint.Scheme("grpc", !r.insecure)) + if err != nil { + log.Error("[resolver] failed to parse discovery endpoint", "error", err) + continue + } + if ept == "" { + continue + } + // filter redundant endpoints + if _, ok := endpoints[ept]; ok { + continue + } + endpoints[ept] = struct{}{} + filtered = append(filtered, in) + } + if r.subsetSize != 0 { + filtered = subset.Subset(r.selectorKey, filtered, r.subsetSize) + } + + addrs := make([]resolver.Address, 0, len(filtered)) + for _, in := range filtered { + ept, _ := endpoint.ParseEndpoint(in.Endpoints, endpoint.Scheme("grpc", !r.insecure)) + addr := resolver.Address{ + ServerName: in.Name, + Attributes: parseAttributes(in.Metadata).WithValue("rawServiceInstance", in), + Addr: ept, + } + addrs = append(addrs, addr) + } + if len(addrs) == 0 { + log.Warn("[resolver] zero endpoint found, refused to write", "instances", ins) + return + } + err := r.cc.UpdateState(resolver.State{Addresses: addrs}) + if err != nil { + log.Error("[resolver] failed to update state", "error", err) + } + + b, _ := json.Marshal(filtered) + log.Info("[resolver] update instances", "instances", string(b)) +} + +func (r *discoveryResolver) Close() { + r.cancel() + err := r.w.Stop() + if err != nil { + log.Error("[resolver] failed to stop watcher", "error", err) + } +} + +func (r *discoveryResolver) ResolveNow(_ resolver.ResolveNowOptions) {} + +func parseAttributes(md map[string]string) (a *attributes.Attributes) { + for k, v := range md { + a = a.WithValue(k, v) + } + return a +} diff --git a/transport/grpc/resolver/discovery/resolver_test.go b/transport/grpc/resolver/discovery/resolver_test.go new file mode 100644 index 0000000..340c488 --- /dev/null +++ b/transport/grpc/resolver/discovery/resolver_test.go @@ -0,0 +1,127 @@ +package discovery + +import ( + "context" + "errors" + "reflect" + "testing" + "time" + + "google.golang.org/grpc/resolver" + + "github.com/go-kratos/kratos/v3/registry" +) + +type testClientConn struct { + resolver.ClientConn // For unimplemented functions + te *testing.T +} + +func (t *testClientConn) UpdateState(s resolver.State) error { + t.te.Log("UpdateState", s) + return nil +} + +type testWatch struct { + err error + + count uint +} + +func (m *testWatch) Next() ([]*registry.ServiceInstance, error) { + time.Sleep(time.Millisecond * 200) + if m.count > 1 { + return nil, nil + } + m.count++ + ins := []*registry.ServiceInstance{ + { + ID: "mock_ID", + Name: "mock_Name", + Version: "mock_Version", + Endpoints: []string{"grpc://127.0.0.1?isSecure=true"}, + }, + { + ID: "mock_ID2", + Name: "mock_Name2", + Version: "mock_Version2", + Endpoints: []string{""}, + }, + } + return ins, m.err +} + +// Watch creates a watcher according to the service name. +func (m *testWatch) Stop() error { + return m.err +} + +func TestWatch(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + r := &discoveryResolver{ + w: &testWatch{}, + cc: &testClientConn{te: t}, + ctx: ctx, + cancel: cancel, + insecure: false, + } + r.ResolveNow(resolver.ResolveNowOptions{}) + go func() { + time.Sleep(time.Second * 2) + r.Close() + }() + r.watch() + t.Log("watch goroutine exited after 2 second") +} + +func TestWatchError(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + r := &discoveryResolver{ + w: &testWatch{err: errors.New("bad")}, + cc: &testClientConn{te: t}, + ctx: ctx, + cancel: cancel, + } + go func() { + time.Sleep(time.Second * 2) + r.Close() + }() + r.watch() + t.Log("watch goroutine exited after 2 second") +} + +func TestWatchContextCancel(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + r := &discoveryResolver{ + w: &testWatch{err: context.Canceled}, + cc: &testClientConn{te: t}, + ctx: ctx, + cancel: cancel, + } + go func() { + time.Sleep(time.Second * 2) + r.Close() + }() + r.watch() + t.Log("watch goroutine exited after 2 second") +} + +func TestParseAttributes(t *testing.T) { + a := parseAttributes(map[string]string{ + "a": "b", + "c": "d", + }) + if !reflect.DeepEqual("b", a.Value("a").(string)) { + t.Errorf("expect b, got %v", a.Value("a")) + } + x := a.WithValue("qq", "ww") + if !reflect.DeepEqual("ww", x.Value("qq").(string)) { + t.Errorf("expect ww, got %v", x.Value("qq")) + } + if x.Value("notfound") != nil { + t.Errorf("expect nil, got %v", x.Value("notfound")) + } +} diff --git a/transport/grpc/server.go b/transport/grpc/server.go new file mode 100644 index 0000000..5095e52 --- /dev/null +++ b/transport/grpc/server.go @@ -0,0 +1,267 @@ +package grpc + +import ( + "context" + "crypto/tls" + "net" + "net/url" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/admin" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" + + "github.com/go-kratos/kratos/v3/internal/endpoint" + "github.com/go-kratos/kratos/v3/internal/host" + "github.com/go-kratos/kratos/v3/internal/matcher" + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +var ( + _ transport.Server = (*Server)(nil) + _ transport.Endpointer = (*Server)(nil) +) + +// ServerOption is gRPC server option. +type ServerOption func(o *Server) + +// Network with server network. +func Network(network string) ServerOption { + return func(s *Server) { + s.network = network + } +} + +// Address with server address. +func Address(addr string) ServerOption { + return func(s *Server) { + s.address = addr + } +} + +// Endpoint with server address. +func Endpoint(endpoint *url.URL) ServerOption { + return func(s *Server) { + s.endpoint = endpoint + } +} + +// Timeout with server timeout. +func Timeout(timeout time.Duration) ServerOption { + return func(s *Server) { + s.timeout = timeout + } +} + +// Middleware with server middleware. +func Middleware(m ...middleware.Middleware) ServerOption { + return func(s *Server) { + s.middleware.Use(m...) + } +} + +func StreamMiddleware(m ...middleware.Middleware) ServerOption { + return func(s *Server) { + s.streamMiddleware.Use(m...) + } +} + +// CustomHealth Checks server. +func CustomHealth() ServerOption { + return func(s *Server) { + s.customHealth = true + } +} + +// TLSConfig with TLS config. +func TLSConfig(c *tls.Config) ServerOption { + return func(s *Server) { + s.tlsConf = c + } +} + +// Listener with server lis +func Listener(lis net.Listener) ServerOption { + return func(s *Server) { + s.lis = lis + } +} + +// UnaryInterceptor returns a ServerOption that sets the UnaryServerInterceptor for the server. +func UnaryInterceptor(in ...grpc.UnaryServerInterceptor) ServerOption { + return func(s *Server) { + s.unaryInts = in + } +} + +// StreamInterceptor returns a ServerOption that sets the StreamServerInterceptor for the server. +func StreamInterceptor(in ...grpc.StreamServerInterceptor) ServerOption { + return func(s *Server) { + s.streamInts = in + } +} + +// DisableReflection disable grpc reflection. +func DisableReflection() ServerOption { + return func(s *Server) { + s.disableReflection = true + } +} + +// Options with grpc options. +func Options(opts ...grpc.ServerOption) ServerOption { + return func(s *Server) { + s.grpcOpts = opts + } +} + +// Server is a gRPC server wrapper. +type Server struct { + *grpc.Server + baseCtx context.Context + tlsConf *tls.Config + lis net.Listener + err error + network string + address string + endpoint *url.URL + timeout time.Duration + middleware matcher.Matcher + streamMiddleware matcher.Matcher + unaryInts []grpc.UnaryServerInterceptor + streamInts []grpc.StreamServerInterceptor + grpcOpts []grpc.ServerOption + health *health.Server + customHealth bool + adminClean func() + disableReflection bool +} + +// NewServer creates a gRPC server by options. +func NewServer(opts ...ServerOption) *Server { + srv := &Server{ + baseCtx: context.Background(), + network: "tcp", + address: ":0", + timeout: 1 * time.Second, + health: health.NewServer(), + middleware: matcher.New(), + streamMiddleware: matcher.New(), + } + for _, o := range opts { + o(srv) + } + unaryInts := []grpc.UnaryServerInterceptor{ + srv.unaryServerInterceptor(), + } + streamInts := []grpc.StreamServerInterceptor{ + srv.streamServerInterceptor(), + } + if len(srv.unaryInts) > 0 { + unaryInts = append(unaryInts, srv.unaryInts...) + } + if len(srv.streamInts) > 0 { + streamInts = append(streamInts, srv.streamInts...) + } + grpcOpts := []grpc.ServerOption{ + grpc.ChainUnaryInterceptor(unaryInts...), + grpc.ChainStreamInterceptor(streamInts...), + } + if srv.tlsConf != nil { + grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(srv.tlsConf))) + } + if len(srv.grpcOpts) > 0 { + grpcOpts = append(grpcOpts, srv.grpcOpts...) + } + srv.Server = grpc.NewServer(grpcOpts...) + // internal register + if !srv.customHealth { + grpc_health_v1.RegisterHealthServer(srv.Server, srv.health) + } + // reflection register + if !srv.disableReflection { + reflection.Register(srv.Server) + } + // admin register + srv.adminClean, _ = admin.Register(srv.Server) + return srv +} + +// Use uses a service middleware with selector. +// selector: +// - '/*' +// - '/helloworld.v1.Greeter/*' +// - '/helloworld.v1.Greeter/SayHello' +func (s *Server) Use(selector string, m ...middleware.Middleware) { + s.middleware.Add(selector, m...) +} + +// Endpoint return a real address to registry endpoint. +// examples: +// +// grpc://127.0.0.1:9000?isSecure=false +func (s *Server) Endpoint() (*url.URL, error) { + if err := s.listenAndEndpoint(); err != nil { + return nil, s.err + } + return s.endpoint, nil +} + +// Start start the gRPC server. +func (s *Server) Start(ctx context.Context) error { + if err := s.listenAndEndpoint(); err != nil { + return s.err + } + s.baseCtx = ctx + log.Info("[gRPC] server listening", "addr", s.lis.Addr().String()) + s.health.Resume() + return s.Serve(s.lis) +} + +// Stop stop the gRPC server. +func (s *Server) Stop(ctx context.Context) error { + if s.adminClean != nil { + s.adminClean() + } + s.health.Shutdown() + + done := make(chan struct{}) + go func() { + defer close(done) + log.Info("[gRPC] server stopping") + s.GracefulStop() + }() + + select { + case <-done: + case <-ctx.Done(): + log.Warn("[gRPC] server couldn't stop gracefully in time, doing force stop") + s.Server.Stop() + } + return nil +} + +func (s *Server) listenAndEndpoint() error { + if s.lis == nil { + lis, err := net.Listen(s.network, s.address) + if err != nil { + s.err = err + return err + } + s.lis = lis + } + if s.endpoint == nil { + addr, err := host.Extract(s.address, s.lis) + if err != nil { + s.err = err + return err + } + s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("grpc", s.tlsConf != nil), addr) + } + return s.err +} diff --git a/transport/grpc/server_test.go b/transport/grpc/server_test.go new file mode 100644 index 0000000..65d4ff6 --- /dev/null +++ b/transport/grpc/server_test.go @@ -0,0 +1,499 @@ +package grpc + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "log/slog" + "net" + "net/url" + "reflect" + "strings" + "sync" + "testing" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/internal/matcher" + pb "github.com/go-kratos/kratos/v3/internal/testdata/helloworld" + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +// server is used to implement helloworld.GreeterServer. +type server struct { + pb.UnimplementedGreeterServer +} + +func (s *server) SayHelloStream(streamServer pb.Greeter_SayHelloStreamServer) error { + tctx, ok := transport.FromServerContext(streamServer.Context()) + if ok { + tctx.ReplyHeader().Set("123", "123") + } + var cnt uint + for { + in, err := streamServer.Recv() + if err != nil { + return err + } + if in.Name == "error" { + return errors.BadRequest("custom_error", fmt.Sprintf("invalid argument %s", in.Name)) + } + if in.Name == "panic" { + panic("server panic") + } + err = streamServer.Send(&pb.HelloReply{ + Message: fmt.Sprintf("hello %s", in.Name), + }) + if err != nil { + return err + } + cnt++ + if cnt > 1 { + return nil + } + } +} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + if in.Name == "error" { + return nil, errors.BadRequest("custom_error", fmt.Sprintf("invalid argument %s", in.Name)) + } + if in.Name == "panic" { + panic("server panic") + } + return &pb.HelloReply{Message: fmt.Sprintf("Hello %+v", in.Name)}, nil +} + +type testKey struct{} + +func TestServer(t *testing.T) { + ctx := context.Background() + ctx = context.WithValue(ctx, testKey{}, "test") + srv := NewServer( + Middleware( + func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if tr, ok := transport.FromServerContext(ctx); ok { + if tr.ReplyHeader() != nil { + tr.ReplyHeader().Set("req_id", "3344") + } + } + return handler(ctx, req) + } + }), + UnaryInterceptor(func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + return handler(ctx, req) + }), + Options(grpc.InitialConnWindowSize(0)), + ) + pb.RegisterGreeterServer(srv, &server{}) + + if e, err := srv.Endpoint(); err != nil || e == nil || strings.HasSuffix(e.Host, ":0") { + t.Fatal(e, err) + } + + go func() { + // start server + if err := srv.Start(ctx); err != nil { + panic(err) + } + }() + time.Sleep(time.Second) + testClient(t, srv) + _ = srv.Stop(ctx) +} + +func testClient(t *testing.T, srv *Server) { + u, err := srv.Endpoint() + if err != nil { + t.Fatal(err) + } + // new a gRPC client + conn, err := NewClient(context.Background(), + WithEndpoint(u.Host), + WithUnaryInterceptor( + func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + return invoker(ctx, method, req, reply, cc, opts...) + }), + WithMiddleware(func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (reply any, err error) { + if tr, ok := transport.FromClientContext(ctx); ok { + header := tr.RequestHeader() + header.Set("x-md-trace", "2233") + } + return handler(ctx, req) + } + }), + ) + defer func() { + _ = conn.Close() + }() + if err != nil { + t.Fatal(err) + } + client := pb.NewGreeterClient(conn) + reply, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "kratos"}) + t.Log(err) + if err != nil { + t.Errorf("failed to call: %v", err) + } + if !reflect.DeepEqual(reply.Message, "Hello kratos") { + t.Errorf("expect %s, got %s", "Hello kratos", reply.Message) + } + + streamCli, err := client.SayHelloStream(context.Background()) + if err != nil { + t.Error(err) + return + } + defer func() { + _ = streamCli.CloseSend() + }() + err = streamCli.Send(&pb.HelloRequest{Name: "cc"}) + if err != nil { + t.Error(err) + return + } + reply, err = streamCli.Recv() + if err != nil { + t.Error(err) + return + } + if !reflect.DeepEqual(reply.Message, "hello cc") { + t.Errorf("expect %s, got %s", "hello cc", reply.Message) + } +} + +func TestNetwork(t *testing.T) { + o := &Server{} + v := "abc" + Network(v)(o) + if !reflect.DeepEqual(v, o.network) { + t.Errorf("expect %s, got %s", v, o.network) + } +} + +func TestAddress(t *testing.T) { + v := "abc" + o := NewServer(Address(v)) + if !reflect.DeepEqual(v, o.address) { + t.Errorf("expect %s, got %s", v, o.address) + } + u, err := o.Endpoint() + if err == nil { + t.Errorf("expect %s, got %s", v, err) + } + if u != nil { + t.Errorf("expect %s, got %s", v, u) + } +} + +func TestTimeout(t *testing.T) { + o := &Server{} + v := time.Duration(123) + Timeout(v)(o) + if !reflect.DeepEqual(v, o.timeout) { + t.Errorf("expect %s, got %s", v, o.timeout) + } +} + +func TestTLSConfig(t *testing.T) { + o := &Server{} + v := &tls.Config{} + TLSConfig(v)(o) + if !reflect.DeepEqual(v, o.tlsConf) { + t.Errorf("expect %v, got %v", v, o.tlsConf) + } +} + +func TestUnaryInterceptor(t *testing.T) { + o := &Server{} + v := []grpc.UnaryServerInterceptor{ + func(context.Context, any, *grpc.UnaryServerInfo, grpc.UnaryHandler) (resp any, err error) { + return nil, nil + }, + func(context.Context, any, *grpc.UnaryServerInfo, grpc.UnaryHandler) (resp any, err error) { + return nil, nil + }, + } + UnaryInterceptor(v...)(o) + if !reflect.DeepEqual(v, o.unaryInts) { + t.Errorf("expect %v, got %v", v, o.unaryInts) + } +} + +func TestStreamInterceptor(t *testing.T) { + o := &Server{} + v := []grpc.StreamServerInterceptor{ + func(any, grpc.ServerStream, *grpc.StreamServerInfo, grpc.StreamHandler) error { + return nil + }, + func(any, grpc.ServerStream, *grpc.StreamServerInfo, grpc.StreamHandler) error { + return nil + }, + } + StreamInterceptor(v...)(o) + if !reflect.DeepEqual(v, o.streamInts) { + t.Errorf("expect %v, got %v", v, o.streamInts) + } +} + +func TestOptions(t *testing.T) { + o := &Server{} + v := []grpc.ServerOption{ + grpc.EmptyServerOption{}, + } + Options(v...)(o) + if !reflect.DeepEqual(v, o.grpcOpts) { + t.Errorf("expect %v, got %v", v, o.grpcOpts) + } +} + +type testResp struct { + Data string +} + +func TestServer_unaryServerInterceptor(t *testing.T) { + u, err := url.Parse("grpc://hello/world") + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + srv := &Server{ + baseCtx: context.Background(), + endpoint: u, + timeout: time.Duration(10), + middleware: matcher.New(), + } + srv.middleware.Use(EmptyMiddleware()) + req := &struct{}{} + rv, err := srv.unaryServerInterceptor()(context.TODO(), req, &grpc.UnaryServerInfo{}, func(context.Context, any) (any, error) { + return &testResp{Data: "hi"}, nil + }) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if !reflect.DeepEqual("hi", rv.(*testResp).Data) { + t.Errorf("expect %s, got %s", "hi", rv.(*testResp).Data) + } +} + +type mockServerStream struct { + ctx context.Context + sentMsg any + recvMsg any + metadata metadata.MD + grpc.ServerStream +} + +func (m *mockServerStream) SetHeader(md metadata.MD) error { + m.metadata = md + return nil +} + +func (m *mockServerStream) SendHeader(md metadata.MD) error { + m.metadata = md + return nil +} + +func (m *mockServerStream) SetTrailer(md metadata.MD) { + m.metadata = md +} + +func (m *mockServerStream) Context() context.Context { + return m.ctx +} + +func (m *mockServerStream) SendMsg(msg any) error { + m.sentMsg = msg + return nil +} + +func (m *mockServerStream) RecvMsg(msg any) error { + m.recvMsg = msg + return nil +} + +func TestServer_streamServerInterceptor(t *testing.T) { + u, err := url.Parse("grpc://hello/world") + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + srv := &Server{ + baseCtx: context.Background(), + endpoint: u, + timeout: time.Duration(10), + middleware: matcher.New(), + streamMiddleware: matcher.New(), + } + + srv.streamMiddleware.Use(EmptyMiddleware()) + + mockStream := &mockServerStream{ + ctx: srv.baseCtx, + } + + handler := func(_ any, stream grpc.ServerStream) error { + resp := &testResp{Data: "stream hi"} + return stream.SendMsg(resp) + } + + info := &grpc.StreamServerInfo{ + FullMethod: "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", + } + + err = srv.streamServerInterceptor()(nil, mockStream, info, handler) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + + // Check response + resp := mockStream.sentMsg.(*testResp) + if !reflect.DeepEqual("stream hi", resp.Data) { + t.Errorf("expect %s, got %s", "stream hi", resp.Data) + } +} + +func TestListener(t *testing.T) { + lis, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal(err) + } + s := &Server{} + Listener(lis)(s) + if !reflect.DeepEqual(lis, s.lis) { + t.Errorf("expect %v, got %v", lis, s.lis) + } + if e, err := s.Endpoint(); err != nil || e == nil { + t.Errorf("expect not empty") + } +} + +func TestStop(t *testing.T) { + timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + tests := []struct { + name string + ctx context.Context + cancel context.CancelFunc + wantForceStop bool + }{ + { + name: "normal", + ctx: context.Background(), + cancel: func() {}, + wantForceStop: false, + }, + { + name: "timeout", + ctx: timeoutCtx, + cancel: cancel, + wantForceStop: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + old := log.Default() + defer log.SetDefault(old) + + // Create a logger to capture logs + var logs safeBytesBuffer + log.SetDefault(slog.New(slog.NewTextHandler(&logs, nil))) + + s := NewServer(Listener(l)) + pb.RegisterGreeterServer(s, &server{}) + + go func() { + err := s.Start(context.Background()) //nolint + if err != nil { + t.Errorf("server error: %v", err) + } + }() + + time.Sleep(100 * time.Millisecond) + + conn, err := NewClient( + context.Background(), + WithEndpoint(l.Addr().String()), + ) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + go func() { + client := pb.NewGreeterClient(conn) + if tt.wantForceStop { + // Simulate a long-running request + s, err := client.SayHelloStream(context.Background()) //nolint + if err != nil { + t.Errorf("server error: %v", err) + } + // Keep the stream open + for { + // Intentionally do not send messages, only receive messages + _, err := s.Recv() + if err != nil { + break + } + } + } else { + _, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "test"}) //nolint + if err != nil { + log.Error("client error", "error", err) + } + } + }() + + time.Sleep(100 * time.Millisecond) + + err = s.Stop(tt.ctx) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + // Check if the stop was forced or graceful + if tt.wantForceStop { + if !strings.Contains(logs.String(), "force stop") { + t.Errorf("Expected force stop\n%s", logs.String()) + } + } else { + if strings.Contains(logs.String(), "force stop") { + t.Errorf("Expected graceful stop\n%s", logs.String()) + } + } + }) + } +} + +type safeBytesBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *safeBytesBuffer) Write(p []byte) (n int, err error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *safeBytesBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} diff --git a/transport/grpc/transport.go b/transport/grpc/transport.go new file mode 100644 index 0000000..dc1cc8f --- /dev/null +++ b/transport/grpc/transport.go @@ -0,0 +1,84 @@ +package grpc + +import ( + "google.golang.org/grpc/metadata" + + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/transport" +) + +var _ transport.Transporter = (*Transport)(nil) + +// Transport is a gRPC transport. +type Transport struct { + endpoint string + operation string + reqHeader headerCarrier + replyHeader headerCarrier + nodeFilters []selector.NodeFilter +} + +// Kind returns the transport kind. +func (tr *Transport) Kind() transport.Kind { + return transport.KindGRPC +} + +// Endpoint returns the transport endpoint. +func (tr *Transport) Endpoint() string { + return tr.endpoint +} + +// Operation returns the transport operation. +func (tr *Transport) Operation() string { + return tr.operation +} + +// RequestHeader returns the request header. +func (tr *Transport) RequestHeader() transport.Header { + return tr.reqHeader +} + +// ReplyHeader returns the reply header. +func (tr *Transport) ReplyHeader() transport.Header { + return tr.replyHeader +} + +// NodeFilters returns the client select filters. +func (tr *Transport) NodeFilters() []selector.NodeFilter { + return tr.nodeFilters +} + +type headerCarrier metadata.MD + +// Get returns the value associated with the passed key. +func (mc headerCarrier) Get(key string) string { + vals := metadata.MD(mc).Get(key) + if len(vals) > 0 { + return vals[0] + } + return "" +} + +// Set stores the key-value pair. +func (mc headerCarrier) Set(key string, value string) { + metadata.MD(mc).Set(key, value) +} + +// Add append value to key-values pair. +func (mc headerCarrier) Add(key string, value string) { + metadata.MD(mc).Append(key, value) +} + +// Keys lists the keys stored in this carrier. +func (mc headerCarrier) Keys() []string { + keys := make([]string, 0, len(mc)) + for k := range metadata.MD(mc) { + keys = append(keys, k) + } + return keys +} + +// Values returns a slice of values associated with the passed key. +func (mc headerCarrier) Values(key string) []string { + return metadata.MD(mc).Get(key) +} diff --git a/transport/grpc/transport_test.go b/transport/grpc/transport_test.go new file mode 100644 index 0000000..07b6161 --- /dev/null +++ b/transport/grpc/transport_test.go @@ -0,0 +1,70 @@ +package grpc + +import ( + "reflect" + "sort" + "testing" + + "github.com/go-kratos/kratos/v3/transport" +) + +func TestTransport_Kind(t *testing.T) { + o := &Transport{} + if !reflect.DeepEqual(transport.KindGRPC, o.Kind()) { + t.Errorf("expect %v, got %v", transport.KindGRPC, o.Kind()) + } +} + +func TestTransport_Endpoint(t *testing.T) { + v := "hello" + o := &Transport{endpoint: v} + if !reflect.DeepEqual(v, o.Endpoint()) { + t.Errorf("expect %v, got %v", v, o.Endpoint()) + } +} + +func TestTransport_Operation(t *testing.T) { + v := "hello" + o := &Transport{operation: v} + if !reflect.DeepEqual(v, o.Operation()) { + t.Errorf("expect %v, got %v", v, o.Operation()) + } +} + +func TestTransport_RequestHeader(t *testing.T) { + v := headerCarrier{} + v.Set("a", "1") + o := &Transport{reqHeader: v} + if !reflect.DeepEqual("1", o.RequestHeader().Get("a")) { + t.Errorf("expect %v, got %v", "1", o.RequestHeader().Get("a")) + } + if !reflect.DeepEqual("", o.RequestHeader().Get("notfound")) { + t.Errorf("expect %v, got %v", "", o.RequestHeader().Get("notfound")) + } +} + +func TestTransport_ReplyHeader(t *testing.T) { + v := headerCarrier{} + v.Set("a", "1") + o := &Transport{replyHeader: v} + if !reflect.DeepEqual("1", o.ReplyHeader().Get("a")) { + t.Errorf("expect %v, got %v", "1", o.ReplyHeader().Get("a")) + } +} + +func TestHeaderCarrier_Keys(t *testing.T) { + v := headerCarrier{} + v.Set("abb", "1") + v.Set("bcc", "2") + want := []string{"abb", "bcc"} + keys := v.Keys() + sort.Slice(want, func(i, j int) bool { + return want[i] < want[j] + }) + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + if !reflect.DeepEqual(want, keys) { + t.Errorf("expect %v, got %v", want, keys) + } +} diff --git a/transport/http/binding.go b/transport/http/binding.go new file mode 100644 index 0000000..92ec664 --- /dev/null +++ b/transport/http/binding.go @@ -0,0 +1,27 @@ +package http + +import ( + "net/http" + "net/url" + + "github.com/go-kratos/kratos/v3/encoding" + "github.com/go-kratos/kratos/v3/encoding/form" + "github.com/go-kratos/kratos/v3/errors" +) + +func bindQuery(vars url.Values, target any) error { + if err := encoding.GetCodec(form.Name).Unmarshal([]byte(vars.Encode()), target); err != nil { + return errors.BadRequest("CODEC", err.Error()) + } + return nil +} + +func bindForm(req *http.Request, target any) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := encoding.GetCodec(form.Name).Unmarshal([]byte(req.Form.Encode()), target); err != nil { + return errors.BadRequest("CODEC", err.Error()) + } + return nil +} diff --git a/transport/http/binding_test.go b/transport/http/binding_test.go new file mode 100644 index 0000000..5adcd4f --- /dev/null +++ b/transport/http/binding_test.go @@ -0,0 +1,136 @@ +package http + +import ( + "errors" + "io" + "net/http" + "net/url" + "reflect" + "strings" + "testing" + + kratoserror "github.com/go-kratos/kratos/v3/errors" +) + +type ( + testBind struct { + Name string `json:"name"` + URL string `json:"url"` + } + testBind2 struct { + Age int `json:"age"` + } +) + +func TestBindQuery(t *testing.T) { + type args struct { + vars url.Values + target any + } + + tests := []struct { + name string + args args + err error + want any + }{ + { + name: "test", + args: args{ + vars: map[string][]string{"name": {"kratos"}, "url": {"https://go-kratos.dev/"}}, + target: &testBind{}, + }, + err: nil, + want: &testBind{"kratos", "https://go-kratos.dev/"}, + }, + { + name: "test1", + args: args{ + vars: map[string][]string{"age": {"kratos"}, "url": {"https://go-kratos.dev/"}}, + target: &testBind2{}, + }, + err: kratoserror.BadRequest("CODEC", "Field Namespace:age ERROR:Invalid Integer Value 'kratos' Type 'int' Namespace 'age'"), + }, + { + name: "test2", + args: args{ + vars: map[string][]string{"age": {"1"}, "url": {"https://go-kratos.dev/"}}, + target: &testBind2{}, + }, + err: nil, + want: &testBind2{Age: 1}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := bindQuery(tt.args.vars, tt.args.target) + if !kratoserror.Is(err, tt.err) { + t.Fatalf("bindQuery() error = %v, err %v", err, tt.err) + } + if err == nil && !reflect.DeepEqual(tt.args.target, tt.want) { + t.Errorf("bindQuery() target = %v, want %v", tt.args.target, tt.want) + } + }) + } +} + +func TestBindForm(t *testing.T) { + type args struct { + req *http.Request + target any + } + + tests := []struct { + name string + args args + err error + want *testBind + }{ + { + name: "error not nil", + args: args{ + req: &http.Request{Method: http.MethodPost}, + target: &testBind{}, + }, + err: errors.New("missing form body"), + want: nil, + }, + { + name: "error is nil", + args: args{ + req: &http.Request{ + Method: http.MethodPost, + Header: http.Header{"Content-Type": {"application/x-www-form-urlencoded; param=value"}}, + Body: io.NopCloser(strings.NewReader("name=kratos&url=https://go-kratos.dev/")), + }, + target: &testBind{}, + }, + err: nil, + want: &testBind{"kratos", "https://go-kratos.dev/"}, + }, + { + name: "error BadRequest", + args: args{ + req: &http.Request{ + Method: http.MethodPost, + Header: http.Header{"Content-Type": {"application/x-www-form-urlencoded; param=value"}}, + Body: io.NopCloser(strings.NewReader("age=a")), + }, + target: &testBind2{}, + }, + err: kratoserror.BadRequest("CODEC", "Field Namespace:age ERROR:Invalid Integer Value 'a' Type 'int' Namespace 'age'"), + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := bindForm(tt.args.req, tt.args.target) + if !reflect.DeepEqual(err, tt.err) { + t.Fatalf("bindForm() error = %v, err %v", err, tt.err) + } + if err == nil && !reflect.DeepEqual(tt.args.target, tt.want) { + t.Errorf("bindForm() target = %v, want %v", tt.args.target, tt.want) + } + }) + } +} diff --git a/transport/http/calloption.go b/transport/http/calloption.go new file mode 100644 index 0000000..4257220 --- /dev/null +++ b/transport/http/calloption.go @@ -0,0 +1,141 @@ +package http + +import ( + "net/http" +) + +const ( + contentTypeJSON = "application/json" + schemeDiscovery = "discovery" + schemeHTTP = "http" + schemeHTTPS = "https" +) + +// CallOption configures a Call before it starts or extracts information from +// a Call after it completes. +type CallOption interface { + // before is called before the call is sent to any server. If before + // returns a non-nil error, the RPC fails with that error. + before(*callInfo) error + + // after is called after the call has completed. after cannot return an + // error, so any failures should be reported via output parameters. + after(*callInfo, *csAttempt) +} + +type callInfo struct { + contentType string + contentTypeSet bool + accept string + operation string + pathTemplate string + headerCarrier *http.Header +} + +// EmptyCallOption does not alter the Call configuration. +// It can be embedded in another structure to carry satellite data for use +// by interceptors. +type EmptyCallOption struct{} + +func (EmptyCallOption) before(*callInfo) error { return nil } +func (EmptyCallOption) after(*callInfo, *csAttempt) {} + +type csAttempt struct { + res *http.Response +} + +// ContentType with request content type. +func ContentType(contentType string) CallOption { + return ContentTypeCallOption{ContentType: contentType} +} + +// ContentTypeCallOption is BodyCallOption +type ContentTypeCallOption struct { + EmptyCallOption + ContentType string +} + +func (o ContentTypeCallOption) before(c *callInfo) error { + c.contentType = o.ContentType + c.contentTypeSet = true + return nil +} + +// Accept sets the request Accept header. +func Accept(contentType string) CallOption { + return AcceptCallOption{ContentType: contentType} +} + +// AcceptCallOption sets the accepted response content type. +type AcceptCallOption struct { + EmptyCallOption + ContentType string +} + +func (o AcceptCallOption) before(c *callInfo) error { + c.accept = o.ContentType + return nil +} + +func defaultCallInfo(path string) callInfo { + return callInfo{ + contentType: contentTypeJSON, + operation: path, + pathTemplate: path, + } +} + +// Operation is serviceMethod call option +func Operation(operation string) CallOption { + return OperationCallOption{Operation: operation} +} + +// OperationCallOption is set ServiceMethod for client call +type OperationCallOption struct { + EmptyCallOption + Operation string +} + +func (o OperationCallOption) before(c *callInfo) error { + c.operation = o.Operation + return nil +} + +// PathTemplate is http path template +func PathTemplate(pattern string) CallOption { + return PathTemplateCallOption{Pattern: pattern} +} + +// PathTemplateCallOption is set path template for client call +type PathTemplateCallOption struct { + EmptyCallOption + Pattern string +} + +func (o PathTemplateCallOption) before(c *callInfo) error { + c.pathTemplate = o.Pattern + return nil +} + +// Header returns a CallOptions that retrieves the http response header +// from server reply. +func Header(header *http.Header) CallOption { + return HeaderCallOption{header: header} +} + +// HeaderCallOption is retrieve response header for client call +type HeaderCallOption struct { + EmptyCallOption + header *http.Header +} + +func (o HeaderCallOption) before(c *callInfo) error { + c.headerCarrier = o.header + return nil +} + +func (o HeaderCallOption) after(_ *callInfo, cs *csAttempt) { + if cs.res != nil && cs.res.Header != nil { + *o.header = cs.res.Header + } +} diff --git a/transport/http/calloption_test.go b/transport/http/calloption_test.go new file mode 100644 index 0000000..36334b5 --- /dev/null +++ b/transport/http/calloption_test.go @@ -0,0 +1,128 @@ +package http + +import ( + "net/http" + "reflect" + "testing" +) + +func TestEmptyCallOptions(t *testing.T) { + e := EmptyCallOption{} + if e.before(&callInfo{}) != nil { + t.Error("EmptyCallOption should be ignored") + } + e.after(&callInfo{}, &csAttempt{}) +} + +func TestContentType(t *testing.T) { + if !reflect.DeepEqual(ContentType("aaa").(ContentTypeCallOption).ContentType, "aaa") { + t.Errorf("want: %v,got: %v", "aaa", ContentType("aaa").(ContentTypeCallOption).ContentType) + } +} + +func TestContentTypeCallOption_before(t *testing.T) { + c := &callInfo{} + err := ContentType("aaa").before(c) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual("aaa", c.contentType) { + t.Errorf("want: %v, got: %v", "aaa", c.contentType) + } +} + +func TestAccept(t *testing.T) { + if !reflect.DeepEqual(Accept("aaa").(AcceptCallOption).ContentType, "aaa") { + t.Errorf("want: %v,got: %v", "aaa", Accept("aaa").(AcceptCallOption).ContentType) + } +} + +func TestAcceptCallOption_before(t *testing.T) { + c := &callInfo{} + err := Accept("aaa").before(c) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual("aaa", c.accept) { + t.Errorf("want: %v, got: %v", "aaa", c.accept) + } +} + +func TestDefaultCallInfo(t *testing.T) { + path := "hi" + rv := defaultCallInfo(path) + if !reflect.DeepEqual(path, rv.pathTemplate) { + t.Errorf("expect %v, got %v", path, rv.pathTemplate) + } + if !reflect.DeepEqual(path, rv.operation) { + t.Errorf("expect %v, got %v", path, rv.operation) + } + if !reflect.DeepEqual("application/json", rv.contentType) { + t.Errorf("expect %v, got %v", "application/json", rv.contentType) + } +} + +func TestOperation(t *testing.T) { + if !reflect.DeepEqual("aaa", Operation("aaa").(OperationCallOption).Operation) { + t.Errorf("want: %v,got: %v", "aaa", Operation("aaa").(OperationCallOption).Operation) + } +} + +func TestOperationCallOption_before(t *testing.T) { + c := &callInfo{} + err := Operation("aaa").before(c) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual("aaa", c.operation) { + t.Errorf("want: %v, got: %v", "aaa", c.operation) + } +} + +func TestPathTemplate(t *testing.T) { + if !reflect.DeepEqual("aaa", PathTemplate("aaa").(PathTemplateCallOption).Pattern) { + t.Errorf("want: %v,got: %v", "aaa", PathTemplate("aaa").(PathTemplateCallOption).Pattern) + } +} + +func TestPathTemplateCallOption_before(t *testing.T) { + c := &callInfo{} + err := PathTemplate("aaa").before(c) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual("aaa", c.pathTemplate) { + t.Errorf("want: %v, got: %v", "aaa", c.pathTemplate) + } +} + +func TestHeader(t *testing.T) { + h := http.Header{"A": []string{"123"}} + if !reflect.DeepEqual(Header(&h).(HeaderCallOption).header.Get("A"), "123") { + t.Errorf("want: %v,got: %v", "123", Header(&h).(HeaderCallOption).header.Get("A")) + } +} + +func TestHeaderCallOption_before(t *testing.T) { + h := http.Header{"A": []string{"123"}} + c := &callInfo{} + o := Header(&h) + err := o.before(c) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(&h, c.headerCarrier) { + t.Errorf("want: %v,got: %v", &h, o.(HeaderCallOption).header) + } +} + +func TestHeaderCallOption_after(t *testing.T) { + h := http.Header{"A": []string{"123"}} + c := &callInfo{} + cs := &csAttempt{res: &http.Response{Header: h}} + o := Header(&h) + o.after(c, cs) + if !reflect.DeepEqual(&h, o.(HeaderCallOption).header) { + t.Errorf("want: %v,got: %v", &h, o.(HeaderCallOption).header) + } +} diff --git a/transport/http/client.go b/transport/http/client.go new file mode 100644 index 0000000..6b902eb --- /dev/null +++ b/transport/http/client.go @@ -0,0 +1,401 @@ +package http + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "net/http" + "time" + + "github.com/go-kratos/kratos/v3/encoding" + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/internal/host" + "github.com/go-kratos/kratos/v3/internal/httputil" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/selector/wrr" + "github.com/go-kratos/kratos/v3/transport" +) + +func init() { + if selector.GlobalSelector() == nil { + selector.SetGlobalSelector(wrr.NewBuilder()) + } +} + +// DecodeErrorFunc is decode error func. +type DecodeErrorFunc func(ctx context.Context, res *http.Response) error + +// EncodeRequestFunc is request encode func. +type EncodeRequestFunc func(ctx context.Context, contentType string, in any) (body []byte, err error) + +// DecodeResponseFunc is response decode func. +type DecodeResponseFunc func(ctx context.Context, res *http.Response, out any) error + +// ClientOption is HTTP client option. +type ClientOption func(*clientOptions) + +// Client is an HTTP transport client. +type clientOptions struct { + ctx context.Context + tlsConf *tls.Config + timeout time.Duration + endpoint string + userAgent string + encoder EncodeRequestFunc + decoder DecodeResponseFunc + errorDecoder DecodeErrorFunc + transport http.RoundTripper + nodeFilters []selector.NodeFilter + discovery registry.Discovery + middleware []middleware.Middleware + block bool + subsetSize int +} + +// WithSubset with client discovery subset size. +// zero value means subset filter disabled +func WithSubset(size int) ClientOption { + return func(o *clientOptions) { + o.subsetSize = size + } +} + +// WithTransport with client transport. +func WithTransport(trans http.RoundTripper) ClientOption { + return func(o *clientOptions) { + o.transport = trans + } +} + +// WithTimeout with client request timeout. +func WithTimeout(d time.Duration) ClientOption { + return func(o *clientOptions) { + o.timeout = d + } +} + +// WithUserAgent with client user agent. +func WithUserAgent(ua string) ClientOption { + return func(o *clientOptions) { + o.userAgent = ua + } +} + +// WithMiddleware with client middleware. +func WithMiddleware(m ...middleware.Middleware) ClientOption { + return func(o *clientOptions) { + o.middleware = m + } +} + +// WithEndpoint with client addr. +func WithEndpoint(endpoint string) ClientOption { + return func(o *clientOptions) { + o.endpoint = endpoint + } +} + +// WithRequestEncoder with client request encoder. +func WithRequestEncoder(encoder EncodeRequestFunc) ClientOption { + return func(o *clientOptions) { + o.encoder = encoder + } +} + +// WithResponseDecoder with client response decoder. +func WithResponseDecoder(decoder DecodeResponseFunc) ClientOption { + return func(o *clientOptions) { + o.decoder = decoder + } +} + +// WithErrorDecoder with client error decoder. +func WithErrorDecoder(errorDecoder DecodeErrorFunc) ClientOption { + return func(o *clientOptions) { + o.errorDecoder = errorDecoder + } +} + +// WithDiscovery with client discovery. +func WithDiscovery(d registry.Discovery) ClientOption { + return func(o *clientOptions) { + o.discovery = d + } +} + +// WithNodeFilter with select filters +func WithNodeFilter(filters ...selector.NodeFilter) ClientOption { + return func(o *clientOptions) { + o.nodeFilters = filters + } +} + +// WithBlock with client block. +func WithBlock() ClientOption { + return func(o *clientOptions) { + o.block = true + } +} + +// WithTLSConfig with tls config. +func WithTLSConfig(c *tls.Config) ClientOption { + return func(o *clientOptions) { + o.tlsConf = c + } +} + +// Client is an HTTP client. +type Client struct { + opts clientOptions + target *Target + r *resolver + cc *http.Client + insecure bool + selector selector.Selector +} + +// NewClient returns an HTTP client. +func NewClient(ctx context.Context, opts ...ClientOption) (*Client, error) { + options := clientOptions{ + ctx: ctx, + timeout: 2000 * time.Millisecond, + encoder: DefaultRequestEncoder, + decoder: DefaultResponseDecoder, + errorDecoder: DefaultErrorDecoder, + transport: http.DefaultTransport, + subsetSize: 25, + } + for _, o := range opts { + o(&options) + } + if options.tlsConf != nil { + if tr, ok := options.transport.(*http.Transport); ok { + cloned := tr.Clone() + cloned.TLSClientConfig = options.tlsConf + options.transport = cloned + } + } + insecure := options.tlsConf == nil + target, err := parseTarget(options.endpoint, insecure) + if err != nil { + return nil, err + } + selector := selector.GlobalSelector().Build() + var r *resolver + if options.discovery != nil { + if target.Scheme == schemeDiscovery { + if r, err = newResolver(ctx, options.discovery, target, selector, options.block, insecure, options.subsetSize); err != nil { + return nil, fmt.Errorf("[http client] new resolver failed for endpoint %q: %w", options.endpoint, err) + } + } else if _, _, err := host.ExtractHostPort(options.endpoint); err != nil { + return nil, fmt.Errorf("[http client] invalid endpoint format %q: %w", options.endpoint, err) + } + } + return &Client{ + opts: options, + target: target, + insecure: insecure, + r: r, + cc: &http.Client{ + Timeout: options.timeout, + Transport: options.transport, + }, + selector: selector, + }, nil +} + +// Invoke makes a rpc call procedure for remote service. +func (client *Client) Invoke(ctx context.Context, method, path string, args any, reply any, opts ...CallOption) error { + var ( + contentType string + body io.Reader + ) + c := defaultCallInfo(path) + for _, o := range opts { + if err := o.before(&c); err != nil { + return err + } + } + if args != nil { + data, err := client.opts.encoder(ctx, c.contentType, args) + if err != nil { + return err + } + contentType = c.contentType + body = bytes.NewReader(data) + } + url := fmt.Sprintf("%s://%s%s", client.target.Scheme, client.target.Authority, path) + req, err := http.NewRequest(method, url, body) + if err != nil { + return err + } + if c.headerCarrier != nil { + req.Header = *c.headerCarrier + } + + if contentType != "" { + req.Header.Set("Content-Type", c.contentType) + } + if c.accept != "" { + req.Header.Set("Accept", c.accept) + } + if client.opts.userAgent != "" { + req.Header.Set("User-Agent", client.opts.userAgent) + } + ctx = transport.NewClientContext(ctx, &Transport{ + endpoint: client.opts.endpoint, + reqHeader: headerCarrier(req.Header), + operation: c.operation, + request: req, + pathTemplate: c.pathTemplate, + }) + return client.invoke(ctx, req, args, reply, c, opts...) +} + +func (client *Client) invoke(ctx context.Context, req *http.Request, args any, reply any, c callInfo, opts ...CallOption) error { + h := func(ctx context.Context, _ any) (any, error) { + res, err := client.do(req.WithContext(ctx)) + if res != nil { + cs := csAttempt{res: res} + for _, o := range opts { + o.after(&c, &cs) + } + } + if err != nil { + return nil, err + } + defer res.Body.Close() + if err := client.opts.decoder(ctx, res, reply); err != nil { + return nil, err + } + return reply, nil + } + var p selector.Peer + ctx = selector.NewPeerContext(ctx, &p) + if len(client.opts.middleware) > 0 { + h = middleware.Chain(client.opts.middleware...)(h) + } + _, err := h(ctx, args) + return err +} + +// Do send an HTTP request and decodes the body of response into target. +// returns an error (of type *Error) if the response status code is not 2xx. +func (client *Client) Do(req *http.Request, opts ...CallOption) (*http.Response, error) { + c := defaultCallInfo(req.URL.Path) + for _, o := range opts { + if err := o.before(&c); err != nil { + return nil, err + } + } + + return client.do(req) +} + +func (client *Client) do(req *http.Request) (*http.Response, error) { + var done func(context.Context, selector.DoneInfo) + if client.r != nil { + var ( + err error + node selector.Node + ) + if node, done, err = client.selector.Select(req.Context(), selector.WithNodeFilter(client.opts.nodeFilters...)); err != nil { + return nil, errors.ServiceUnavailable("NODE_NOT_FOUND", err.Error()) + } + if client.insecure { + req.URL.Scheme = schemeHTTP + } else { + req.URL.Scheme = schemeHTTPS + } + req.URL.Host = node.Address() + req.Host = node.Address() + } + resp, err := client.cc.Do(req) + if err == nil { + t, ok := transport.FromClientContext(req.Context()) + if ok { + ht, ok := t.(*Transport) + if ok { + ht.replyHeader = headerCarrier(resp.Header) + } + } + err = client.opts.errorDecoder(req.Context(), resp) + } + if done != nil { + done(req.Context(), selector.DoneInfo{Err: err}) + } + if err != nil { + return nil, err + } + return resp, nil +} + +// Close tears down the Transport and all underlying connections. +func (client *Client) Close() error { + if client.r != nil { + return client.r.Close() + } + return nil +} + +// DefaultRequestEncoder is an HTTP request encoder. +func DefaultRequestEncoder(_ context.Context, contentType string, in any) ([]byte, error) { + if body, ok := httpBody(in); ok { + return body.GetData(), nil + } + name := httputil.ContentSubtype(contentType) + codec := encoding.GetCodec(name) + if codec == nil { + return nil, errors.BadRequest("CODEC", fmt.Sprintf("unregister Content-Type: %s", contentType)) + } + body, err := codec.Marshal(in) + if err != nil { + return nil, err + } + return body, err +} + +// DefaultResponseDecoder is an HTTP response decoder. +func DefaultResponseDecoder(_ context.Context, res *http.Response, v any) error { + defer res.Body.Close() + data, err := io.ReadAll(res.Body) + if err != nil { + return err + } + if body, ok := httpBody(v); ok { + body.ContentType = res.Header.Get("Content-Type") + body.Data = data + return nil + } + return CodecForResponse(res).Unmarshal(data, v) +} + +// DefaultErrorDecoder is an HTTP error decoder. +func DefaultErrorDecoder(_ context.Context, res *http.Response) error { + if res.StatusCode >= 200 && res.StatusCode <= 299 { + return nil + } + defer res.Body.Close() + data, err := io.ReadAll(res.Body) + if err == nil { + e := new(errors.Error) + if err = CodecForResponse(res).Unmarshal(data, e); err == nil { + e.Code = int32(res.StatusCode) + return e + } + } + return errors.Newf(res.StatusCode, errors.UnknownReason, "").WithCause(err) +} + +// CodecForResponse get encoding.Codec via http.Response +func CodecForResponse(r *http.Response) encoding.Codec { + codec := encoding.GetCodec(httputil.ContentSubtype(r.Header.Get("Content-Type"))) + if codec != nil { + return codec + } + return encoding.GetCodec("json") +} diff --git a/transport/http/client_test.go b/transport/http/client_test.go new file mode 100644 index 0000000..8549294 --- /dev/null +++ b/transport/http/client_test.go @@ -0,0 +1,471 @@ +package http + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "reflect" + "strconv" + "testing" + "time" + + "google.golang.org/genproto/googleapis/api/httpbody" + "google.golang.org/protobuf/types/known/emptypb" + + kratoserrors "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" +) + +type mockRoundTripper struct{} + +func (rt *mockRoundTripper) RoundTrip(_ *http.Request) (resp *http.Response, err error) { + return +} + +type captureRoundTripper struct { + req *http.Request +} + +func (rt *captureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + rt.req = req + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/protojson"}}, + Body: io.NopCloser(bytes.NewBufferString("{}")), + }, nil +} + +type mockCallOption struct { + needErr bool +} + +func (x *mockCallOption) before(_ *callInfo) error { + if x.needErr { + return errors.New("option need return err") + } + return nil +} + +func (x *mockCallOption) after(_ *callInfo, _ *csAttempt) { + log.Println("run in mockCallOption.after") +} + +func TestWithSubset(t *testing.T) { + co := &clientOptions{} + o := WithSubset(1) + o(co) + if co.subsetSize != 1 { + t.Error("expected subset size to be 1") + } +} + +func TestWithTransport(t *testing.T) { + ov := &mockRoundTripper{} + o := WithTransport(ov) + co := &clientOptions{} + o(co) + if !reflect.DeepEqual(co.transport, ov) { + t.Errorf("expected transport to be %v, got %v", ov, co.transport) + } +} + +func TestWithTimeout(t *testing.T) { + ov := 1 * time.Second + o := WithTimeout(ov) + co := &clientOptions{} + o(co) + if !reflect.DeepEqual(co.timeout, ov) { + t.Errorf("expected timeout to be %v, got %v", ov, co.timeout) + } +} + +func TestWithBlock(t *testing.T) { + o := WithBlock() + co := &clientOptions{} + o(co) + if !co.block { + t.Errorf("expected block to be true, got %v", co.block) + } +} + +func TestWithTLSConfig(t *testing.T) { + ov := &tls.Config{} + o := WithTLSConfig(ov) + co := &clientOptions{} + o(co) + if !reflect.DeepEqual(co.tlsConf, ov) { + t.Errorf("expected tls config to be %v, got %v", ov, co.tlsConf) + } +} + +func TestWithUserAgent(t *testing.T) { + ov := "kratos" + o := WithUserAgent(ov) + co := &clientOptions{} + o(co) + if !reflect.DeepEqual(co.userAgent, ov) { + t.Errorf("expected user agent to be %v, got %v", ov, co.userAgent) + } +} + +func TestWithMiddleware(t *testing.T) { + o := &clientOptions{} + v := []middleware.Middleware{ + func(middleware.Handler) middleware.Handler { return nil }, + } + WithMiddleware(v...)(o) + if !reflect.DeepEqual(o.middleware, v) { + t.Errorf("expected middleware to be %v, got %v", v, o.middleware) + } +} + +func TestWithEndpoint(t *testing.T) { + ov := "some-endpoint" + o := WithEndpoint(ov) + co := &clientOptions{} + o(co) + if !reflect.DeepEqual(co.endpoint, ov) { + t.Errorf("expected endpoint to be %v, got %v", ov, co.endpoint) + } +} + +func TestWithRequestEncoder(t *testing.T) { + o := &clientOptions{} + v := func(context.Context, string, any) (body []byte, err error) { + return nil, nil + } + WithRequestEncoder(v)(o) + if o.encoder == nil { + t.Errorf("expected encoder to be not nil") + } +} + +func TestWithResponseDecoder(t *testing.T) { + o := &clientOptions{} + v := func(context.Context, *http.Response, any) error { return nil } + WithResponseDecoder(v)(o) + if o.decoder == nil { + t.Errorf("expected encoder to be not nil") + } +} + +func TestWithErrorDecoder(t *testing.T) { + o := &clientOptions{} + v := func(context.Context, *http.Response) error { return nil } + WithErrorDecoder(v)(o) + if o.errorDecoder == nil { + t.Errorf("expected encoder to be not nil") + } +} + +type mockDiscovery struct{} + +func (*mockDiscovery) GetService(_ context.Context, _ string) ([]*registry.ServiceInstance, error) { + return nil, nil +} + +func (*mockDiscovery) Watch(_ context.Context, _ string) (registry.Watcher, error) { + return &mockWatcher{}, nil +} + +type mockWatcher struct{} + +func (m *mockWatcher) Next() ([]*registry.ServiceInstance, error) { + instance := ®istry.ServiceInstance{ + ID: "1", + Name: "kratos", + Version: "v1", + Metadata: map[string]string{}, + Endpoints: []string{fmt.Sprintf("http://127.0.0.1:9001?isSecure=%s", strconv.FormatBool(false))}, + } + time.Sleep(time.Millisecond * 500) + return []*registry.ServiceInstance{instance}, nil +} + +func (*mockWatcher) Stop() error { + return nil +} + +func TestWithDiscovery(t *testing.T) { + ov := &mockDiscovery{} + o := WithDiscovery(ov) + co := &clientOptions{} + o(co) + if !reflect.DeepEqual(co.discovery, ov) { + t.Errorf("expected discovery to be %v, got %v", ov, co.discovery) + } +} + +func TestWithNodeFilter(t *testing.T) { + ov := func(context.Context, []selector.Node) []selector.Node { + return []selector.Node{&selector.DefaultNode{}} + } + o := WithNodeFilter(ov) + co := &clientOptions{} + o(co) + for _, n := range co.nodeFilters { + ret := n(context.Background(), nil) + if len(ret) != 1 { + t.Errorf("expected node length to be 1, got %v", len(ret)) + } + } +} + +func TestDefaultRequestEncoder(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(`{"a":"1", "b": 2}`))) + r.Header.Set("Content-Type", "application/xml") + + v1 := &struct { + A string `json:"a"` + B int64 `json:"b"` + }{"a", 1} + b, err := DefaultRequestEncoder(context.TODO(), "application/json", v1) + if err != nil { + t.Fatal(err) + } + v1b := &struct { + A string `json:"a"` + B int64 `json:"b"` + }{} + err = json.Unmarshal(b, v1b) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(v1b, v1) { + t.Errorf("expected %v, got %v", v1, v1b) + } +} + +func TestDefaultRequestEncoderHTTPBody(t *testing.T) { + body := &httpbody.HttpBody{Data: []byte("raw request")} + got, err := DefaultRequestEncoder(context.TODO(), "application/octet-stream", body) + if err != nil { + t.Fatal(err) + } + if string(got) != "raw request" { + t.Errorf("expected %v, got %v", "raw request", string(got)) + } +} + +func TestDefaultRequestEncoderUnknownCodec(t *testing.T) { + _, err := DefaultRequestEncoder(context.TODO(), "application/x-unknown", &struct{}{}) + if err == nil { + t.Fatal("expected error") + } + se := new(kratoserrors.Error) + if !errors.As(err, &se) { + t.Fatalf("expected kratos error, got %T", err) + } + if se.Reason != "CODEC" { + t.Errorf("expected %v, got %v", "CODEC", se.Reason) + } +} + +func TestInvokeAcceptHeader(t *testing.T) { + rt := &captureRoundTripper{} + client, err := NewClient(context.Background(), WithEndpoint("127.0.0.1:8888"), WithTransport(rt)) + if err != nil { + t.Fatal(err) + } + err = client.Invoke( + context.Background(), + http.MethodPost, + "/go", + &emptypb.Empty{}, + &emptypb.Empty{}, + Accept("application/protojson"), + ContentType("application/protojson"), + ) + if err != nil { + t.Fatal(err) + } + if got := rt.req.Header.Get("Accept"); got != "application/protojson" { + t.Errorf("expected %v got %v", "application/protojson", got) + } + if got := rt.req.Header.Get("Content-Type"); got != "application/protojson" { + t.Errorf("expected %v got %v", "application/protojson", got) + } +} + +func TestDefaultResponseDecoder(t *testing.T) { + resp1 := &http.Response{ + Header: make(http.Header), + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"a":"1", "b": 2}`)), + } + v1 := &struct { + A string `json:"a"` + B int64 `json:"b"` + }{} + err := DefaultResponseDecoder(context.TODO(), resp1, v1) + if err != nil { + t.Fatal(err) + } + if v1.A != "1" { + t.Errorf("expected %v, got %v", "1", v1.A) + } + if v1.B != int64(2) { + t.Errorf("expected %v, got %v", 2, v1.B) + } + + resp2 := &http.Response{ + Header: make(http.Header), + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString("{badjson}")), + } + v2 := &struct { + A string `json:"a"` + B int64 `json:"b"` + }{} + err = DefaultResponseDecoder(context.TODO(), resp2, v2) + syntaxErr := &json.SyntaxError{} + if !errors.As(err, &syntaxErr) { + t.Errorf("expected %v, got %v", syntaxErr, err) + } +} + +func TestDefaultResponseDecoderHTTPBody(t *testing.T) { + resp := &http.Response{ + Header: http.Header{"Content-Type": []string{"application/pdf"}}, + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString("raw response")), + } + var body *httpbody.HttpBody + if err := DefaultResponseDecoder(context.TODO(), resp, &body); err != nil { + t.Fatal(err) + } + if body.GetContentType() != "application/pdf" { + t.Errorf("expected %v, got %v", "application/pdf", body.GetContentType()) + } + if string(body.GetData()) != "raw response" { + t.Errorf("expected %v, got %v", "raw response", string(body.GetData())) + } +} + +func TestDefaultErrorDecoder(t *testing.T) { + for i := 200; i < 300; i++ { + resp := &http.Response{Header: make(http.Header), StatusCode: i} + if DefaultErrorDecoder(context.TODO(), resp) != nil { + t.Errorf("expected no error, got %v", DefaultErrorDecoder(context.TODO(), resp)) + } + } + resp1 := &http.Response{ + Header: make(http.Header), + StatusCode: 300, + Body: io.NopCloser(bytes.NewBufferString("{\"foo\":\"bar\"}")), + } + if DefaultErrorDecoder(context.TODO(), resp1) == nil { + t.Errorf("expected error, got nil") + } + + resp2 := &http.Response{ + Header: make(http.Header), + StatusCode: 500, + Body: io.NopCloser(bytes.NewBufferString(`{"code":54321, "message": "hi", "reason": "FOO"}`)), + } + err := DefaultErrorDecoder(context.TODO(), resp2) + if err == nil { + t.Errorf("expected error, got nil") + } + if err.(*kratoserrors.Error).Code != int32(500) { + t.Errorf("expected %v, got %v", 500, err.(*kratoserrors.Error).Code) + } + if err.(*kratoserrors.Error).Message != "hi" { + t.Errorf("expected %v, got %v", "hi", err.(*kratoserrors.Error).Message) + } + if err.(*kratoserrors.Error).Reason != "FOO" { + t.Errorf("expected %v, got %v", "FOO", err.(*kratoserrors.Error).Reason) + } +} + +func TestCodecForResponse(t *testing.T) { + resp := &http.Response{Header: make(http.Header)} + resp.Header.Set("Content-Type", "application/xml") + c := CodecForResponse(resp) + if !reflect.DeepEqual("xml", c.Name()) { + t.Errorf("expected %v, got %v", "xml", c.Name()) + } +} + +func TestNewClient(t *testing.T) { + _, err := NewClient(context.Background(), WithEndpoint("127.0.0.1:8888")) + if err != nil { + t.Error(err) + } + _, err = NewClient(context.Background(), WithEndpoint("127.0.0.1:9999"), WithTLSConfig(&tls.Config{ServerName: "www.kratos.com", RootCAs: nil})) + if err != nil { + t.Error(err) + } + _, err = NewClient(context.Background(), WithDiscovery(&mockDiscovery{}), WithEndpoint("discovery:///go-kratos")) + if err != nil { + t.Error(err) + } + _, err = NewClient(context.Background(), WithDiscovery(&mockDiscovery{}), WithEndpoint("127.0.0.1:8888")) + if err != nil { + t.Error(err) + } + _, err = NewClient(context.Background(), WithEndpoint("127.0.0.1:8888:xxxxa")) + if err == nil { + t.Error("except a parseTarget error") + } + _, err = NewClient(context.Background(), WithDiscovery(&mockDiscovery{}), WithEndpoint("https://go-kratos.dev/")) + if err == nil { + t.Error("err should not be equal to nil") + } + + client, err := NewClient( + context.Background(), + WithDiscovery(&mockDiscovery{}), + WithEndpoint("discovery:///go-kratos"), + WithMiddleware(func(handler middleware.Handler) middleware.Handler { + t.Logf("handle in middleware") + return func(ctx context.Context, req any) (any, error) { + return handler(ctx, req) + } + }), + ) + if err != nil { + t.Fatal(err) + } + + err = client.Invoke(context.Background(), http.MethodPost, "/go", map[string]string{"name": "kratos"}, nil, EmptyCallOption{}, &mockCallOption{}) + if err == nil { + t.Error("err should not be equal to nil") + } + err = client.Invoke(context.Background(), http.MethodPost, "/go", map[string]string{"name": "kratos"}, nil, EmptyCallOption{}, &mockCallOption{needErr: true}) + if err == nil { + t.Error("err should be equal to callOption err") + } + client.opts.encoder = func(context.Context, string, any) (body []byte, err error) { + return nil, errors.New("mock test encoder error") + } + err = client.Invoke(context.Background(), http.MethodPost, "/go", map[string]string{"name": "kratos"}, nil, EmptyCallOption{}) + if err == nil { + t.Error("err should be equal to encoder error") + } +} + +func TestNewClientWithTLSDoesNotModifyDefaultTransport(t *testing.T) { + defaultTransport, ok := http.DefaultTransport.(*http.Transport) + if !ok { + t.Skip("http.DefaultTransport is not *http.Transport") + } + originalTLSConfig := defaultTransport.TLSClientConfig + + _, err := NewClient(context.Background(), WithEndpoint("127.0.0.1:9999"), WithTLSConfig(&tls.Config{ServerName: "www.kratos.com"})) + if err != nil { + t.Error(err) + } + + if defaultTransport.TLSClientConfig != originalTLSConfig { + t.Error("NewClient modified http.DefaultTransport.TLSClientConfig") + } +} diff --git a/transport/http/codec.go b/transport/http/codec.go new file mode 100644 index 0000000..7b283d0 --- /dev/null +++ b/transport/http/codec.go @@ -0,0 +1,218 @@ +package http + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + + "github.com/gorilla/mux" + "google.golang.org/genproto/googleapis/api/httpbody" + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v3/encoding" + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/internal/httputil" +) + +// SupportPackageIsVersion3 These constants should not be referenced from any other code. +const SupportPackageIsVersion3 = true + +const defaultHTTPBodyContentType = "application/octet-stream" + +var protoMessageType = reflect.TypeOf((*proto.Message)(nil)).Elem() + +// Redirector replies to the request with a redirect to url +// which may be a path relative to the request path. +type Redirector interface { + error + Redirect() (string, int) +} + +// Request type net/http. +type Request = http.Request + +// ResponseWriter type net/http. +type ResponseWriter = http.ResponseWriter + +// Flusher type net/http +type Flusher = http.Flusher + +// DecodeRequestFunc is decode request func. +type DecodeRequestFunc func(*http.Request, any) error + +// EncodeResponseFunc is encode response func. +type EncodeResponseFunc func(http.ResponseWriter, *http.Request, any) error + +// EncodeErrorFunc is encode error func. +type EncodeErrorFunc func(http.ResponseWriter, *http.Request, error) + +// DefaultRequestVars decodes the request vars to object. +func DefaultRequestVars(r *http.Request, v any) error { + raws := mux.Vars(r) + vars := make(url.Values, len(raws)) + for k, v := range raws { + vars[k] = []string{v} + } + return bindQuery(vars, v) +} + +// DefaultRequestQuery decodes the request vars to object. +func DefaultRequestQuery(r *http.Request, v any) error { + return bindQuery(r.URL.Query(), v) +} + +// DefaultRequestDecoder decodes the request body to object. +func DefaultRequestDecoder(r *http.Request, v any) error { + if body, ok := httpBody(v); ok { + data, err := io.ReadAll(r.Body) + r.Body = io.NopCloser(bytes.NewBuffer(data)) + if err != nil { + return errors.BadRequest("CODEC", err.Error()) + } + body.ContentType = r.Header.Get("Content-Type") + body.Data = data + return nil + } + codec, ok := CodecForRequest(r, "Content-Type") + if !ok { + return errors.BadRequest("CODEC", fmt.Sprintf("unregister Content-Type: %s", r.Header.Get("Content-Type"))) + } + data, err := io.ReadAll(r.Body) + + // reset body. + r.Body = io.NopCloser(bytes.NewBuffer(data)) + + if err != nil { + return errors.BadRequest("CODEC", err.Error()) + } + if len(data) == 0 { + return nil + } + if err = decodeWithCodec(codec, data, v); err != nil { + return errors.BadRequest("CODEC", fmt.Sprintf("body unmarshal %s", err.Error())) + } + return nil +} + +// DefaultResponseEncoder encodes the object to the HTTP response. +func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v any) error { + if v == nil { + return nil + } + if body, ok := httpBody(v); ok { + contentType := body.GetContentType() + if contentType == "" { + contentType = defaultHTTPBodyContentType + } + w.Header().Set("Content-Type", contentType) + _, err := w.Write(body.GetData()) + return err + } + if rd, ok := v.(Redirector); ok { + url, code := rd.Redirect() + http.Redirect(w, r, url, code) + return nil + } + codec, _ := CodecForRequest(r, "Accept") + data, err := codec.Marshal(v) + if err != nil { + return err + } + w.Header().Set("Content-Type", httputil.ContentType(codec.Name())) + _, err = w.Write(data) + if err != nil { + return err + } + return nil +} + +// DefaultErrorEncoder encodes the error to the HTTP response. +func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) { + var rd *redirect + if errors.As(err, &rd) { + url, code := rd.Redirect() + http.Redirect(w, r, url, code) + return + } + se := errors.FromError(err) + codec, _ := CodecForRequest(r, "Accept") + body, err := codec.Marshal(se) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", httputil.ContentType(codec.Name())) + w.WriteHeader(int(se.Code)) + _, _ = w.Write(body) +} + +// CodecForRequest get encoding.Codec via http.Request +func CodecForRequest(r *http.Request, name string) (encoding.Codec, bool) { + for _, accept := range r.Header[name] { + codec := encoding.GetCodec(httputil.ContentSubtype(accept)) + if codec != nil { + return codec, true + } + } + return encoding.GetCodec("json"), false +} + +func httpBody(v any) (*httpbody.HttpBody, bool) { + switch body := v.(type) { + case *httpbody.HttpBody: + return body, body != nil + case **httpbody.HttpBody: + if body == nil { + return nil, false + } + if *body == nil { + *body = new(httpbody.HttpBody) + } + return *body, true + default: + return nil, false + } +} + +func decodeWithCodec(codec encoding.Codec, data []byte, v any) error { + switch codec.Name() { + case "proto", "protojson": + default: + return codec.Unmarshal(data, v) + } + + if msg, ok := v.(proto.Message); ok { + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Pointer && rv.IsNil() { + return codec.Unmarshal(data, v) + } + return codec.Unmarshal(data, msg) + } + + rv := reflect.ValueOf(v) + if !rv.IsValid() || rv.Kind() != reflect.Pointer || rv.IsNil() { + return codec.Unmarshal(data, v) + } + + elem := rv.Type().Elem() + if elem.Kind() != reflect.Pointer || !elem.Implements(protoMessageType) { + return codec.Unmarshal(data, v) + } + + target := rv.Elem() + if target.IsNil() { + target.Set(reflect.New(elem.Elem())) + } + return codec.Unmarshal(data, target.Interface()) +} + +// BodyContentType returns the content type carried by v or a binary default. +func BodyContentType(v any) string { + if body, ok := httpBody(v); ok && body.GetContentType() != "" { + return body.GetContentType() + } + return defaultHTTPBodyContentType +} diff --git a/transport/http/codec_go1.20.go b/transport/http/codec_go1.20.go new file mode 100644 index 0000000..d5a7357 --- /dev/null +++ b/transport/http/codec_go1.20.go @@ -0,0 +1,9 @@ +//go:build go1.20 + +package http + +import "net/http" + +// ResponseController is type net/http.ResponseController which was added in Go 1.20. + +type ResponseController = http.ResponseController diff --git a/transport/http/codec_test.go b/transport/http/codec_test.go new file mode 100644 index 0000000..ec5dcdf --- /dev/null +++ b/transport/http/codec_test.go @@ -0,0 +1,328 @@ +package http + +import ( + "bytes" + "context" + "io" + "net/http" + "strings" + "testing" + + "google.golang.org/genproto/googleapis/api/httpbody" + + "github.com/go-kratos/kratos/v3/encoding" + _ "github.com/go-kratos/kratos/v3/encoding/protojson" + "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/internal/testdata/binding" +) + +func TestDefaultRequestDecoder(t *testing.T) { + var ( + bodyStr = `{"a":"1", "b": 2}` + r, _ = http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(bodyStr))) + ) + r.Header.Set("Content-Type", "application/json") + + v1 := &struct { + A string `json:"a"` + B int64 `json:"b"` + }{} + err := DefaultRequestDecoder(r, &v1) + if err != nil { + t.Fatal(err) + } + if v1.A != "1" { + t.Errorf("expected %v, got %v", "1", v1.A) + } + if v1.B != int64(2) { + t.Errorf("expected %v, got %v", 2, v1.B) + } + + data, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + if bodyStr != string(data) { + t.Errorf("expected %v, got %v", bodyStr, string(data)) + } +} + +func TestDefaultRequestDecoderHTTPBody(t *testing.T) { + const bodyStr = "raw file content" + r, _ := http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(bodyStr))) + r.Header.Set("Content-Type", "text/plain") + + var body *httpbody.HttpBody + if err := DefaultRequestDecoder(r, &body); err != nil { + t.Fatal(err) + } + if body.GetContentType() != "text/plain" { + t.Errorf("expected %v, got %v", "text/plain", body.GetContentType()) + } + if string(body.GetData()) != bodyStr { + t.Errorf("expected %v, got %v", bodyStr, string(body.GetData())) + } + + data, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + if string(data) != bodyStr { + t.Errorf("expected request body reset to %q, got %q", bodyStr, string(data)) + } +} + +func TestDefaultRequestDecoderProtoJSONMessageFieldPointer(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(`{"naming":"go"}`))) + r.Header.Set("Content-Type", "application/protojson") + + var sub *binding.Sub + if err := DefaultRequestDecoder(r, &sub); err != nil { + t.Fatal(err) + } + if sub == nil { + t.Fatal("expected message field to be allocated") + } + if sub.Name != "go" { + t.Errorf("expected %v, got %v", "go", sub.Name) + } +} + +func TestDefaultRequestDecoderProtoJSONRejectsScalarField(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(`"kratos"`))) + r.Header.Set("Content-Type", "application/protojson") + + var name string + err := DefaultRequestDecoder(r, &name) + if err == nil { + t.Fatal("expected scalar protojson body to fail") + } + if !strings.Contains(err.Error(), "want proto.Message") { + t.Errorf("expected proto message type error, got %v", err) + } +} + +func TestDefaultResponseEncoderProtoJSONRejectsScalarField(t *testing.T) { + w := &mockResponseWriter{StatusCode: http.StatusOK, header: make(http.Header)} + r, _ := http.NewRequest(http.MethodGet, "", nil) + r.Header.Set("Accept", "application/protojson") + + err := DefaultResponseEncoder(w, r, "kratos") + if err == nil { + t.Fatal("expected scalar protojson response to fail") + } + if !strings.Contains(err.Error(), "want proto.Message") { + t.Errorf("expected proto message type error, got %v", err) + } +} + +func TestDefaultResponseDecoderProtoJSONMessage(t *testing.T) { + resp := &http.Response{ + Header: http.Header{"Content-Type": []string{"application/protojson"}}, + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`{"naming":"go"}`)), + } + + sub := new(binding.Sub) + if err := DefaultResponseDecoder(context.TODO(), resp, sub); err != nil { + t.Fatal(err) + } + if sub.Name != "go" { + t.Errorf("expected %v, got %v", "go", sub.Name) + } +} + +func TestDefaultResponseDecoderProtoJSONRejectsScalarField(t *testing.T) { + resp := &http.Response{ + Header: http.Header{"Content-Type": []string{"application/protojson"}}, + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`"kratos"`)), + } + + var name string + err := DefaultResponseDecoder(context.TODO(), resp, &name) + if err == nil { + t.Fatal("expected scalar protojson response to fail") + } + if !strings.Contains(err.Error(), "want proto.Message") { + t.Errorf("expected proto message type error, got %v", err) + } +} + +type mockResponseWriter struct { + StatusCode int + Data []byte + header http.Header +} + +func (w *mockResponseWriter) Header() http.Header { + return w.header +} + +func (w *mockResponseWriter) Write(b []byte) (int, error) { + w.Data = b + return len(b), nil +} + +func (w *mockResponseWriter) WriteHeader(statusCode int) { + w.StatusCode = statusCode +} + +type errorCodec struct{} + +func (errorCodec) Marshal(any) ([]byte, error) { + return nil, errors.New(500, "mock", "marshal error") +} + +func (errorCodec) Unmarshal([]byte, any) error { + return nil +} + +func (errorCodec) Name() string { + return "mock" +} + +func TestDefaultResponseEncoder(t *testing.T) { + var ( + w = &mockResponseWriter{StatusCode: 200, header: make(http.Header)} + r, _ = http.NewRequest(http.MethodPost, "", nil) + v = &struct { + A string `json:"a"` + B int64 `json:"b"` + }{ + A: "1", + B: 2, + } + ) + r.Header.Set("Content-Type", "application/json") + + err := DefaultResponseEncoder(w, r, v) + if err != nil { + t.Fatal(err) + } + if w.Header().Get("Content-Type") != "application/json" { + t.Errorf("expected %v, got %v", "application/json", w.Header().Get("Content-Type")) + } + if w.StatusCode != 200 { + t.Errorf("expected %v, got %v", 200, w.StatusCode) + } + if w.Data == nil { + t.Errorf("expected not nil, got %v", w.Data) + } +} + +func TestDefaultResponseEncoderHTTPBody(t *testing.T) { + w := &mockResponseWriter{StatusCode: 200, header: make(http.Header)} + r, _ := http.NewRequest(http.MethodGet, "", nil) + body := &httpbody.HttpBody{ + ContentType: "application/octet-stream", + Data: []byte("raw response"), + } + + if err := DefaultResponseEncoder(w, r, body); err != nil { + t.Fatal(err) + } + if got := w.Header().Get("Content-Type"); got != "application/octet-stream" { + t.Errorf("expected %v, got %v", "application/octet-stream", got) + } + if string(w.Data) != "raw response" { + t.Errorf("expected %v, got %v", "raw response", string(w.Data)) + } +} + +func TestDefaultErrorEncoder(t *testing.T) { + var ( + w = &mockResponseWriter{header: make(http.Header)} + r, _ = http.NewRequest(http.MethodPost, "", nil) + err = errors.New(511, "", "") + ) + r.Header.Set("Content-Type", "application/json") + + DefaultErrorEncoder(w, r, err) + if w.Header().Get("Content-Type") != "application/json" { + t.Errorf("expected %v, got %v", "application/json", w.Header().Get("Content-Type")) + } + if w.StatusCode != 511 { + t.Errorf("expected %v, got %v", 511, w.StatusCode) + } + if w.Data == nil { + t.Errorf("expected not nil, got %v", w.Data) + } +} + +func TestDefaultErrorEncoderRedirect(t *testing.T) { + w := &mockResponseWriter{header: make(http.Header)} + r, _ := http.NewRequest(http.MethodGet, "/test", nil) + + DefaultErrorEncoder(w, r, NewRedirect("/redirect", http.StatusTemporaryRedirect)) + + if w.StatusCode != http.StatusTemporaryRedirect { + t.Errorf("expected %v, got %v", http.StatusTemporaryRedirect, w.StatusCode) + } + if w.Header().Get("Location") != "/redirect" { + t.Errorf("expected %v, got %v", "/redirect", w.Header().Get("Location")) + } +} + +func TestDefaultErrorEncoderMarshalError(t *testing.T) { + encoding.RegisterCodec(errorCodec{}) + w := &mockResponseWriter{header: make(http.Header)} + r, _ := http.NewRequest(http.MethodGet, "", nil) + r.Header.Set("Accept", "application/mock") + + DefaultErrorEncoder(w, r, errors.New(500, "mock", "marshal error")) + + if w.StatusCode != http.StatusInternalServerError { + t.Errorf("expected %v, got %v", http.StatusInternalServerError, w.StatusCode) + } + if w.Header().Get("Content-Type") != "" { + t.Errorf("expected empty content type, got %v", w.Header().Get("Content-Type")) + } + if w.Data != nil { + t.Errorf("expected nil, got %v", w.Data) + } +} + +func TestDefaultResponseEncoderEncodeNil(t *testing.T) { + var ( + w = &mockResponseWriter{StatusCode: 204, header: make(http.Header)} + r, _ = http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(""))) + ) + r.Header.Set("Content-Type", "application/json") + + err := DefaultResponseEncoder(w, r, nil) + if err != nil { + t.Fatal(err) + } + if w.Header().Get("Content-Type") != "" { + t.Errorf("expected empty string, got %v", w.Header().Get("Content-Type")) + } + if w.StatusCode != 204 { + t.Errorf("expected %v, got %v", 204, w.StatusCode) + } + if w.Data != nil { + t.Errorf("expected nil, got %v", w.Data) + } +} + +func TestCodecForRequest(t *testing.T) { + r, _ := http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(""))) + r.Header.Set("Content-Type", "application/xml") + c, ok := CodecForRequest(r, "Content-Type") + if !ok { + t.Fatalf("expected true, got %v", ok) + } + if c.Name() != "xml" { + t.Errorf("expected %v, got %v", "xml", c.Name()) + } + + r, _ = http.NewRequest(http.MethodPost, "", io.NopCloser(bytes.NewBufferString(`{"a":"1", "b": 2}`))) + r.Header.Set("Content-Type", "blablablabla") + c, ok = CodecForRequest(r, "Content-Type") + if ok { + t.Fatalf("expected false, got %v", ok) + } + if c.Name() != "json" { + t.Errorf("expected %v, got %v", "json", c.Name()) + } +} diff --git a/transport/http/context.go b/transport/http/context.go new file mode 100644 index 0000000..478b300 --- /dev/null +++ b/transport/http/context.go @@ -0,0 +1,186 @@ +package http + +import ( + "context" + "encoding/json" + "encoding/xml" + "io" + "net/http" + "net/url" + "time" + + "github.com/gorilla/mux" + + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +var _ Context = (*wrapper)(nil) + +// Context is an HTTP Context. +type Context interface { + context.Context + Vars() url.Values + Query() url.Values + Form() url.Values + Header() http.Header + Request() *http.Request + Response() http.ResponseWriter + Middleware(middleware.Handler) middleware.Handler + Bind(any) error + BindVars(any) error + BindQuery(any) error + BindForm(any) error + Returns(any, error) error + Result(int, any) error + JSON(int, any) error + XML(int, any) error + String(int, string) error + Blob(int, string, []byte) error + Stream(int, string, io.Reader) error + Reset(http.ResponseWriter, *http.Request) +} + +type responseWriter struct { + code int + w http.ResponseWriter +} + +func (w *responseWriter) reset(res http.ResponseWriter) { + w.w = res + w.code = http.StatusOK +} +func (w *responseWriter) Header() http.Header { return w.w.Header() } +func (w *responseWriter) WriteHeader(statusCode int) { w.code = statusCode } +func (w *responseWriter) Write(data []byte) (int, error) { + w.w.WriteHeader(w.code) + return w.w.Write(data) +} +func (w *responseWriter) Unwrap() http.ResponseWriter { return w.w } + +type wrapper struct { + router *Router + req *http.Request + res http.ResponseWriter + w responseWriter +} + +func (c *wrapper) Header() http.Header { + return c.req.Header +} + +func (c *wrapper) Vars() url.Values { + raws := mux.Vars(c.req) + vars := make(url.Values, len(raws)) + for k, v := range raws { + vars[k] = []string{v} + } + return vars +} + +func (c *wrapper) Form() url.Values { + if err := c.req.ParseForm(); err != nil { + return url.Values{} + } + return c.req.Form +} + +func (c *wrapper) Query() url.Values { + return c.req.URL.Query() +} +func (c *wrapper) Request() *http.Request { return c.req } +func (c *wrapper) Response() http.ResponseWriter { return c.res } +func (c *wrapper) Middleware(h middleware.Handler) middleware.Handler { + if tr, ok := transport.FromServerContext(c.req.Context()); ok { + return middleware.Chain(c.router.srv.middleware.Match(tr.Operation())...)(h) + } + return middleware.Chain(c.router.srv.middleware.Match(c.req.URL.Path)...)(h) +} +func (c *wrapper) Bind(v any) error { return c.router.srv.decBody(c.req, v) } +func (c *wrapper) BindVars(v any) error { return c.router.srv.decVars(c.req, v) } +func (c *wrapper) BindQuery(v any) error { return c.router.srv.decQuery(c.req, v) } +func (c *wrapper) BindForm(v any) error { return bindForm(c.req, v) } +func (c *wrapper) Returns(v any, err error) error { + if err != nil { + return err + } + return c.router.srv.enc(&c.w, c.req, v) +} + +func (c *wrapper) Result(code int, v any) error { + c.w.WriteHeader(code) + return c.router.srv.enc(&c.w, c.req, v) +} + +func (c *wrapper) JSON(code int, v any) error { + c.res.Header().Set("Content-Type", contentTypeJSON) + c.res.WriteHeader(code) + return json.NewEncoder(c.res).Encode(v) +} + +func (c *wrapper) XML(code int, v any) error { + c.res.Header().Set("Content-Type", "application/xml") + c.res.WriteHeader(code) + return xml.NewEncoder(c.res).Encode(v) +} + +func (c *wrapper) String(code int, text string) error { + c.res.Header().Set("Content-Type", "text/plain") + c.res.WriteHeader(code) + _, err := c.res.Write([]byte(text)) + if err != nil { + return err + } + return nil +} + +func (c *wrapper) Blob(code int, contentType string, data []byte) error { + c.res.Header().Set("Content-Type", contentType) + c.res.WriteHeader(code) + _, err := c.res.Write(data) + if err != nil { + return err + } + return nil +} + +func (c *wrapper) Stream(code int, contentType string, rd io.Reader) error { + c.res.Header().Set("Content-Type", contentType) + c.res.WriteHeader(code) + _, err := io.Copy(c.res, rd) + return err +} + +func (c *wrapper) Reset(res http.ResponseWriter, req *http.Request) { + c.w.reset(res) + c.res = res + c.req = req +} + +func (c *wrapper) Deadline() (time.Time, bool) { + if c.req == nil { + return time.Time{}, false + } + return c.req.Context().Deadline() +} + +func (c *wrapper) Done() <-chan struct{} { + if c.req == nil { + return nil + } + return c.req.Context().Done() +} + +func (c *wrapper) Err() error { + if c.req == nil { + return context.Canceled + } + return c.req.Context().Err() +} + +func (c *wrapper) Value(key any) any { + if c.req == nil { + return nil + } + return c.req.Context().Value(key) +} diff --git a/transport/http/context_test.go b/transport/http/context_test.go new file mode 100644 index 0000000..bb95c7c --- /dev/null +++ b/transport/http/context_test.go @@ -0,0 +1,251 @@ +package http + +import ( + "bytes" + "context" + "errors" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + "time" +) + +var testRouter = &Router{srv: NewServer()} + +func TestContextHeader(t *testing.T) { + w := wrapper{ + router: testRouter, + req: &http.Request{Header: map[string][]string{"name": {"kratos"}}}, + res: nil, + w: responseWriter{}, + } + h := w.Header() + if !reflect.DeepEqual(h, http.Header{"name": {"kratos"}}) { + t.Errorf("expected %v, got %v", http.Header{"name": {"kratos"}}, h) + } +} + +func TestContextForm(t *testing.T) { + w := wrapper{ + router: testRouter, + req: &http.Request{Header: map[string][]string{"name": {"kratos"}}, Method: http.MethodPost}, + res: nil, + w: responseWriter{}, + } + form := w.Form() + if !reflect.DeepEqual(form, url.Values{}) { + t.Errorf("expected %v, got %v", url.Values{}, form) + } + + w = wrapper{ + router: testRouter, + req: &http.Request{Form: map[string][]string{"name": {"kratos"}}}, + res: nil, + w: responseWriter{}, + } + form = w.Form() + if !reflect.DeepEqual(form, url.Values{"name": {"kratos"}}) { + t.Errorf("expected %v, got %v", url.Values{"name": {"kratos"}}, form) + } +} + +func TestContextQuery(t *testing.T) { + w := wrapper{ + router: testRouter, + req: &http.Request{URL: &url.URL{Scheme: "https", Host: "github.com", Path: "go-kratos/kratos", RawQuery: "page=1"}, Method: http.MethodPost}, + res: nil, + w: responseWriter{}, + } + q := w.Query() + if !reflect.DeepEqual(q, url.Values{"page": {"1"}}) { + t.Errorf("expected %v, got %v", url.Values{"page": {"1"}}, q) + } +} + +func TestContextRequest(t *testing.T) { + req := &http.Request{Method: http.MethodPost} + w := wrapper{ + router: testRouter, + req: req, + res: nil, + w: responseWriter{}, + } + res := w.Request() + if !reflect.DeepEqual(res, req) { + t.Errorf("expected %v, got %v", req, res) + } +} + +func TestContextResponse(t *testing.T) { + res := httptest.NewRecorder() + w := wrapper{ + router: &Router{srv: &Server{enc: DefaultResponseEncoder}}, + req: &http.Request{Method: http.MethodPost}, + res: res, + w: responseWriter{200, res}, + } + if !reflect.DeepEqual(w.Response(), res) { + t.Errorf("expected %v, got %v", res, w.Response()) + } + err := w.Returns(map[string]string{}, nil) + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + needErr := errors.New("some error") + err = w.Returns(map[string]string{}, needErr) + if !errors.Is(err, needErr) { + t.Errorf("expected %v, got %v", needErr, err) + } +} + +func TestResponseUnwrap(t *testing.T) { + res := httptest.NewRecorder() + f := func(rw http.ResponseWriter, _ *http.Request, _ any) error { + u, ok := rw.(interface { + Unwrap() http.ResponseWriter + }) + if !ok { + return errors.New("can not unwrap") + } + w := u.Unwrap() + if !reflect.DeepEqual(w, res) { + return errors.New("underlying response writer not equal") + } + return nil + } + + w := wrapper{ + router: &Router{srv: &Server{enc: f}}, + req: nil, + res: res, + w: responseWriter{200, res}, + } + err := w.Result(200, "ok") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } +} + +func TestContextBindQuery(t *testing.T) { + w := wrapper{ + router: testRouter, + req: &http.Request{URL: &url.URL{Scheme: "https", Host: "go-kratos-dev", RawQuery: "page=2"}}, + res: nil, + w: responseWriter{}, + } + type BindQuery struct { + Page int `json:"page"` + } + b := BindQuery{} + err := w.BindQuery(&b) + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + if !reflect.DeepEqual(b, BindQuery{Page: 2}) { + t.Errorf("expected %v, got %v", BindQuery{Page: 2}, b) + } +} + +func TestContextBindForm(t *testing.T) { + w := wrapper{ + router: testRouter, + req: &http.Request{URL: &url.URL{Scheme: "https", Host: "go-kratos-dev"}, Form: map[string][]string{"page": {"2"}}}, + res: nil, + w: responseWriter{}, + } + type BindForm struct { + Page int `json:"page"` + } + b := BindForm{} + err := w.BindForm(&b) + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + if !reflect.DeepEqual(b, BindForm{Page: 2}) { + t.Errorf("expected %v, got %v", BindForm{Page: 2}, b) + } +} + +func TestContextResponseReturn(t *testing.T) { + writer := httptest.NewRecorder() + w := wrapper{ + router: testRouter, + req: nil, + res: writer, + w: responseWriter{}, + } + err := w.JSON(200, "success") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + err = w.XML(200, "success") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + err = w.String(200, "success") + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + err = w.Blob(200, "blob", []byte("success")) + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + err = w.Stream(200, "stream", bytes.NewBuffer([]byte("success"))) + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } +} + +func TestContextCtx(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + req := &http.Request{Method: http.MethodPost} + req = req.WithContext(ctx) + w := wrapper{ + router: testRouter, + req: req, + res: nil, + w: responseWriter{}, + } + _, ok := w.Deadline() + if !ok { + t.Errorf("expected %v, got %v", true, ok) + } + done := w.Done() + if done == nil { + t.Errorf("expected %v, got %v", true, ok) + } + err := w.Err() + if err != nil { + t.Errorf("expected %v, got %v", nil, err) + } + v := w.Value("test") + if v != nil { + t.Errorf("expected %v, got %v", nil, v) + } + + w = wrapper{ + router: &Router{srv: &Server{enc: DefaultResponseEncoder}}, + req: nil, + res: nil, + w: responseWriter{}, + } + _, ok = w.Deadline() + if ok { + t.Errorf("expected %v, got %v", false, ok) + } + done = w.Done() + if done != nil { + t.Errorf("expected not nil, got %v", done) + } + err = w.Err() + if err == nil { + t.Errorf("expected not %v, got %v", nil, err) + } + v = w.Value("test") + if v != nil { + t.Errorf("expected %v, got %v", nil, v) + } +} diff --git a/transport/http/filter.go b/transport/http/filter.go new file mode 100644 index 0000000..43bec80 --- /dev/null +++ b/transport/http/filter.go @@ -0,0 +1,16 @@ +package http + +import "net/http" + +// FilterFunc is a function which receives a http.Handler and returns another http.Handler. +type FilterFunc func(http.Handler) http.Handler + +// FilterChain returns a FilterFunc that specifies the chained handler for HTTP Router. +func FilterChain(filters ...FilterFunc) FilterFunc { + return func(next http.Handler) http.Handler { + for i := len(filters) - 1; i >= 0; i-- { + next = filters[i](next) + } + return next + } +} diff --git a/transport/http/path.go b/transport/http/path.go new file mode 100644 index 0000000..c670076 --- /dev/null +++ b/transport/http/path.go @@ -0,0 +1,93 @@ +package http + +import ( + "reflect" + "regexp" + "strings" + + "google.golang.org/protobuf/proto" + + "github.com/go-kratos/kratos/v3/encoding/form" +) + +var pathTemplateParamRE = regexp.MustCompile(`{([.\w]+)(=[^{}]*)?}`) + +// BuildPathOption configures path construction. +type BuildPathOption func(*buildPathOptions) + +type buildPathOptions struct { + queryParams bool + omitFields []string +} + +// WithQueryParams appends request fields that are not bound in the path as query parameters. +func WithQueryParams() BuildPathOption { + return func(o *buildPathOptions) { + o.queryParams = true + } +} + +// WithOmitFields excludes fields from generated query parameters. +func WithOmitFields(fields ...string) BuildPathOption { + return func(o *buildPathOptions) { + o.omitFields = append(o.omitFields, fields...) + } +} + +// BuildPath builds an HTTP request path from a path template and request message. +func BuildPath(pathTemplate string, msg any, opts ...BuildPathOption) string { + if msg == nil || (reflect.ValueOf(msg).Kind() == reflect.Pointer && reflect.ValueOf(msg).IsNil()) { + return pathTemplate + } + + options := buildPathOptions{} + for _, opt := range opts { + opt(&options) + } + + queryParams, _ := form.EncodeValues(msg) + pathParams := make(map[string]struct{}) + path := pathTemplate + if strings.ContainsRune(pathTemplate, '{') { + path = pathTemplateParamRE.ReplaceAllStringFunc(pathTemplate, func(in string) string { + matches := pathTemplateParamRE.FindStringSubmatch(in) + key := matches[1] + pathParams[key] = struct{}{} + return queryParams.Get(key) + }) + } + + if !options.queryParams { + if v, ok := msg.(proto.Message); ok { + if query := form.EncodeFieldMask(v.ProtoReflect()); query != "" { + return path + "?" + query + } + } + return path + } + if len(queryParams) > 0 { + for key := range pathParams { + delete(queryParams, key) + } + omitQueryParams(queryParams, options.omitFields) + if query := queryParams.Encode(); query != "" { + path += "?" + query + } + } + return path +} + +func omitQueryParams(values map[string][]string, fields []string) { + for _, field := range fields { + if field == "" { + continue + } + delete(values, field) + prefix := field + "." + for key := range values { + if strings.HasPrefix(key, prefix) { + delete(values, key) + } + } + } +} diff --git a/transport/http/path_test.go b/transport/http/path_test.go new file mode 100644 index 0000000..31e5e53 --- /dev/null +++ b/transport/http/path_test.go @@ -0,0 +1,233 @@ +package http + +import ( + "testing" + + "google.golang.org/protobuf/types/known/fieldmaskpb" + + "github.com/go-kratos/kratos/v3/internal/testdata/binding" +) + +func TestBuildPath(t *testing.T) { + tests := []struct { + name string + pathTemplate string + request *binding.HelloRequest + opts []BuildPathOption + want string + }{ + { + name: "path", + pathTemplate: "/helloworld/{name}", + request: &binding.HelloRequest{Name: "kratos"}, + want: "/helloworld/kratos", + }, + { + name: "query", + pathTemplate: "/helloworld/{name}", + request: &binding.HelloRequest{Name: "kratos", Sub: &binding.Sub{Name: "go"}}, + opts: []BuildPathOption{WithQueryParams()}, + want: "/helloworld/kratos?sub.naming=go", + }, + { + name: "resource name", + pathTemplate: "/v1/{name=publishers/*/books/*}", + request: &binding.HelloRequest{Name: "publishers/go/books/kratos"}, + opts: []BuildPathOption{WithQueryParams()}, + want: "/v1/publishers/go/books/kratos", + }, + { + name: "omit body field query params", + pathTemplate: "/helloworld/{name}", + request: &binding.HelloRequest{Name: "kratos", Sub: &binding.Sub{Name: "go"}}, + opts: []BuildPathOption{WithQueryParams(), WithOmitFields("sub")}, + want: "/helloworld/kratos", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := BuildPath(tt.pathTemplate, tt.request, tt.opts...); got != tt.want { + t.Errorf("expected %s got %s", tt.want, got) + } + }) + } +} + +func TestBuildPathCompatibility(t *testing.T) { + tests := []struct { + pathTemplate string + request *binding.HelloRequest + opts []BuildPathOption + want string + }{ + { + pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.naming}", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "2233!!!!"}}, + want: "http://helloworld.Greeter/helloworld/test/sub/2233!!!!", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.naming}", + request: nil, + want: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.naming}", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{}/sub/{sub.naming}", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "hello"}}, + want: "http://helloworld.Greeter/helloworld/{}/sub/hello", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{}/sub/{sub.name.cc}", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "hello"}}, + want: "http://helloworld.Greeter/helloworld/{}/sub/", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{}/sub/{test_repeated}", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "hello"}, TestRepeated: []string{"123", "456"}}, + want: "http://helloworld.Greeter/helloworld/{}/sub/123", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.naming}", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "5566!!!"}}, + want: "http://helloworld.Greeter/helloworld/test/sub/5566!!!", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/sub", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "2233!!!"}}, + want: "http://helloworld.Greeter/helloworld/sub", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.name}", + request: &binding.HelloRequest{Name: "test"}, + want: "http://helloworld.Greeter/helloworld/test/sub/", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub", + request: &binding.HelloRequest{Name: "go", Sub: &binding.Sub{Name: "kratos"}}, + opts: []BuildPathOption{WithQueryParams()}, + want: "http://helloworld.Greeter/helloworld/go/sub?sub.naming=kratos", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{name=publishers/*/books/*}/sub", + request: &binding.HelloRequest{Name: "publishers/go/books/kratos", Sub: &binding.Sub{Name: "kratos"}}, + opts: []BuildPathOption{WithQueryParams()}, + want: "http://helloworld.Greeter/helloworld/publishers/go/books/kratos/sub?sub.naming=kratos", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{name=**}/sub", + request: &binding.HelloRequest{Name: "publishers/go/books/kratos", Sub: &binding.Sub{Name: "kratos"}}, + opts: []BuildPathOption{WithQueryParams()}, + want: "http://helloworld.Greeter/helloworld/publishers/go/books/kratos/sub?sub.naming=kratos", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{sub.naming=publishers/*}", + request: &binding.HelloRequest{Sub: &binding.Sub{Name: "publishers/kratos"}}, + want: "http://helloworld.Greeter/helloworld/publishers/kratos", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/sub/{sub.naming}", + request: &binding.HelloRequest{Sub: &binding.Sub{Name: "kratos"}, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"name", "sub.naming"}}}, + want: "http://helloworld.Greeter/helloworld/sub/kratos?updateMask=name,sub.naming", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/sub/[{sub.naming}]", + request: &binding.HelloRequest{Sub: &binding.Sub{Name: "kratos"}}, + want: "http://helloworld.Greeter/helloworld/sub/[kratos]", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/[{name}]/sub/[{sub.naming}]", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "kratos"}}, + want: "http://helloworld.Greeter/helloworld/[test]/sub/[kratos]", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/[{}]/sub/[{sub.naming}]", + request: &binding.HelloRequest{Sub: &binding.Sub{Name: "kratos"}}, + want: "http://helloworld.Greeter/helloworld/[{}]/sub/[kratos]", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/[{}]/sub/[{sub.naming}]/{[]}", + request: &binding.HelloRequest{Sub: &binding.Sub{Name: "kratos"}}, + want: "http://helloworld.Greeter/helloworld/[{}]/sub/[kratos]/{[]}", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{[sub]}/[{sub.naming}]", + request: &binding.HelloRequest{Sub: &binding.Sub{Name: "kratos"}}, + want: "http://helloworld.Greeter/helloworld/{[sub]}/[kratos]", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{[name]}/[{sub.naming}]", + request: &binding.HelloRequest{Name: "test", Sub: &binding.Sub{Name: "kratos"}}, + want: "http://helloworld.Greeter/helloworld/{[name]}/[kratos]", + }, + { + pathTemplate: "http://helloworld.Greeter/helloworld/{}/[]/[{sub.naming}]", + request: &binding.HelloRequest{Sub: &binding.Sub{Name: "kratos"}}, + want: "http://helloworld.Greeter/helloworld/{}/[]/[kratos]", + }, + } + + for _, test := range tests { + if path := BuildPath(test.pathTemplate, test.request, test.opts...); path != test.want { + t.Fatalf("want: %s, got: %s", test.want, path) + } + } +} + +func BenchmarkBuildPath(b *testing.B) { + benchmarks := []struct { + name string + pathTemplate string + msg *binding.HelloRequest + opts []BuildPathOption + }{ + { + name: "NoParams", + pathTemplate: "http://helloworld.Greeter/helloworld/sub", + msg: &binding.HelloRequest{ + Name: "test", + Sub: &binding.Sub{Name: "kratos"}, + }, + }, + { + name: "NoParamsWithQuery", + pathTemplate: "http://helloworld.Greeter/helloworld/sub", + msg: &binding.HelloRequest{ + Name: "test", + Sub: &binding.Sub{Name: "kratos"}, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"name", "sub.naming"}, + }, + }, + opts: []BuildPathOption{WithQueryParams()}, + }, + { + name: "WithParams", + pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.naming}", + msg: &binding.HelloRequest{ + Name: "test", + Sub: &binding.Sub{Name: "kratos"}, + }, + }, + { + name: "WithParamsAndQuery", + pathTemplate: "http://helloworld.Greeter/helloworld/{name}/sub/{sub.naming}", + msg: &binding.HelloRequest{ + Name: "test", + Sub: &binding.Sub{Name: "kratos"}, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"name", "sub.naming"}, + }, + }, + opts: []BuildPathOption{WithQueryParams()}, + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + BuildPath(bm.pathTemplate, bm.msg, bm.opts...) + } + }) + } +} diff --git a/transport/http/pprof/pprof.go b/transport/http/pprof/pprof.go new file mode 100644 index 0000000..eab56bf --- /dev/null +++ b/transport/http/pprof/pprof.go @@ -0,0 +1,17 @@ +package pprof + +import ( + "net/http" + "net/http/pprof" +) + +// NewHandler new a pprof handler. +func NewHandler() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + return mux +} diff --git a/transport/http/redirect.go b/transport/http/redirect.go new file mode 100644 index 0000000..d1ce62a --- /dev/null +++ b/transport/http/redirect.go @@ -0,0 +1,22 @@ +package http + +type redirect struct { + URL string + Code int +} + +func (r *redirect) Redirect() (string, int) { + return r.URL, r.Code +} + +func (r *redirect) Error() string { + return "redirect to " + r.URL +} + +// NewRedirect new a redirect with url, which may be a path relative to the request path. +// The provided code should be in the 3xx range and is usually StatusMovedPermanently, StatusFound or StatusSeeOther. +// If the Content-Type header has not been set, Redirect sets it to "text/html; charset=utf-8" and writes a small HTML body. +// Setting the Content-Type header to any value, including nil, disables that behavior. +func NewRedirect(url string, code int) Redirector { + return &redirect{URL: url, Code: code} +} diff --git a/transport/http/redirect_test.go b/transport/http/redirect_test.go new file mode 100644 index 0000000..879966a --- /dev/null +++ b/transport/http/redirect_test.go @@ -0,0 +1,24 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestRedirect(t *testing.T) { + var ( + redirectURL = "/redirect" + redirectCode = 302 + ) + r := httptest.NewRequest(http.MethodPost, "/test", nil) + w := httptest.NewRecorder() + _ = DefaultResponseEncoder(w, r, NewRedirect(redirectURL, redirectCode)) + + if w.Code != redirectCode { + t.Fatalf("want %d but got %d", redirectCode, w.Code) + } + if v := w.Header().Get("Location"); v != redirectURL { + t.Fatalf("want %s but got %s", redirectURL, v) + } +} diff --git a/transport/http/resolver.go b/transport/http/resolver.go new file mode 100644 index 0000000..eb22520 --- /dev/null +++ b/transport/http/resolver.go @@ -0,0 +1,154 @@ +package http + +import ( + "context" + "errors" + "net/url" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/go-kratos/kratos/v3/internal/endpoint" + "github.com/go-kratos/kratos/v3/internal/subset" + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" +) + +// Target is resolver target +type Target struct { + Scheme string + Authority string + Endpoint string +} + +func parseTarget(endpoint string, insecure bool) (*Target, error) { + if !strings.Contains(endpoint, "://") { + if insecure { + endpoint = schemeHTTP + "://" + endpoint + } else { + endpoint = schemeHTTPS + "://" + endpoint + } + } + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + target := &Target{Scheme: u.Scheme, Authority: u.Host} + if len(u.Path) > 1 { + target.Endpoint = u.Path[1:] + } + return target, nil +} + +type resolver struct { + rebalancer selector.Rebalancer + + target *Target + watcher registry.Watcher + selectorKey string + subsetSize int + + insecure bool +} + +func newResolver(ctx context.Context, discovery registry.Discovery, target *Target, + rebalancer selector.Rebalancer, block, insecure bool, subsetSize int, +) (*resolver, error) { + // this is new resolver + watcher, err := discovery.Watch(ctx, target.Endpoint) + if err != nil { + return nil, err + } + r := &resolver{ + target: target, + watcher: watcher, + rebalancer: rebalancer, + insecure: insecure, + selectorKey: uuid.New().String(), + subsetSize: subsetSize, + } + if block { + done := make(chan error, 1) + go func() { + for { + services, err := watcher.Next() + if err != nil { + done <- err + return + } + if r.update(services) { + done <- nil + return + } + } + }() + select { + case err := <-done: + if err != nil { + stopErr := watcher.Stop() + if stopErr != nil { + log.Error("failed to stop http client watcher", "target", target, "error", stopErr) + } + return nil, err + } + case <-ctx.Done(): + log.Error("http client watch service reached context deadline", "target", target) + stopErr := watcher.Stop() + if stopErr != nil { + log.Error("failed to stop http client watcher", "target", target, "error", stopErr) + } + return nil, ctx.Err() + } + } + go func() { + for { + services, err := watcher.Next() + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + log.Error("http client watch service got unexpected error", "target", target, "error", err) + time.Sleep(time.Second) + continue + } + r.update(services) + } + }() + return r, nil +} + +func (r *resolver) update(services []*registry.ServiceInstance) bool { + filtered := make([]*registry.ServiceInstance, 0, len(services)) + for _, ins := range services { + ept, err := endpoint.ParseEndpoint(ins.Endpoints, endpoint.Scheme(schemeHTTP, !r.insecure)) + if err != nil { + log.Error("failed to parse discovery endpoint", "target", r.target, "endpoints", ins.Endpoints, "error", err) + continue + } + if ept == "" { + continue + } + filtered = append(filtered, ins) + } + if r.subsetSize != 0 { + filtered = subset.Subset(r.selectorKey, filtered, r.subsetSize) + } + nodes := make([]selector.Node, 0, len(filtered)) + for _, ins := range filtered { + ept, _ := endpoint.ParseEndpoint(ins.Endpoints, endpoint.Scheme(schemeHTTP, !r.insecure)) + nodes = append(nodes, selector.NewNode(schemeHTTP, ept, ins)) + } + + if len(nodes) == 0 { + log.Warn("[http resolver] zero endpoint found, refused to write", "endpoint", r.target.Endpoint, "nodes", nodes) + return false + } + r.rebalancer.Apply(nodes) + return true +} + +func (r *resolver) Close() error { + return r.watcher.Stop() +} diff --git a/transport/http/resolver_test.go b/transport/http/resolver_test.go new file mode 100644 index 0000000..53c4f87 --- /dev/null +++ b/transport/http/resolver_test.go @@ -0,0 +1,203 @@ +package http + +import ( + "context" + "errors" + "fmt" + "reflect" + "strconv" + "sync" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/registry" + "github.com/go-kratos/kratos/v3/selector" +) + +func TestParseTarget(t *testing.T) { + target, err := parseTarget("localhost:8000", true) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if !reflect.DeepEqual(&Target{Scheme: "http", Authority: "localhost:8000"}, target) { + t.Errorf("expect %v, got %v", &Target{Scheme: "http", Authority: "localhost:8000"}, target) + } + + target, err = parseTarget("discovery:///demo", true) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if !reflect.DeepEqual(&Target{Scheme: "discovery", Authority: "", Endpoint: "demo"}, target) { + t.Errorf("expect %v, got %v", &Target{Scheme: "discovery", Authority: "", Endpoint: "demo"}, target) + } + + target, err = parseTarget("127.0.0.1:8000", true) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if !reflect.DeepEqual(&Target{Scheme: "http", Authority: "127.0.0.1:8000"}, target) { + t.Errorf("expect %v, got %v", &Target{Scheme: "http", Authority: "127.0.0.1:8000"}, target) + } + + target, err = parseTarget("https://127.0.0.1:8000", false) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if !reflect.DeepEqual(&Target{Scheme: "https", Authority: "127.0.0.1:8000"}, target) { + t.Errorf("expect %v, got %v", &Target{Scheme: "https", Authority: "127.0.0.1:8000"}, target) + } + + target, err = parseTarget("127.0.0.1:8000", false) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if !reflect.DeepEqual(&Target{Scheme: "https", Authority: "127.0.0.1:8000"}, target) { + t.Errorf("expect %v, got %v", &Target{Scheme: "https", Authority: "127.0.0.1:8000"}, target) + } +} + +type mockRebalancer struct{} + +func (m *mockRebalancer) Apply(_ []selector.Node) {} + +type mockDiscoveries struct { + isSecure bool + nextErr bool + stopErr bool +} + +func (d *mockDiscoveries) GetService(_ context.Context, _ string) ([]*registry.ServiceInstance, error) { + return nil, nil +} + +const errServiceName = "needErr" + +func (d *mockDiscoveries) Watch(ctx context.Context, serviceName string) (registry.Watcher, error) { + if serviceName == errServiceName { + return nil, errors.New("mock test service name watch err") + } + return &mockWatch{ctx: ctx, isSecure: d.isSecure, nextErr: d.nextErr, stopErr: d.stopErr}, nil +} + +type mockWatch struct { + ctx context.Context + + isSecure bool + count int + + nextErr bool + stopErr bool + + lock sync.Mutex +} + +func (m *mockWatch) Next() ([]*registry.ServiceInstance, error) { + select { + case <-m.ctx.Done(): + return nil, m.ctx.Err() + default: + } + m.lock.Lock() + defer m.lock.Unlock() + if m.nextErr { + return nil, errors.New("mock test error") + } + if m.count == 1 { + return nil, errors.New("mock test error") + } + m.count++ + instance := ®istry.ServiceInstance{ + ID: "1", + Name: "kratos", + Version: "v1", + Metadata: map[string]string{}, + Endpoints: []string{fmt.Sprintf("http://127.0.0.1:9001?isSecure=%s", strconv.FormatBool(m.isSecure))}, + } + if m.count > 3 { + time.Sleep(time.Millisecond * 500) + } + return []*registry.ServiceInstance{instance}, nil +} + +func (m *mockWatch) Stop() error { + m.lock.Lock() + defer m.lock.Unlock() + if m.stopErr { + return errors.New("mock test error") + } + // Mark the next call to return an error. + m.nextErr = true + return nil +} + +func TestResolver(t *testing.T) { + ta, err := parseTarget("discovery://helloworld", true) + if err != nil { + t.Errorf("parse err %v", err) + return + } + + cancelCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Async mode should not return an error. + r, err := newResolver(cancelCtx, &mockDiscoveries{true, false, false}, ta, &mockRebalancer{}, false, false, 25) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if r != nil { + _ = r.Close() + } + + // Sync mode should run successfully. + r, err = newResolver(cancelCtx, &mockDiscoveries{false, false, false}, ta, &mockRebalancer{}, true, true, 25) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if r != nil { + _ = r.Close() + } + + // Sync mode should return an error when Next and Stop fail. + r, err = newResolver(cancelCtx, &mockDiscoveries{false, true, true}, ta, &mockRebalancer{}, true, true, 25) + if err == nil { + t.Errorf("expect err, got nil") + } + if r != nil { + _ = r.Close() + } + + // Sync mode should return an error when service name watch fails. + r, err = newResolver(cancelCtx, &mockDiscoveries{false, true, true}, &Target{ + Scheme: "discovery", + Endpoint: errServiceName, + }, &mockRebalancer{}, true, true, 25) + if err == nil { + t.Errorf("expect err, got nil") + } + if r != nil { + _ = r.Close() + } + + cancel() + + // This should log context.Canceled. + r, err = newResolver(cancelCtx, &mockDiscoveries{false, false, false}, ta, &mockRebalancer{}, false, false, 25) + if err != nil { + t.Errorf("expect %v, got %v", nil, err) + } + if r != nil { + _ = r.Close() + } + + // Sync mode should return an error when the service is canceled. + r, err = newResolver(cancelCtx, &mockDiscoveries{false, false, true}, ta, &mockRebalancer{}, true, true, 25) + if err == nil { + t.Errorf("expect ctx cancel err, got nil") + } + if r != nil { + _ = r.Close() + } + + time.Sleep(100 * time.Millisecond) +} diff --git a/transport/http/router.go b/transport/http/router.go new file mode 100644 index 0000000..2bb164c --- /dev/null +++ b/transport/http/router.go @@ -0,0 +1,104 @@ +package http + +import ( + "net/http" + "path" +) + +// WalkRouteFunc is the type of the function called for each route visited by Walk. +type WalkRouteFunc func(RouteInfo) error + +// RouteInfo is an HTTP route info. +type RouteInfo struct { + Path string + Method string +} + +// HandlerFunc defines a function to serve HTTP requests. +type HandlerFunc func(Context) error + +// Router is an HTTP router. +type Router struct { + prefix string + srv *Server + filters []FilterFunc +} + +func newRouter(prefix string, srv *Server, filters ...FilterFunc) *Router { + r := &Router{ + prefix: prefix, + srv: srv, + filters: filters, + } + return r +} + +// Group returns a new router group. +func (r *Router) Group(prefix string, filters ...FilterFunc) *Router { + newFilters := make([]FilterFunc, 0, len(r.filters)+len(filters)) + newFilters = append(newFilters, r.filters...) + newFilters = append(newFilters, filters...) + return newRouter(path.Join(r.prefix, prefix), r.srv, newFilters...) +} + +// Handle registers a new route with a matcher for the URL path and method. +func (r *Router) Handle(method, relativePath string, h HandlerFunc, filters ...FilterFunc) { + next := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + ctx := &wrapper{router: r} + ctx.Reset(res, req) + if err := h(ctx); err != nil { + r.srv.ene(res, req, err) + } + })) + next = FilterChain(filters...)(next) + next = FilterChain(r.filters...)(next) + route := r.srv.router.Handle(path.Join(r.prefix, relativePath), next) + if method != "*" { + route.Methods(method) + } +} + +// GET registers a new GET route for a path with matching handler in the router. +func (r *Router) GET(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodGet, path, h, m...) +} + +// HEAD registers a new HEAD route for a path with matching handler in the router. +func (r *Router) HEAD(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodHead, path, h, m...) +} + +// POST registers a new POST route for a path with matching handler in the router. +func (r *Router) POST(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodPost, path, h, m...) +} + +// PUT registers a new PUT route for a path with matching handler in the router. +func (r *Router) PUT(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodPut, path, h, m...) +} + +// PATCH registers a new PATCH route for a path with matching handler in the router. +func (r *Router) PATCH(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodPatch, path, h, m...) +} + +// DELETE registers a new DELETE route for a path with matching handler in the router. +func (r *Router) DELETE(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodDelete, path, h, m...) +} + +// CONNECT registers a new CONNECT route for a path with matching handler in the router. +func (r *Router) CONNECT(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodConnect, path, h, m...) +} + +// OPTIONS registers a new OPTIONS route for a path with matching handler in the router. +func (r *Router) OPTIONS(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodOptions, path, h, m...) +} + +// TRACE registers a new TRACE route for a path with matching handler in the router. +func (r *Router) TRACE(path string, h HandlerFunc, m ...FilterFunc) { + r.Handle(http.MethodTrace, path, h, m...) +} diff --git a/transport/http/router_test.go b/transport/http/router_test.go new file mode 100644 index 0000000..b007db7 --- /dev/null +++ b/transport/http/router_test.go @@ -0,0 +1,269 @@ +package http + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/http/httptest" + "reflect" + "runtime" + "strings" + "sync" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/internal/host" +) + +const appJSONStr = "application/json" + +type User struct { + Name string `json:"name"` +} + +func corsFilter(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + log.Println("cors:", r.Method, r.RequestURI) + w.Header().Set("Access-Control-Allow-Methods", r.Method) + return + } + next.ServeHTTP(w, r) + }) +} + +func authFilter(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println("auth:", r.Method, r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} + +func loggingFilter(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println("logging:", r.Method, r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} + +func TestRoute(t *testing.T) { + ctx := context.Background() + srv := NewServer( + Filter(corsFilter, loggingFilter), + ) + route := srv.Route("/v1") + route.GET("/users/{name}", func(ctx Context) error { + u := new(User) + u.Name = ctx.Vars().Get("name") + return ctx.Result(200, u) + }, authFilter) + route.POST("/users", func(ctx Context) error { + u := new(User) + if err := ctx.Bind(u); err != nil { + return err + } + return ctx.Result(201, u) + }) + route.PUT("/users", func(ctx Context) error { + u := new(User) + if err := ctx.Bind(u); err != nil { + return err + } + h := ctx.Middleware(func(context.Context, any) (any, error) { + return u, nil + }) + return ctx.Returns(h(ctx, u)) + }) + + if e, err := srv.Endpoint(); err != nil || e == nil { + t.Fatal(e, err) + } + go func() { + if err := srv.Start(ctx); err != nil { + panic(err) + } + }() + time.Sleep(time.Second) + testRoute(t, srv) + _ = srv.Stop(ctx) +} + +func testRoute(t *testing.T, srv *Server) { + port, ok := host.Port(srv.lis) + if !ok { + t.Fatalf("extract port error: %v", srv.lis) + } + base := fmt.Sprintf("http://127.0.0.1:%d/v1", port) + // GET + resp, err := http.Get(base + "/users/foo") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatalf("code: %d", resp.StatusCode) + } + if v := resp.Header.Get("Content-Type"); v != appJSONStr { + t.Fatalf("contentType: %s", v) + } + u := new(User) + if err = json.NewDecoder(resp.Body).Decode(u); err != nil { + t.Fatal(err) + } + if u.Name != "foo" { + t.Fatalf("got %s want foo", u.Name) + } + // POST + resp, err = http.Post(base+"/users", appJSONStr, strings.NewReader(`{"name":"bar"}`)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 201 { + t.Fatalf("code: %d", resp.StatusCode) + } + if v := resp.Header.Get("Content-Type"); v != appJSONStr { + t.Fatalf("contentType: %s", v) + } + u = new(User) + if err = json.NewDecoder(resp.Body).Decode(u); err != nil { + t.Fatal(err) + } + if u.Name != "bar" { + t.Fatalf("got %s want bar", u.Name) + } + // PUT + req, _ := http.NewRequest(http.MethodPut, base+"/users", strings.NewReader(`{"name":"bar"}`)) + req.Header.Set("Content-Type", appJSONStr) + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatalf("code: %d", resp.StatusCode) + } + if v := resp.Header.Get("Content-Type"); v != appJSONStr { + t.Fatalf("contentType: %s", v) + } + u = new(User) + if err = json.NewDecoder(resp.Body).Decode(u); err != nil { + t.Fatal(err) + } + if u.Name != "bar" { + t.Fatalf("got %s want bar", u.Name) + } + // OPTIONS + req, _ = http.NewRequest(http.MethodOptions, base+"/users", nil) + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatalf("code: %d", resp.StatusCode) + } + if resp.Header.Get("Access-Control-Allow-Methods") != http.MethodOptions { + t.Fatal("cors failed") + } +} + +func TestRouter_Group(t *testing.T) { + r := &Router{} + rr := r.Group("a", func(http.Handler) http.Handler { return nil }) + if !reflect.DeepEqual("a", rr.prefix) { + t.Errorf("expected %q, got %q", "a", rr.prefix) + } +} + +func TestHandle(_ *testing.T) { + r := newRouter("/", NewServer()) + h := func(Context) error { + return nil + } + r.GET("/get", h) + r.HEAD("/head", h) + r.PATCH("/patch", h) + r.DELETE("/delete", h) + r.CONNECT("/connect", h) + r.OPTIONS("/options", h) + r.TRACE("/trace", h) +} + +func TestRouterHandleWildcardMethod(t *testing.T) { + srv := NewServer() + srv.Route("/").Handle("*", "/wild", func(ctx Context) error { + return ctx.String(http.StatusNoContent, "") + }) + + for _, method := range []string{http.MethodGet, http.MethodPost} { + t.Run(method, func(t *testing.T) { + req := httptest.NewRequest(method, "/wild", nil) + w := httptest.NewRecorder() + srv.ServeHTTP(w, req) + if w.Code != http.StatusNoContent { + t.Fatalf("expected %d, got %d", http.StatusNoContent, w.Code) + } + }) + } +} + +func TestRouter_ContextDataRace(t *testing.T) { + runtime.GOMAXPROCS(runtime.NumCPU()) + + ctx := context.Background() + srvPort := 38888 + srvAddr := fmt.Sprintf(":%d", srvPort) + srv := NewServer(Timeout(time.Millisecond*50), Address(srvAddr)) + + router := srv.Route("/") + router.GET("/ping", func(ctx Context) error { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://www.baidu.com", nil) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return ctx.String(200, err.Error()) + } + _ = resp.Body.Close() + return ctx.String(200, "pong") + }) + + // start server + go func() { + if err := srv.Start(ctx); err != nil { + if errors.Is(err, http.ErrServerClosed) { + return + } + panic(err) + } + }() + + time.Sleep(time.Second) + + // start client + workers := 10 + wg := sync.WaitGroup{} + wg.Add(workers) + for i := 0; i < workers; i++ { + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/ping", srvPort), nil) + res, err := http.DefaultClient.Do(req) + if err != nil { + break + } + _ = res.Body.Close() + } + }() + } + wg.Wait() + _ = srv.Stop(ctx) + t.Log("test end") +} diff --git a/transport/http/server.go b/transport/http/server.go new file mode 100644 index 0000000..384bf05 --- /dev/null +++ b/transport/http/server.go @@ -0,0 +1,368 @@ +package http + +import ( + "context" + "crypto/tls" + "errors" + "net" + "net/http" + "net/url" + "time" + + "github.com/gorilla/mux" + + "github.com/go-kratos/kratos/v3/internal/endpoint" + "github.com/go-kratos/kratos/v3/internal/host" + "github.com/go-kratos/kratos/v3/internal/matcher" + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/transport" +) + +var ( + _ transport.Server = (*Server)(nil) + _ transport.Endpointer = (*Server)(nil) + _ http.Handler = (*Server)(nil) +) + +// ServerOption is an HTTP server option. +type ServerOption func(*Server) + +// Network with server network. +func Network(network string) ServerOption { + return func(s *Server) { + s.network = network + } +} + +// Address with server address. +func Address(addr string) ServerOption { + return func(s *Server) { + s.address = addr + } +} + +// Endpoint with server address. +func Endpoint(endpoint *url.URL) ServerOption { + return func(s *Server) { + s.endpoint = endpoint + } +} + +// Timeout with server timeout. +func Timeout(timeout time.Duration) ServerOption { + return func(s *Server) { + s.timeout = timeout + } +} + +// Middleware with service middleware option. +func Middleware(m ...middleware.Middleware) ServerOption { + return func(o *Server) { + o.middleware.Use(m...) + } +} + +// Filter with HTTP middleware option. +func Filter(filters ...FilterFunc) ServerOption { + return func(o *Server) { + o.filters = filters + } +} + +// RequestVarsDecoder with request decoder. +func RequestVarsDecoder(dec DecodeRequestFunc) ServerOption { + return func(o *Server) { + o.decVars = dec + } +} + +// RequestQueryDecoder with request decoder. +func RequestQueryDecoder(dec DecodeRequestFunc) ServerOption { + return func(o *Server) { + o.decQuery = dec + } +} + +// RequestDecoder with request decoder. +func RequestDecoder(dec DecodeRequestFunc) ServerOption { + return func(o *Server) { + o.decBody = dec + } +} + +// ResponseEncoder with response encoder. +func ResponseEncoder(en EncodeResponseFunc) ServerOption { + return func(o *Server) { + o.enc = en + } +} + +// ErrorEncoder with error encoder. +func ErrorEncoder(en EncodeErrorFunc) ServerOption { + return func(o *Server) { + o.ene = en + } +} + +// TLSConfig with TLS config. +func TLSConfig(c *tls.Config) ServerOption { + return func(o *Server) { + o.tlsConf = c + } +} + +// StrictSlash is with mux's StrictSlash +// If true, when the path pattern is "/path/", accessing "/path" will +// redirect to the former and vice versa. +func StrictSlash(strictSlash bool) ServerOption { + return func(o *Server) { + o.strictSlash = strictSlash + } +} + +// Listener with server lis +func Listener(lis net.Listener) ServerOption { + return func(s *Server) { + s.lis = lis + } +} + +// PathPrefix with mux's PathPrefix, router will be replaced by a subrouter that start with prefix. +func PathPrefix(prefix string) ServerOption { + return func(s *Server) { + s.router = s.router.PathPrefix(prefix).Subrouter() + } +} + +func NotFoundHandler(handler http.Handler) ServerOption { + return func(s *Server) { + s.router.NotFoundHandler = handler + } +} + +func MethodNotAllowedHandler(handler http.Handler) ServerOption { + return func(s *Server) { + s.router.MethodNotAllowedHandler = handler + } +} + +// Server is an HTTP server wrapper. +type Server struct { + *http.Server + lis net.Listener + tlsConf *tls.Config + endpoint *url.URL + err error + network string + address string + timeout time.Duration + filters []FilterFunc + middleware matcher.Matcher + decVars DecodeRequestFunc + decQuery DecodeRequestFunc + decBody DecodeRequestFunc + enc EncodeResponseFunc + ene EncodeErrorFunc + strictSlash bool + router *mux.Router +} + +// NewServer creates an HTTP server by options. +func NewServer(opts ...ServerOption) *Server { + srv := &Server{ + network: "tcp", + address: ":0", + timeout: 1 * time.Second, + middleware: matcher.New(), + decVars: DefaultRequestVars, + decQuery: DefaultRequestQuery, + decBody: DefaultRequestDecoder, + enc: DefaultResponseEncoder, + ene: DefaultErrorEncoder, + strictSlash: true, + router: mux.NewRouter(), + } + srv.router.NotFoundHandler = http.DefaultServeMux + srv.router.MethodNotAllowedHandler = http.DefaultServeMux + for _, o := range opts { + o(srv) + } + srv.router.StrictSlash(srv.strictSlash) + srv.router.Use(srv.filter()) + srv.Server = &http.Server{ + Handler: FilterChain(srv.filters...)(srv.router), + TLSConfig: srv.tlsConf, + } + return srv +} + +// Use uses a service middleware with selector. +// selector: +// - '/*' +// - '/helloworld.v1.Greeter/*' +// - '/helloworld.v1.Greeter/SayHello' +func (s *Server) Use(selector string, m ...middleware.Middleware) { + s.middleware.Add(selector, m...) +} + +// WalkRoute walks the router and all its sub-routers, calling walkFn for each route in the tree. +func (s *Server) WalkRoute(fn WalkRouteFunc) error { + return s.router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error { + methods, err := route.GetMethods() + if err != nil { + return nil // ignore no methods + } + path, err := route.GetPathTemplate() + if err != nil { + return err + } + for _, method := range methods { + if err := fn(RouteInfo{Method: method, Path: path}); err != nil { + return err + } + } + return nil + }) +} + +// WalkHandle walks the router and all its sub-routers, calling walkFn for each route in the tree. +func (s *Server) WalkHandle(handle func(method, path string, handler http.HandlerFunc)) error { + return s.WalkRoute(func(r RouteInfo) error { + handle(r.Method, r.Path, s.ServeHTTP) + return nil + }) +} + +// Route registers an HTTP router. +func (s *Server) Route(prefix string, filters ...FilterFunc) *Router { + return newRouter(prefix, s, filters...) +} + +// Handle registers a new route with a matcher for the URL path. +func (s *Server) Handle(path string, h http.Handler) { + s.router.Handle(path, h) +} + +// HandlePrefix registers a new route with a matcher for the URL path prefix. +func (s *Server) HandlePrefix(prefix string, h http.Handler) { + s.router.PathPrefix(prefix).Handler(h) +} + +// HandleFunc registers a new route with a matcher for the URL path. +func (s *Server) HandleFunc(path string, h http.HandlerFunc) { + s.router.HandleFunc(path, h) +} + +// HandleHeader registers a new route with a matcher for the header. +func (s *Server) HandleHeader(key, val string, h http.HandlerFunc) { + s.router.Headers(key, val).Handler(h) +} + +// ServeHTTP should write reply headers and data to the ResponseWriter and then return. +func (s *Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { + s.Handler.ServeHTTP(res, req) +} + +func (s *Server) filter() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var ( + ctx context.Context + cancel context.CancelFunc + ) + if s.timeout > 0 { + ctx, cancel = context.WithTimeout(req.Context(), s.timeout) + } else { + ctx, cancel = context.WithCancel(req.Context()) + } + defer cancel() + + pathTemplate := req.URL.Path + if route := mux.CurrentRoute(req); route != nil { + // /path/123 -> /path/{id} + pathTemplate, _ = route.GetPathTemplate() + } + + tr := &Transport{ + operation: pathTemplate, + pathTemplate: pathTemplate, + reqHeader: headerCarrier(req.Header), + replyHeader: headerCarrier(w.Header()), + request: req, + response: w, + } + if s.endpoint != nil { + tr.endpoint = s.endpoint.String() + } + tr.request = req.WithContext(transport.NewServerContext(ctx, tr)) + next.ServeHTTP(w, tr.request) + }) + } +} + +// Endpoint return a real address to registry endpoint. +// examples: +// +// https://127.0.0.1:8000 +// Legacy: http://127.0.0.1:8000?isSecure=false +func (s *Server) Endpoint() (*url.URL, error) { + if err := s.listenAndEndpoint(); err != nil { + return nil, err + } + return s.endpoint, nil +} + +// Start start the HTTP server. +func (s *Server) Start(ctx context.Context) error { + if err := s.listenAndEndpoint(); err != nil { + return err + } + s.BaseContext = func(net.Listener) context.Context { + return ctx + } + log.Info("[HTTP] server listening", "addr", s.lis.Addr().String()) + var err error + if s.tlsConf != nil { + err = s.ServeTLS(s.lis, "", "") + } else { + err = s.Serve(s.lis) + } + if !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} + +// Stop stop the HTTP server. +func (s *Server) Stop(ctx context.Context) error { + log.Info("[HTTP] server stopping") + err := s.Shutdown(ctx) + if err != nil { + if ctx.Err() != nil { + log.Warn("[HTTP] server couldn't stop gracefully in time, doing force stop") + err = s.Close() + } + } + return err +} + +func (s *Server) listenAndEndpoint() error { + if s.lis == nil { + lis, err := net.Listen(s.network, s.address) + if err != nil { + s.err = err + return err + } + s.lis = lis + } + if s.endpoint == nil { + addr, err := host.Extract(s.address, s.lis) + if err != nil { + s.err = err + return err + } + s.endpoint = endpoint.NewEndpoint(endpoint.Scheme("http", s.tlsConf != nil), addr) + } + return s.err +} diff --git a/transport/http/server_test.go b/transport/http/server_test.go new file mode 100644 index 0000000..c6c3e79 --- /dev/null +++ b/transport/http/server_test.go @@ -0,0 +1,488 @@ +package http + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "sync" + "testing" + "time" + + kratoserrors "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/internal/host" + "github.com/go-kratos/kratos/v3/log" +) + +var h = func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(testData{Path: r.RequestURI}) +} + +type testKey struct{} + +type testData struct { + Path string `json:"path"` +} + +// handleFuncWrapper is a wrapper for http.HandlerFunc to implement http.Handler +type handleFuncWrapper struct { + fn http.HandlerFunc +} + +func (x *handleFuncWrapper) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + x.fn.ServeHTTP(writer, request) +} + +func newHandleFuncWrapper(fn http.HandlerFunc) http.Handler { + return &handleFuncWrapper{fn: fn} +} + +func TestServeHTTP(t *testing.T) { + ln, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal(err) + } + mux := NewServer(Listener(ln)) + mux.HandleFunc("/index", h) + mux.Route("/errors").GET("/cause", func(Context) error { + return kratoserrors.BadRequest("xxx", "zzz"). + WithMetadata(map[string]string{"foo": "bar"}). + WithCause(errors.New("error cause")) + }) + if err = mux.WalkRoute(func(r RouteInfo) error { + t.Logf("WalkRoute: %+v", r) + return nil + }); err != nil { + t.Fatal(err) + } + if e, err := mux.Endpoint(); err != nil || e == nil || strings.HasSuffix(e.Host, ":0") { + t.Fatal(e, err) + } + srv := http.Server{Handler: mux} + go func() { + if err := srv.Serve(ln); err != nil { + if kratoserrors.Is(err, http.ErrServerClosed) { + return + } + panic(err) + } + }() + time.Sleep(time.Second) + if err := srv.Shutdown(context.Background()); err != nil { + t.Log(err) + } +} + +func TestServer(t *testing.T) { + ctx := context.Background() + srv := NewServer() + srv.Handle("/index", newHandleFuncWrapper(h)) + srv.HandleFunc("/index/{id:[0-9]+}", h) + srv.HandlePrefix("/test/prefix", newHandleFuncWrapper(h)) + srv.HandleHeader("content-type", "application/grpc-web+json", func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(testData{Path: r.RequestURI}) + }) + srv.Route("/errors").GET("/cause", func(Context) error { + return kratoserrors.BadRequest("xxx", "zzz"). + WithMetadata(map[string]string{"foo": "bar"}). + WithCause(errors.New("error cause")) + }) + + if e, err := srv.Endpoint(); err != nil || e == nil || strings.HasSuffix(e.Host, ":0") { + t.Fatal(e, err) + } + + go func() { + if err := srv.Start(ctx); err != nil { + panic(err) + } + }() + time.Sleep(time.Second) + testHeader(t, srv) + testClient(t, srv) + testAccept(t, srv) + time.Sleep(time.Second) + if srv.Stop(ctx) != nil { + t.Errorf("expected nil got %v", srv.Stop(ctx)) + } +} + +func testAccept(t *testing.T, srv *Server) { + tests := []struct { + method string + path string + contentType string + }{ + {http.MethodGet, "/errors/cause", "application/json"}, + {http.MethodGet, "/errors/cause", "application/proto"}, + } + e, err := srv.Endpoint() + if err != nil { + t.Errorf("expected nil got %v", err) + } + client, err := NewClient(context.Background(), WithEndpoint(e.Host)) + if err != nil { + t.Errorf("expected nil got %v", err) + } + for _, test := range tests { + req, err := http.NewRequest(test.method, e.String()+test.path, nil) + if err != nil { + t.Errorf("expected nil got %v", err) + } + req.Header.Set("Content-Type", test.contentType) + resp, err := client.Do(req) + if kratoserrors.Code(err) != 400 { + t.Errorf("expected 400 got %v", err) + } + if err == nil { + resp.Body.Close() + } + } +} + +func testHeader(t *testing.T, srv *Server) { + e, err := srv.Endpoint() + if err != nil { + t.Errorf("expected nil got %v", err) + } + client, err := NewClient(context.Background(), WithEndpoint(e.Host)) + if err != nil { + t.Errorf("expected nil got %v", err) + } + req, err := http.NewRequest(http.MethodGet, e.String()+"/index", nil) + if err != nil { + t.Errorf("expected nil got %v", err) + } + req.Header.Set("content-type", "application/grpc-web+json") + resp, err := client.Do(req) + if err != nil { + t.Errorf("expected nil got %v", err) + } + resp.Body.Close() +} + +func testClient(t *testing.T, srv *Server) { + tests := []struct { + method string + path string + code int + }{ + {http.MethodGet, "/index", http.StatusOK}, + {http.MethodPut, "/index", http.StatusOK}, + {http.MethodPost, "/index", http.StatusOK}, + {http.MethodPatch, "/index", http.StatusOK}, + {http.MethodDelete, "/index", http.StatusOK}, + + {http.MethodGet, "/index/1", http.StatusOK}, + {http.MethodPut, "/index/1", http.StatusOK}, + {http.MethodPost, "/index/1", http.StatusOK}, + {http.MethodPatch, "/index/1", http.StatusOK}, + {http.MethodDelete, "/index/1", http.StatusOK}, + + {http.MethodGet, "/index/notfound", http.StatusNotFound}, + {http.MethodGet, "/errors/cause", http.StatusBadRequest}, + {http.MethodGet, "/test/prefix/123111", http.StatusOK}, + } + e, err := srv.Endpoint() + if err != nil { + t.Fatal(err) + } + client, err := NewClient(context.Background(), WithEndpoint(e.Host)) + if err != nil { + t.Fatal(err) + } + defer client.Close() + for _, test := range tests { + var res testData + reqURL := e.String() + test.path + req, err := http.NewRequest(test.method, reqURL, nil) + if err != nil { + t.Fatal(err) + } + resp, err := client.Do(req) + if kratoserrors.Code(err) != test.code { + t.Fatalf("want %v, but got %v", test, err) + } + if err != nil { + continue + } + if resp.StatusCode != 200 { + _ = resp.Body.Close() + t.Fatalf("http status got %d", resp.StatusCode) + } + content, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + t.Fatalf("read resp error %v", err) + } + err = json.Unmarshal(content, &res) + if err != nil { + t.Fatalf("unmarshal resp error %v", err) + } + if res.Path != test.path { + t.Errorf("expected %s got %s", test.path, res.Path) + } + } + for _, test := range tests { + var res testData + err := client.Invoke(context.Background(), test.method, test.path, nil, &res) + if kratoserrors.Code(err) != test.code { + t.Fatalf("want %v, but got %v", test, err) + } + if err != nil { + continue + } + if res.Path != test.path { + t.Errorf("expected %s got %s", test.path, res.Path) + } + } +} + +func BenchmarkServer(b *testing.B) { + fn := func(w http.ResponseWriter, r *http.Request) { + data := &testData{Path: r.RequestURI} + _ = json.NewEncoder(w).Encode(data) + if r.Context().Value(testKey{}) != "test" { + w.WriteHeader(500) + } + } + ctx := context.Background() + ctx = context.WithValue(ctx, testKey{}, "test") + srv := NewServer() + srv.HandleFunc("/index", fn) + go func() { + if err := srv.Start(ctx); err != nil { + panic(err) + } + }() + time.Sleep(time.Second) + port, ok := host.Port(srv.lis) + if !ok { + b.Errorf("expected port got %v", srv.lis) + } + client, err := NewClient(context.Background(), WithEndpoint(fmt.Sprintf("127.0.0.1:%d", port))) + if err != nil { + b.Errorf("expected nil got %v", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var res testData + err := client.Invoke(context.Background(), http.MethodPost, "/index", nil, &res) + if err != nil { + b.Errorf("expected nil got %v", err) + } + } + _ = srv.Stop(ctx) +} + +func TestNetwork(t *testing.T) { + o := &Server{} + v := "abc" + Network(v)(o) + if !reflect.DeepEqual(v, o.network) { + t.Errorf("expected %v got %v", v, o.network) + } +} + +func TestAddress(t *testing.T) { + o := &Server{} + v := "abc" + Address(v)(o) + if !reflect.DeepEqual(v, o.address) { + t.Errorf("expected %v got %v", v, o.address) + } +} + +func TestTimeout(t *testing.T) { + o := &Server{} + v := time.Duration(123) + Timeout(v)(o) + if !reflect.DeepEqual(v, o.timeout) { + t.Errorf("expected %v got %v", v, o.timeout) + } +} + +func TestRequestDecoder(t *testing.T) { + o := &Server{} + v := func(*http.Request, any) error { return nil } + RequestDecoder(v)(o) + if o.decBody == nil { + t.Errorf("expected nil got %v", o.decBody) + } +} + +func TestResponseEncoder(t *testing.T) { + o := &Server{} + v := func(http.ResponseWriter, *http.Request, any) error { return nil } + ResponseEncoder(v)(o) + if o.enc == nil { + t.Errorf("expected nil got %v", o.enc) + } +} + +func TestErrorEncoder(t *testing.T) { + o := &Server{} + v := func(http.ResponseWriter, *http.Request, error) {} + ErrorEncoder(v)(o) + if o.ene == nil { + t.Errorf("expected nil got %v", o.ene) + } +} + +func TestTLSConfig(t *testing.T) { + o := &Server{} + v := &tls.Config{} + TLSConfig(v)(o) + if !reflect.DeepEqual(v, o.tlsConf) { + t.Errorf("expected %v got %v", v, o.tlsConf) + } +} + +func TestStrictSlash(t *testing.T) { + o := &Server{} + v := true + StrictSlash(v)(o) + if !reflect.DeepEqual(v, o.strictSlash) { + t.Errorf("expected %v got %v", v, o.tlsConf) + } +} + +func TestListener(t *testing.T) { + lis, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal(err) + } + s := &Server{} + Listener(lis)(s) + if !reflect.DeepEqual(s.lis, lis) { + t.Errorf("expected %v got %v", lis, s.lis) + } + if e, err := s.Endpoint(); err != nil || e == nil { + t.Errorf("expected not empty") + } +} + +func TestNotFoundHandler(t *testing.T) { + mux := http.NewServeMux() + srv := NewServer(NotFoundHandler(mux)) + if !reflect.DeepEqual(srv.router.NotFoundHandler, mux) { + t.Errorf("expected %v got %v", mux, srv.router.NotFoundHandler) + } +} + +func TestMethodNotAllowedHandler(t *testing.T) { + mux := http.NewServeMux() + srv := NewServer(MethodNotAllowedHandler(mux)) + if !reflect.DeepEqual(srv.router.MethodNotAllowedHandler, mux) { + t.Errorf("expected %v got %v", mux, srv.router.MethodNotAllowedHandler) + } +} + +func TestStop(t *testing.T) { + timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + tests := []struct { + name string + sleep time.Duration + ctx context.Context + cancel context.CancelFunc + wantForceStop bool + }{ + { + name: "normal", + sleep: 0, + ctx: context.Background(), + cancel: func() {}, + wantForceStop: false, + }, + { + name: "timeout", + sleep: 2 * time.Second, + ctx: timeoutCtx, + cancel: cancel, + wantForceStop: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + old := log.Default() + defer log.SetDefault(old) + + // Create a logger to capture logs + var logs safeBytesBuffer + log.SetDefault(slog.New(slog.NewTextHandler(&logs, nil))) + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t := time.NewTimer(tt.sleep) + defer t.Stop() + select { + case <-t.C: + case <-r.Context().Done(): + } + w.WriteHeader(http.StatusOK) + })) + defer testServer.Close() + + go func() { + resp, err := http.Get(testServer.URL) + if err != nil { + return + } + _ = resp.Body.Close() + }() + + time.Sleep(100 * time.Millisecond) + + s := &Server{ + Server: testServer.Config, + } + + tt.cancel() + err := s.Stop(tt.ctx) + if err != nil { + t.Errorf("Expected no error, got %v", err) + return + } + + // Check if the stop was forced or graceful + if tt.wantForceStop { + if !strings.Contains(logs.String(), "force stop") { + t.Errorf("Expected force stop\n%s", logs.String()) + } + } else { + if strings.Contains(logs.String(), "force stop") { + t.Errorf("Expected graceful stop\n%s", logs.String()) + } + } + }) + } +} + +type safeBytesBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *safeBytesBuffer) Write(p []byte) (n int, err error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *safeBytesBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} diff --git a/transport/http/status/status.go b/transport/http/status/status.go new file mode 100644 index 0000000..16c9bcf --- /dev/null +++ b/transport/http/status/status.go @@ -0,0 +1,114 @@ +package status + +import ( + "net/http" + + "google.golang.org/grpc/codes" +) + +const ( + // ClientClosed is non-standard http status code, + // which defined by nginx. + // https://httpstatus.in/499/ + ClientClosed = 499 +) + +// Converter is a status converter. +type Converter interface { + // ToGRPCCode converts an HTTP error code into the corresponding gRPC response status. + ToGRPCCode(code int) codes.Code + + // FromGRPCCode converts a gRPC error code into the corresponding HTTP response status. + FromGRPCCode(code codes.Code) int +} + +type statusConverter struct{} + +// DefaultConverter default converter. +var DefaultConverter Converter = statusConverter{} + +// ToGRPCCode converts an HTTP error code into the corresponding gRPC response status. +// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto +func (c statusConverter) ToGRPCCode(code int) codes.Code { + switch code { + case http.StatusOK: + return codes.OK + case http.StatusBadRequest: + return codes.InvalidArgument + case http.StatusUnauthorized: + return codes.Unauthenticated + case http.StatusForbidden: + return codes.PermissionDenied + case http.StatusNotFound: + return codes.NotFound + case http.StatusConflict: + return codes.Aborted + case http.StatusTooManyRequests: + return codes.ResourceExhausted + case http.StatusInternalServerError: + return codes.Internal + case http.StatusNotImplemented: + return codes.Unimplemented + case http.StatusServiceUnavailable: + return codes.Unavailable + case http.StatusGatewayTimeout: + return codes.DeadlineExceeded + case ClientClosed: + return codes.Canceled + case http.StatusPreconditionFailed: + return codes.FailedPrecondition + } + return codes.Unknown +} + +// FromGRPCCode converts a gRPC error code into the corresponding HTTP response status. +// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto +func (c statusConverter) FromGRPCCode(code codes.Code) int { + switch code { + case codes.OK: + return http.StatusOK + case codes.Canceled: + return ClientClosed + case codes.Unknown: + return http.StatusInternalServerError + case codes.InvalidArgument: + return http.StatusBadRequest + case codes.DeadlineExceeded: + return http.StatusGatewayTimeout + case codes.NotFound: + return http.StatusNotFound + case codes.AlreadyExists: + return http.StatusConflict + case codes.PermissionDenied: + return http.StatusForbidden + case codes.Unauthenticated: + return http.StatusUnauthorized + case codes.ResourceExhausted: + return http.StatusTooManyRequests + case codes.Aborted: + return http.StatusConflict + case codes.OutOfRange: + return http.StatusBadRequest + case codes.Unimplemented: + return http.StatusNotImplemented + case codes.Internal: + return http.StatusInternalServerError + case codes.Unavailable: + return http.StatusServiceUnavailable + case codes.DataLoss: + return http.StatusInternalServerError + case codes.FailedPrecondition: + return http.StatusPreconditionFailed + } + return http.StatusInternalServerError +} + +// ToGRPCCode converts an HTTP error code into the corresponding gRPC response status. +func ToGRPCCode(code int) codes.Code { + return DefaultConverter.ToGRPCCode(code) +} + +// FromGRPCCode converts a gRPC error code into the corresponding HTTP response status. +func FromGRPCCode(code codes.Code) int { + return DefaultConverter.FromGRPCCode(code) +} diff --git a/transport/http/status/status_test.go b/transport/http/status/status_test.go new file mode 100644 index 0000000..7dde41b --- /dev/null +++ b/transport/http/status/status_test.go @@ -0,0 +1,72 @@ +package status + +import ( + "net/http" + "testing" + + "google.golang.org/grpc/codes" +) + +func TestToGRPCCode(t *testing.T) { + tests := []struct { + name string + code int + want codes.Code + }{ + {"http.StatusOK", http.StatusOK, codes.OK}, + {"http.StatusBadRequest", http.StatusBadRequest, codes.InvalidArgument}, + {"http.StatusUnauthorized", http.StatusUnauthorized, codes.Unauthenticated}, + {"http.StatusForbidden", http.StatusForbidden, codes.PermissionDenied}, + {"http.StatusNotFound", http.StatusNotFound, codes.NotFound}, + {"http.StatusConflict", http.StatusConflict, codes.Aborted}, + {"http.StatusTooManyRequests", http.StatusTooManyRequests, codes.ResourceExhausted}, + {"http.StatusInternalServerError", http.StatusInternalServerError, codes.Internal}, + {"http.StatusNotImplemented", http.StatusNotImplemented, codes.Unimplemented}, + {"http.StatusServiceUnavailable", http.StatusServiceUnavailable, codes.Unavailable}, + {"http.StatusGatewayTimeout", http.StatusGatewayTimeout, codes.DeadlineExceeded}, + {"StatusClientClosed", ClientClosed, codes.Canceled}, + {"http.StatusPreconditionFailed", http.StatusPreconditionFailed, codes.FailedPrecondition}, + {"else", 100000, codes.Unknown}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ToGRPCCode(tt.code); got != tt.want { + t.Errorf("GRPCCodeFromStatus() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFromGRPCCode(t *testing.T) { + tests := []struct { + name string + code codes.Code + want int + }{ + {"codes.OK", codes.OK, http.StatusOK}, + {"codes.Canceled", codes.Canceled, ClientClosed}, + {"codes.Unknown", codes.Unknown, http.StatusInternalServerError}, + {"codes.InvalidArgument", codes.InvalidArgument, http.StatusBadRequest}, + {"codes.DeadlineExceeded", codes.DeadlineExceeded, http.StatusGatewayTimeout}, + {"codes.NotFound", codes.NotFound, http.StatusNotFound}, + {"codes.AlreadyExists", codes.AlreadyExists, http.StatusConflict}, + {"codes.PermissionDenied", codes.PermissionDenied, http.StatusForbidden}, + {"codes.Unauthenticated", codes.Unauthenticated, http.StatusUnauthorized}, + {"codes.ResourceExhausted", codes.ResourceExhausted, http.StatusTooManyRequests}, + {"codes.FailedPrecondition", codes.FailedPrecondition, http.StatusPreconditionFailed}, + {"codes.Aborted", codes.Aborted, http.StatusConflict}, + {"codes.OutOfRange", codes.OutOfRange, http.StatusBadRequest}, + {"codes.Unimplemented", codes.Unimplemented, http.StatusNotImplemented}, + {"codes.Internal", codes.Internal, http.StatusInternalServerError}, + {"codes.Unavailable", codes.Unavailable, http.StatusServiceUnavailable}, + {"codes.DataLoss", codes.DataLoss, http.StatusInternalServerError}, + {"else", codes.Code(10000), http.StatusInternalServerError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FromGRPCCode(tt.code); got != tt.want { + t.Errorf("StatusFromGRPCCode() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/transport/http/stream.go b/transport/http/stream.go new file mode 100644 index 0000000..2b682b0 --- /dev/null +++ b/transport/http/stream.go @@ -0,0 +1,895 @@ +package http + +import ( + "bufio" + "bytes" + "context" + stderrors "errors" + "fmt" + "io" + stdhttp "net/http" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/go-kratos/kratos/v3/encoding" + kerrors "github.com/go-kratos/kratos/v3/errors" + "github.com/go-kratos/kratos/v3/internal/httputil" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/selector" + "github.com/go-kratos/kratos/v3/transport" +) + +const ( + sseContentType = "text/event-stream" + + websocketControlPrefix = "\x1e" + websocketControlEnd = websocketControlPrefix + "end" + websocketControlError = websocketControlPrefix + "error:" +) + +type streamMode int + +const ( + streamModeSSE streamMode = iota + 1 + streamModeWebSocket +) + +// ServerStream adapts HTTP streaming transports to grpc generated stream interfaces. +type ServerStream interface { + grpc.ServerStream + Send(any) error + Recv(any) error + SendAndClose(any) error + Close(error) error + SetContext(context.Context) + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error +} + +// ClientStream adapts HTTP streaming clients to grpc generated stream interfaces. +type ClientStream interface { + grpc.ClientStream + Send(any) error + Recv(any) error + CloseAndRecv(any) error +} + +type serverStream struct { + ctx context.Context + req *stdhttp.Request + res stdhttp.ResponseWriter + mode streamMode + conn *websocket.Conn + header metadata.MD + trailer metadata.MD + encoder encoding.Codec + decoder encoding.Codec + started bool + writeMu sync.Mutex + upgrader websocket.Upgrader + bodyField string +} + +// ServerStreamOption customizes a server stream created by the HTTP transport. +type ServerStreamOption func(*serverStream) + +// WithStreamBodyField declares the request message field that carries each streamed +// frame's payload. It is used for client-streaming RPCs whose HTTP rule maps a named +// body field (e.g. body: "data"): every received frame is decoded into that field while +// the remaining fields are bound from the request query and path vars. +func WithStreamBodyField(name string) ServerStreamOption { + return func(s *serverStream) { + s.bodyField = name + } +} + +// NewServerSentEventServerStream returns a stream that writes server messages as SSE events. +func NewServerSentEventServerStream(ctx Context) ServerStream { + s := &serverStream{ + ctx: detachStreamContext(ctx), + req: ctx.Request(), + res: ctx.Response(), + mode: streamModeSSE, + } + s.encoder = streamCodecFromHeaders(s.req.Header, "Accept", "Content-Type") + s.decoder = streamCodecFromHeaders(s.req.Header, "Content-Type", "Accept") + return s +} + +// NewWebSocketServerStream upgrades the current request and returns a WebSocket stream. +func NewWebSocketServerStream(ctx Context, opts ...ServerStreamOption) (ServerStream, error) { + s := &serverStream{ + ctx: detachStreamContext(ctx), + req: ctx.Request(), + res: ctx.Response(), + mode: streamModeWebSocket, + } + for _, opt := range opts { + opt(s) + } + s.encoder = streamCodecFromHeaders(s.req.Header, "Accept", "Content-Type") + s.decoder = streamCodecFromHeaders(s.req.Header, "Content-Type", "Accept") + conn, err := s.upgrader.Upgrade(ctx.Response(), ctx.Request(), nil) + if err != nil { + return nil, err + } + s.conn = conn + return s, nil +} + +// SetContext stores the streaming handler context. The server timeout and +// cancellation are detached so a long-lived stream is not torn down by the +// per-request server timeout; only the context values (tracing, auth, metadata +// injected by middleware) are preserved. The stream lifecycle is instead driven +// by Send/Recv errors and the read/write deadlines set via SetReadDeadline and +// SetWriteDeadline. This mirrors how the gRPC transport leaves streams on the +// connection-scoped context rather than the per-request timeout context. +func (s *serverStream) SetContext(ctx context.Context) { + s.ctx = detachStreamContext(ctx) +} + +// detachStreamContext returns a context that keeps the values of ctx but drops +// its deadline and cancellation, so the per-request server timeout does not +// abort a long-lived stream. +func detachStreamContext(ctx context.Context) context.Context { + if ctx == nil { + return context.Background() + } + return context.WithoutCancel(ctx) +} + +// SetReadDeadline sets the deadline for future Recv calls. A zero value for t +// disables the deadline. For WebSocket streams it is applied to the underlying +// connection; for SSE streams it is applied via http.ResponseController. +func (s *serverStream) SetReadDeadline(t time.Time) error { + switch s.mode { + case streamModeWebSocket: + if s.conn == nil { + return stderrors.New("http: websocket connection not established") + } + return s.conn.SetReadDeadline(t) + case streamModeSSE: + return stdhttp.NewResponseController(s.res).SetReadDeadline(t) + default: + return stderrors.New("unknown HTTP stream mode") + } +} + +// SetWriteDeadline sets the deadline for future Send calls. A zero value for t +// disables the deadline. For WebSocket streams it is serialized against in-flight +// writes via the stream's write mutex; for SSE streams it is applied via +// http.ResponseController. +func (s *serverStream) SetWriteDeadline(t time.Time) error { + switch s.mode { + case streamModeWebSocket: + if s.conn == nil { + return stderrors.New("http: websocket connection not established") + } + s.writeMu.Lock() + defer s.writeMu.Unlock() + return s.conn.SetWriteDeadline(t) + case streamModeSSE: + return stdhttp.NewResponseController(s.res).SetWriteDeadline(t) + default: + return stderrors.New("unknown HTTP stream mode") + } +} + +func (s *serverStream) SetHeader(md metadata.MD) error { + s.header = metadata.Join(s.header, md) + if s.mode == streamModeSSE && !s.started { + copyMetadataToHeader(s.res.Header(), md) + } + return nil +} + +func (s *serverStream) SendHeader(md metadata.MD) error { + if err := s.SetHeader(md); err != nil { + return err + } + if s.mode == streamModeSSE { + s.startSSE() + } + return nil +} + +func (s *serverStream) SetTrailer(md metadata.MD) { + s.trailer = metadata.Join(s.trailer, md) +} + +func (s *serverStream) Context() context.Context { + if s.ctx == nil { + return context.Background() + } + return s.ctx +} + +func (s *serverStream) Send(m any) error { + return s.SendMsg(m) +} + +func (s *serverStream) Recv(m any) error { + if err := s.recvMessage(m); err != nil { + return err + } + if s.req != nil { + if err := DefaultRequestQuery(s.req, m); err != nil { + return err + } + if err := DefaultRequestVars(s.req, m); err != nil { + return err + } + } + return nil +} + +// recvMessage decodes the next frame. When a named body field is declared the frame +// carries only that field's payload, so it is decoded into a freshly allocated sub-message +// and assigned back onto m; otherwise the frame is decoded into m directly. The generator +// only declares a body field for a singular message-kind field, so a mismatch here is a +// programming error and is reported rather than silently ignored. +func (s *serverStream) recvMessage(m any) error { + if s.bodyField == "" { + return s.RecvMsg(m) + } + pm, ok := m.(proto.Message) + if !ok { + return fmt.Errorf("http: stream body field %q requires a proto.Message, got %T", s.bodyField, m) + } + fd := pm.ProtoReflect().Descriptor().Fields().ByName(protoreflect.Name(s.bodyField)) + if fd == nil || fd.Kind() != protoreflect.MessageKind || fd.IsList() || fd.IsMap() { + return fmt.Errorf("http: stream body field %q is not a singular message field", s.bodyField) + } + sub := pm.ProtoReflect().NewField(fd) + if err := s.RecvMsg(sub.Message().Interface()); err != nil { + return err + } + pm.ProtoReflect().Set(fd, sub) + return nil +} + +func (s *serverStream) SendAndClose(m any) error { + return s.SendMsg(m) +} + +func (s *serverStream) SendMsg(m any) error { + switch s.mode { + case streamModeSSE: + return s.sendSSE("message", m) + case streamModeWebSocket: + return s.writeWebSocketMessage(m) + default: + return stderrors.New("unknown HTTP stream mode") + } +} + +func (s *serverStream) RecvMsg(m any) error { + if s.mode != streamModeWebSocket { + return io.EOF + } + return readWebSocketMessage(s.conn, m, s.decoder) +} + +func (s *serverStream) Close(err error) error { + switch s.mode { + case streamModeSSE: + if err == nil { + return nil + } + if !s.started { + return err + } + _ = s.sendSSE("error", kerrors.FromError(err)) + return nil + case streamModeWebSocket: + if s.conn == nil { + return err + } + if err != nil { + _ = s.writeWebSocketControl(websocketControlError + err.Error()) + _ = s.writeWebSocketClose(websocket.CloseInternalServerErr, err.Error()) + _ = s.conn.Close() + return nil + } + _ = s.writeWebSocketClose(websocket.CloseNormalClosure, "") + return s.conn.Close() + default: + return err + } +} + +func (s *serverStream) startSSE() { + if s.started { + return + } + h := s.res.Header() + h.Set("Content-Type", sseContentType) + h.Set("Cache-Control", "no-cache") + h.Set("Connection", "keep-alive") + h.Set("X-Accel-Buffering", "no") + copyMetadataToHeader(h, s.header) + s.res.WriteHeader(stdhttp.StatusOK) + s.started = true +} + +func (s *serverStream) sendSSE(event string, v any) error { + data, err := marshalStreamMessage(v, s.encoder) + if err != nil { + return err + } + s.writeMu.Lock() + defer s.writeMu.Unlock() + s.startSSE() + if _, err = fmt.Fprintf(s.res, "event: %s\n", event); err != nil { + return err + } + for _, line := range bytes.Split(data, []byte("\n")) { + if _, err = fmt.Fprintf(s.res, "data: %s\n", line); err != nil { + return err + } + } + if _, err = io.WriteString(s.res, "\n"); err != nil { + return err + } + if flusher, ok := s.res.(stdhttp.Flusher); ok { + flusher.Flush() + } + return nil +} + +func (s *serverStream) writeWebSocketMessage(m any) error { + data, err := marshalStreamMessage(m, s.encoder) + if err != nil { + return err + } + s.writeMu.Lock() + defer s.writeMu.Unlock() + return s.conn.WriteMessage(websocket.TextMessage, data) +} + +func (s *serverStream) writeWebSocketControl(message string) error { + s.writeMu.Lock() + defer s.writeMu.Unlock() + return s.conn.WriteMessage(websocket.TextMessage, []byte(message)) +} + +func (s *serverStream) writeWebSocketClose(code int, text string) error { + s.writeMu.Lock() + defer s.writeMu.Unlock() + msg := websocket.FormatCloseMessage(code, text) + return s.conn.WriteControl(websocket.CloseMessage, msg, time.Now().Add(time.Second)) +} + +type sseClientStream struct { + ctx context.Context + res *stdhttp.Response + scanner *bufio.Scanner + decoder encoding.Codec + closeOnce sync.Once + closeErr error +} + +func newSSEClientStream(ctx context.Context, res *stdhttp.Response, decoder encoding.Codec) ClientStream { + scanner := bufio.NewScanner(res.Body) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + return &sseClientStream{ctx: ctx, res: res, scanner: scanner, decoder: decoder} +} + +func (s *sseClientStream) Header() (metadata.MD, error) { + return metadataFromHeader(s.res.Header), nil +} + +func (s *sseClientStream) Trailer() metadata.MD { + return metadataFromHeader(s.res.Trailer) +} + +func (s *sseClientStream) CloseSend() error { + return s.closeBody() +} + +func (s *sseClientStream) Context() context.Context { + if s.ctx == nil { + return context.Background() + } + return s.ctx +} + +func (s *sseClientStream) Send(any) error { + return stderrors.New("SSE client stream does not support Send") +} + +func (s *sseClientStream) Recv(m any) error { + return s.RecvMsg(m) +} + +func (s *sseClientStream) CloseAndRecv(any) error { + return stderrors.New("SSE client stream does not support CloseAndRecv") +} + +func (s *sseClientStream) SendMsg(any) error { + return stderrors.New("SSE client stream does not support SendMsg") +} + +func (s *sseClientStream) RecvMsg(m any) error { + for { + event, data, err := s.readEvent() + if err != nil { + _ = s.closeBody() + return err + } + switch event { + case "", "message": + if err := unmarshalStreamMessage(data, m, s.decoder); err != nil { + _ = s.closeBody() + return err + } + return nil + case "error": + _ = s.closeBody() + se := new(kerrors.Error) + if err := unmarshalStreamMessage(data, se, s.decoder); err == nil { + return se + } + return stderrors.New(string(data)) + } + } +} + +func (s *sseClientStream) closeBody() error { + if s.res == nil || s.res.Body == nil { + return nil + } + s.closeOnce.Do(func() { + s.closeErr = s.res.Body.Close() + }) + return s.closeErr +} + +func (s *sseClientStream) readEvent() (string, []byte, error) { + var ( + event string + data bytes.Buffer + ) + for s.scanner.Scan() { + line := s.scanner.Text() + if line == "" { + if event == "" && data.Len() == 0 { + continue + } + return event, bytes.TrimSuffix(data.Bytes(), []byte("\n")), nil + } + switch { + case strings.HasPrefix(line, "event:"): + event = strings.TrimSpace(strings.TrimPrefix(line, "event:")) + case strings.HasPrefix(line, "data:"): + value := strings.TrimPrefix(line, "data:") + value = strings.TrimPrefix(value, " ") + data.WriteString(value) + data.WriteByte('\n') + } + } + if err := s.scanner.Err(); err != nil { + return "", nil, err + } + return "", nil, io.EOF +} + +type websocketClientStream struct { + ctx context.Context + conn *websocket.Conn + header stdhttp.Header + done func(error) + encoder encoding.Codec + decoder encoding.Codec + mu sync.Mutex + sendClosed bool + closed bool + closeOnce sync.Once + closeErr error + writeMu sync.Mutex +} + +func (s *websocketClientStream) Header() (metadata.MD, error) { + return metadataFromHeader(s.header), nil +} + +func (s *websocketClientStream) Trailer() metadata.MD { + return nil +} + +func (s *websocketClientStream) CloseSend() error { + s.mu.Lock() + if s.sendClosed || s.closed { + s.mu.Unlock() + return nil + } + s.sendClosed = true + s.mu.Unlock() + return s.writeControl(websocketControlEnd) +} + +func (s *websocketClientStream) Context() context.Context { + if s.ctx == nil { + return context.Background() + } + return s.ctx +} + +func (s *websocketClientStream) Send(m any) error { + return s.SendMsg(m) +} + +func (s *websocketClientStream) Recv(m any) error { + return s.RecvMsg(m) +} + +func (s *websocketClientStream) CloseAndRecv(m any) error { + if err := s.CloseSend(); err != nil { + return err + } + defer s.close(nil) + return s.RecvMsg(m) +} + +func (s *websocketClientStream) SendMsg(m any) error { + if err := s.checkSendOpen(); err != nil { + return err + } + data, err := marshalStreamMessage(m, s.encoder) + if err != nil { + return err + } + s.writeMu.Lock() + defer s.writeMu.Unlock() + if err := s.checkSendOpen(); err != nil { + return err + } + return s.conn.WriteMessage(websocket.TextMessage, data) +} + +func (s *websocketClientStream) RecvMsg(m any) error { + if err := readWebSocketMessage(s.conn, m, s.decoder); err != nil { + doneErr := err + if stderrors.Is(err, io.EOF) { + doneErr = nil + } + _ = s.close(doneErr) + return err + } + return nil +} + +func (s *websocketClientStream) writeControl(message string) error { + s.writeMu.Lock() + defer s.writeMu.Unlock() + return s.conn.WriteMessage(websocket.TextMessage, []byte(message)) +} + +func (s *websocketClientStream) close(err error) error { + s.closeOnce.Do(func() { + s.mu.Lock() + s.closed = true + s.sendClosed = true + s.mu.Unlock() + if s.done != nil { + s.done(err) + } + s.writeMu.Lock() + defer s.writeMu.Unlock() + _ = s.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second)) + s.closeErr = s.conn.Close() + }) + return s.closeErr +} + +func (s *websocketClientStream) checkSendOpen() error { + s.mu.Lock() + defer s.mu.Unlock() + switch { + case s.sendClosed: + return stderrors.New("websocket client stream send side is closed") + case s.closed: + return stderrors.New("websocket client stream is closed") + default: + return nil + } +} + +// ServerSentEvent opens an HTTP server-streaming call and receives replies as SSE events. +func (client *Client) ServerSentEvent(ctx context.Context, method, path string, args any, opts ...CallOption) (ClientStream, error) { + var ( + contentType string + body io.Reader + ) + c := defaultCallInfo(path) + for _, o := range opts { + if err := o.before(&c); err != nil { + return nil, err + } + } + if args != nil { + data, err := client.opts.encoder(ctx, c.contentType, args) + if err != nil { + return nil, err + } + contentType = c.contentType + body = bytes.NewReader(data) + } else if c.contentTypeSet { + contentType = c.contentType + } + url := fmt.Sprintf("%s://%s%s", client.target.Scheme, client.target.Authority, path) + req, err := stdhttp.NewRequest(method, url, body) + if err != nil { + return nil, err + } + prepareClientRequest(client, req, contentType, c) + ctx = transport.NewClientContext(ctx, &Transport{ + endpoint: client.opts.endpoint, + reqHeader: headerCarrier(req.Header), + operation: c.operation, + request: req, + pathTemplate: c.pathTemplate, + }) + h := func(ctx context.Context, _ any) (any, error) { + res, doErr := client.do(req.WithContext(ctx)) //nolint:bodyclose // newSSEClientStream owns and closes res.Body on success. + if res != nil { + cs := csAttempt{res: res} + for _, o := range opts { + o.after(&c, &cs) + } + } + if doErr != nil { + if res != nil { + _ = res.Body.Close() + } + return nil, doErr + } + return newSSEClientStream(ctx, res, streamCodecFromCallInfo(c, "Accept", "Content-Type")), nil + } + var p selector.Peer + ctx = selector.NewPeerContext(ctx, &p) + if len(client.opts.middleware) > 0 { + h = middleware.Chain(client.opts.middleware...)(h) + } + stream, err := h(ctx, args) + if err != nil { + return nil, err + } + return clientStreamFromHandler(stream) +} + +// WebSocket opens an HTTP bidirectional streaming call over WebSocket. +func (client *Client) WebSocket(ctx context.Context, path string, opts ...CallOption) (ClientStream, error) { + c := defaultCallInfo(path) + for _, o := range opts { + if err := o.before(&c); err != nil { + return nil, err + } + } + scheme := "ws" + if client.target.Scheme == schemeHTTPS { + scheme = "wss" + } + url := fmt.Sprintf("%s://%s%s", scheme, client.target.Authority, path) + header := stdhttp.Header{} + if c.headerCarrier != nil { + header = *c.headerCarrier + } + if c.accept != "" { + header.Set("Accept", c.accept) + } + if c.contentTypeSet { + header.Set("Content-Type", c.contentType) + } + if client.opts.userAgent != "" { + header.Set("User-Agent", client.opts.userAgent) + } + req, err := stdhttp.NewRequestWithContext(ctx, stdhttp.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header = header + ctx = transport.NewClientContext(ctx, &Transport{ + endpoint: client.opts.endpoint, + reqHeader: headerCarrier(req.Header), + operation: c.operation, + request: req, + pathTemplate: c.pathTemplate, + }) + + h := func(ctx context.Context, _ any) (any, error) { + var done func(context.Context, selector.DoneInfo) + dialURL := req.URL.String() + if client.r != nil { + node, doneFunc, selectErr := client.selector.Select(ctx, selector.WithNodeFilter(client.opts.nodeFilters...)) + if selectErr != nil { + return nil, kerrors.ServiceUnavailable("NODE_NOT_FOUND", selectErr.Error()) + } + done = doneFunc + if client.insecure { + scheme = "ws" + } else { + scheme = "wss" + } + req.URL.Scheme = scheme + req.URL.Host = node.Address() + req.Host = node.Address() + dialURL = fmt.Sprintf("%s://%s%s", scheme, node.Address(), path) + } + dialer := websocket.Dialer{ + Proxy: stdhttp.ProxyFromEnvironment, + HandshakeTimeout: client.opts.timeout, + TLSClientConfig: client.opts.tlsConf, + } + conn, res, dialErr := dialer.DialContext(ctx, dialURL, req.Header) + if res != nil { + cs := csAttempt{res: res} + for _, o := range opts { + o.after(&c, &cs) + } + } + if dialErr != nil { + if res != nil && res.Body != nil { + _ = res.Body.Close() + } + if done != nil { + done(ctx, selector.DoneInfo{Err: dialErr}) + } + return nil, dialErr + } + var resHeader stdhttp.Header + if res != nil { + resHeader = res.Header + } + if res != nil && res.Body != nil { + _ = res.Body.Close() + } + return &websocketClientStream{ + ctx: ctx, + conn: conn, + header: resHeader, + encoder: streamCodecFromCallInfo(c, "Content-Type", "Accept"), + decoder: streamCodecFromCallInfo(c, "Accept", "Content-Type"), + done: func(err error) { + if done != nil { + done(ctx, selector.DoneInfo{Err: err}) + } + }, + }, nil + } + var p selector.Peer + ctx = selector.NewPeerContext(ctx, &p) + if len(client.opts.middleware) > 0 { + h = middleware.Chain(client.opts.middleware...)(h) + } + stream, err := h(ctx, nil) + if err != nil { + return nil, err + } + return clientStreamFromHandler(stream) +} + +func clientStreamFromHandler(v any) (ClientStream, error) { + stream, ok := v.(ClientStream) + if !ok { + return nil, stderrors.New("http stream middleware returned non-client stream") + } + return stream, nil +} + +func prepareClientRequest(client *Client, req *stdhttp.Request, contentType string, c callInfo) { + if c.headerCarrier != nil { + req.Header = *c.headerCarrier + } + if contentType != "" { + req.Header.Set("Content-Type", c.contentType) + } + if c.accept != "" { + req.Header.Set("Accept", c.accept) + } + if client.opts.userAgent != "" { + req.Header.Set("User-Agent", client.opts.userAgent) + } +} + +func marshalStreamMessage(v any, codec encoding.Codec) ([]byte, error) { + if body, ok := httpBody(v); ok { + return body.GetData(), nil + } + if codec == nil { + codec = defaultStreamCodec() + } + return codec.Marshal(v) +} + +func unmarshalStreamMessage(data []byte, v any, codec encoding.Codec) error { + if body, ok := httpBody(v); ok { + body.Data = data + return nil + } + if codec == nil { + codec = defaultStreamCodec() + } + return codec.Unmarshal(data, v) +} + +func readWebSocketMessage(conn *websocket.Conn, m any, codec encoding.Codec) error { + for { + messageType, data, err := conn.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + return io.EOF + } + return err + } + if messageType != websocket.TextMessage && messageType != websocket.BinaryMessage { + continue + } + text := string(data) + switch { + case text == websocketControlEnd: + return io.EOF + case strings.HasPrefix(text, websocketControlError): + return stderrors.New(strings.TrimPrefix(text, websocketControlError)) + default: + return unmarshalStreamMessage(data, m, codec) + } + } +} + +func streamCodecFromCallInfo(c callInfo, names ...string) encoding.Codec { + header := stdhttp.Header{} + if c.accept != "" { + header.Set("Accept", c.accept) + } + if c.contentTypeSet { + header.Set("Content-Type", c.contentType) + } + return streamCodecFromHeaders(header, names...) +} + +func streamCodecFromHeaders(header stdhttp.Header, names ...string) encoding.Codec { + for _, name := range names { + for _, values := range header.Values(name) { + for _, value := range strings.Split(values, ",") { + contentType := strings.TrimSpace(value) + if codec := encoding.GetCodec(httputil.ContentSubtype(contentType)); codec != nil { + return codec + } + } + } + } + return defaultStreamCodec() +} + +func defaultStreamCodec() encoding.Codec { + if codec := encoding.GetCodec("protojson"); codec != nil { + return codec + } + return encoding.GetCodec("json") +} + +func copyMetadataToHeader(h stdhttp.Header, md metadata.MD) { + for k, values := range md { + for _, v := range values { + h.Add(k, v) + } + } +} + +func metadataFromHeader(h stdhttp.Header) metadata.MD { + md := metadata.MD{} + for k, values := range h { + for _, v := range values { + md.Append(k, v) + } + } + return md +} diff --git a/transport/http/stream_test.go b/transport/http/stream_test.go new file mode 100644 index 0000000..14e873a --- /dev/null +++ b/transport/http/stream_test.go @@ -0,0 +1,686 @@ +package http + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/go-kratos/kratos/v3/encoding" + "github.com/go-kratos/kratos/v3/internal/testdata/binding" + "github.com/go-kratos/kratos/v3/middleware" + "github.com/go-kratos/kratos/v3/selector" + transportpkg "github.com/go-kratos/kratos/v3/transport" +) + +type streamTestCodec struct{} + +func (streamTestCodec) Marshal(any) ([]byte, error) { + return []byte("stream-test-codec"), nil +} + +func (streamTestCodec) Unmarshal(data []byte, v any) error { + if out, ok := v.(*binding.HelloRequest); ok { + out.Name = string(data) + } + return nil +} + +func (streamTestCodec) Name() string { + return "x-stream-test" +} + +func TestServerSentEventStream(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/events", nil) + srv := NewServer() + ctx := &wrapper{router: srv.Route("/")} + ctx.Reset(w, req) + + stream := NewServerSentEventServerStream(ctx) + if err := stream.Send(&binding.HelloRequest{Name: "kratos"}); err != nil { + t.Fatal(err) + } + if err := stream.Close(nil); err != nil { + t.Fatal(err) + } + + res := w.Result() + defer res.Body.Close() + if got := res.Header.Get("Content-Type"); got != sseContentType { + t.Fatalf("expected %v, got %v", sseContentType, got) + } + + clientStream := newSSEClientStream(context.Background(), res, nil) + var out binding.HelloRequest + if err := clientStream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "kratos" { + t.Fatalf("expected %v, got %v", "kratos", out.GetName()) + } + if err := clientStream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } +} + +func TestServerSentEventStreamUsesAcceptCodec(t *testing.T) { + encoding.RegisterCodec(streamTestCodec{}) + srv := NewServer() + srv.Route("/").GET("/events", func(ctx Context) error { + stream := NewServerSentEventServerStream(ctx) + if err := stream.Send(&binding.HelloRequest{Name: "ignored"}); err != nil { + return err + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + stream, err := client.ServerSentEvent( + context.Background(), + http.MethodGet, + "/events", + nil, + Accept("text/event-stream, application/x-stream-test"), + ContentType("application/x-stream-test"), + ) + if err != nil { + t.Fatal(err) + } + + var out binding.HelloRequest + if err := stream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "stream-test-codec" { + t.Fatalf("expected custom codec, got %q", out.GetName()) + } +} + +func TestServerSentEventStreamUsesClientMiddleware(t *testing.T) { + srv := NewServer() + srv.Route("/").GET("/events", func(ctx Context) error { + if got := ctx.Request().Header.Get("X-Stream-Middleware"); got != "sse" { + return fmt.Errorf("expected middleware header, got %q", got) + } + stream := NewServerSentEventServerStream(ctx) + if err := stream.Send(&binding.HelloRequest{Name: "kratos"}); err != nil { + return err + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient( + context.Background(), + WithEndpoint(ts.URL), + WithTimeout(time.Second), + WithMiddleware(func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + tr, ok := transportpkg.FromClientContext(ctx) + if !ok { + return nil, errors.New("missing client transport") + } + tr.RequestHeader().Set("X-Stream-Middleware", "sse") + return handler(ctx, req) + } + }), + ) + if err != nil { + t.Fatal(err) + } + + stream, err := client.ServerSentEvent(context.Background(), http.MethodGet, "/events", nil, Accept("text/event-stream")) + if err != nil { + t.Fatal(err) + } + defer func() { _ = stream.CloseSend() }() + var out binding.HelloRequest + if err := stream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "kratos" { + t.Fatalf("expected %v, got %v", "kratos", out.GetName()) + } +} + +func TestSSEClientStreamClosesBodyOnEOF(t *testing.T) { + body := &closeCountingBody{} + res := &http.Response{Header: make(http.Header), Body: body} + stream := newSSEClientStream(context.Background(), res, nil) + + var out binding.HelloRequest + if err := stream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } + if body.closed != 1 { + t.Fatalf("expected body to be closed once, got %d", body.closed) + } + if err := stream.CloseSend(); err != nil { + t.Fatal(err) + } + if body.closed != 1 { + t.Fatalf("expected idempotent close, got %d", body.closed) + } +} + +func TestWebSocketStreamBindsPathQueryAndExchangesMessages(t *testing.T) { + srv := NewServer() + srv.Route("/").GET("/ws/{name}", func(ctx Context) error { + stream, err := NewWebSocketServerStream(ctx) + if err != nil { + return err + } + + in := new(binding.HelloRequest) + if err := stream.Recv(in); err != nil { + return stream.Close(err) + } + if in.GetName() != "kratos" { + return stream.Close(fmt.Errorf("expected path name kratos, got %s", in.GetName())) + } + if in.GetSub().GetName() != "go" { + return stream.Close(fmt.Errorf("expected query sub go, got %s", in.GetSub().GetName())) + } + if err := stream.Send(&binding.HelloRequest{Name: in.GetName(), Sub: in.GetSub()}); err != nil { + return stream.Close(err) + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + + stream, err := client.WebSocket(context.Background(), "/ws/kratos?sub.naming=go", Accept("application/protojson")) + if err != nil { + t.Fatal(err) + } + if err := stream.Send(&binding.HelloRequest{}); err != nil { + t.Fatal(err) + } + + var out binding.HelloRequest + if err := stream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "kratos" { + t.Fatalf("expected %v, got %v", "kratos", out.GetName()) + } + if out.GetSub().GetName() != "go" { + t.Fatalf("expected %v, got %v", "go", out.GetSub().GetName()) + } + if err := stream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } +} + +func TestWebSocketStreamBindsNamedBodyField(t *testing.T) { + srv := NewServer() + srv.Route("/").GET("/ws/{name}", func(ctx Context) error { + stream, err := NewWebSocketServerStream(ctx, WithStreamBodyField("sub")) + if err != nil { + return err + } + + in := new(binding.HelloRequest) + if err := stream.Recv(in); err != nil { + return stream.Close(err) + } + // name comes from the path var, the sub message from the streamed frame payload. + if in.GetName() != "kratos" { + return stream.Close(fmt.Errorf("expected path name kratos, got %s", in.GetName())) + } + if in.GetSub().GetName() != "go" { + return stream.Close(fmt.Errorf("expected body sub go, got %s", in.GetSub().GetName())) + } + if err := stream.Send(&binding.HelloRequest{Name: in.GetName(), Sub: in.GetSub()}); err != nil { + return stream.Close(err) + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + + stream, err := client.WebSocket(context.Background(), "/ws/kratos", Accept("application/protojson")) + if err != nil { + t.Fatal(err) + } + // The client streams only the body field (Sub), mirroring generated code that + // sends m.Sub instead of the whole request message. + if err := stream.Send(&binding.Sub{Name: "go"}); err != nil { + t.Fatal(err) + } + + var out binding.HelloRequest + if err := stream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "kratos" { + t.Fatalf("expected %v, got %v", "kratos", out.GetName()) + } + if out.GetSub().GetName() != "go" { + t.Fatalf("expected %v, got %v", "go", out.GetSub().GetName()) + } + if err := stream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } +} + +func TestServerStreamRecvMessageRejectsInvalidBodyField(t *testing.T) { + tests := []struct { + name string + bodyField string + }{ + {name: "unknown field", bodyField: "does_not_exist"}, + {name: "scalar field", bodyField: "name"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &serverStream{mode: streamModeWebSocket, bodyField: tt.bodyField} + // The field validation happens before any frame is read, so no live + // connection is required to exercise the error path. + err := s.recvMessage(new(binding.HelloRequest)) + if err == nil { + t.Fatalf("expected error for body field %q, got nil", tt.bodyField) + } + if !strings.Contains(err.Error(), tt.bodyField) { + t.Fatalf("expected error to mention %q, got %v", tt.bodyField, err) + } + }) + } +} + +func TestWebSocketStreamUsesContentTypeCodec(t *testing.T) { + encoding.RegisterCodec(streamTestCodec{}) + srv := NewServer() + srv.Route("/").GET("/ws", func(ctx Context) error { + stream, err := NewWebSocketServerStream(ctx) + if err != nil { + return err + } + in := new(binding.HelloRequest) + if err := stream.Recv(in); err != nil { + return stream.Close(err) + } + if in.GetName() != "stream-test-codec" { + return stream.Close(fmt.Errorf("expected custom codec, got %q", in.GetName())) + } + if err := stream.Send(&binding.HelloRequest{Name: "ignored"}); err != nil { + return stream.Close(err) + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + stream, err := client.WebSocket( + context.Background(), + "/ws", + Accept("application/x-stream-test"), + ContentType("application/x-stream-test"), + ) + if err != nil { + t.Fatal(err) + } + if err := stream.Send(&binding.HelloRequest{Name: "ignored"}); err != nil { + t.Fatal(err) + } + var out binding.HelloRequest + if err := stream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "stream-test-codec" { + t.Fatalf("expected custom codec, got %q", out.GetName()) + } +} + +func TestWebSocketStreamCloseSendPreventsSend(t *testing.T) { + srv := NewServer() + srv.Route("/").GET("/ws", func(ctx Context) error { + stream, err := NewWebSocketServerStream(ctx) + if err != nil { + return err + } + in := new(binding.HelloRequest) + if err := stream.Recv(in); !errors.Is(err, io.EOF) { + return stream.Close(err) + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + stream, err := client.WebSocket(context.Background(), "/ws", Accept("application/protojson")) + if err != nil { + t.Fatal(err) + } + if err := stream.CloseSend(); err != nil { + t.Fatal(err) + } + if err := stream.CloseSend(); err != nil { + t.Fatal(err) + } + if err := stream.Send(&binding.HelloRequest{Name: "late"}); err == nil { + t.Fatal("expected Send after CloseSend to fail") + } + + var out binding.HelloRequest + if err := stream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } +} + +func TestWebSocketStreamUsesClientMiddleware(t *testing.T) { + srv := NewServer() + srv.Route("/").GET("/ws", func(ctx Context) error { + if got := ctx.Request().Header.Get("X-Stream-Middleware"); got != "websocket" { + return fmt.Errorf("expected middleware header, got %q", got) + } + stream, err := NewWebSocketServerStream(ctx) + if err != nil { + return err + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient( + context.Background(), + WithEndpoint(ts.URL), + WithTimeout(time.Second), + WithMiddleware(func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req any) (any, error) { + tr, ok := transportpkg.FromClientContext(ctx) + if !ok { + return nil, errors.New("missing client transport") + } + tr.RequestHeader().Set("X-Stream-Middleware", "websocket") + return handler(ctx, req) + } + }), + ) + if err != nil { + t.Fatal(err) + } + + stream, err := client.WebSocket(context.Background(), "/ws") + if err != nil { + t.Fatal(err) + } + var out binding.HelloRequest + if err := stream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } +} + +func TestWebSocketStreamNormalEOFReportsSelectorSuccess(t *testing.T) { + srv := NewServer() + srv.Route("/").GET("/ws", func(ctx Context) error { + stream, err := NewWebSocketServerStream(ctx) + if err != nil { + return err + } + if err := stream.Send(&binding.HelloRequest{Name: "kratos"}); err != nil { + return stream.Close(err) + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + u, err := url.Parse(ts.URL) + if err != nil { + t.Fatal(err) + } + done := make(chan selector.DoneInfo, 1) + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + client.r = &resolver{} + client.selector = &streamTestSelector{ + node: selector.NewNode("http", u.Host, nil), + done: done, + } + + stream, err := client.WebSocket(context.Background(), "/ws", Accept("application/protojson")) + if err != nil { + t.Fatal(err) + } + var out binding.HelloRequest + if err := stream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "kratos" { + t.Fatalf("expected %v, got %v", "kratos", out.GetName()) + } + if err := stream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } + select { + case di := <-done: + if di.Err != nil { + t.Fatalf("expected selector success, got %v", di.Err) + } + case <-time.After(time.Second): + t.Fatal("timed out waiting for selector done") + } +} + +func TestWebSocketStreamSetReadDeadline(t *testing.T) { + recvErr := make(chan error, 1) + srv := NewServer() + srv.Route("/").GET("/ws", func(ctx Context) error { + stream, err := NewWebSocketServerStream(ctx) + if err != nil { + return err + } + if err = stream.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil { + recvErr <- err + return stream.Close(err) + } + var in binding.HelloRequest + err = stream.Recv(&in) + recvErr <- err + return stream.Close(err) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + // Open the stream but never send, so the server-side Recv hits its read deadline. + if _, err = client.WebSocket(context.Background(), "/ws", Accept("application/protojson")); err != nil { + t.Fatal(err) + } + + select { + case err := <-recvErr: + if err == nil { + t.Fatal("expected read deadline error, got nil") + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for read deadline") + } +} + +func TestWebSocketStreamSetWriteDeadline(t *testing.T) { + srv := NewServer() + srv.Route("/").GET("/ws", func(ctx Context) error { + stream, err := NewWebSocketServerStream(ctx) + if err != nil { + return err + } + if err := stream.SetWriteDeadline(time.Now().Add(time.Second)); err != nil { + return stream.Close(err) + } + if err := stream.Send(&binding.HelloRequest{Name: "kratos"}); err != nil { + return stream.Close(err) + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + client, err := NewClient(context.Background(), WithEndpoint(ts.URL), WithTimeout(time.Second)) + if err != nil { + t.Fatal(err) + } + stream, err := client.WebSocket(context.Background(), "/ws", Accept("application/protojson")) + if err != nil { + t.Fatal(err) + } + var out binding.HelloRequest + if err := stream.Recv(&out); err != nil { + t.Fatal(err) + } + if out.GetName() != "kratos" { + t.Fatalf("expected %v, got %v", "kratos", out.GetName()) + } + if err := stream.Recv(&out); !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF, got %v", err) + } +} + +func TestServerSentEventStreamSetDeadlines(t *testing.T) { + srv := NewServer() + var setErr error + srv.Route("/").GET("/events", func(ctx Context) error { + stream := NewServerSentEventServerStream(ctx) + if err := stream.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + setErr = err + } + if err := stream.SetWriteDeadline(time.Now().Add(time.Second)); err != nil { + setErr = err + } + if err := stream.Send(&binding.HelloRequest{Name: "kratos"}); err != nil { + return stream.Close(err) + } + return stream.Close(nil) + }) + + ts := httptest.NewServer(srv) + defer ts.Close() + res, err := ts.Client().Get(ts.URL + "/events") + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + if _, err := io.ReadAll(res.Body); err != nil { + t.Fatal(err) + } + if setErr != nil { + t.Fatalf("expected SSE deadline setters to succeed, got %v", setErr) + } +} + +func TestServerStreamSetDeadlineUnknownMode(t *testing.T) { + s := &serverStream{mode: streamModeWebSocket} + if err := s.SetReadDeadline(time.Now()); err == nil { + t.Fatal("expected error when websocket connection is not established") + } + if err := s.SetWriteDeadline(time.Now()); err == nil { + t.Fatal("expected error when websocket connection is not established") + } +} + +type streamCtxKey struct{} + +func TestServerStreamDetachesServerTimeout(t *testing.T) { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/events", nil) + // Simulate the per-request server timeout context plus a middleware-injected value. + base := context.WithValue(req.Context(), streamCtxKey{}, "trace-123") + ctx, cancel := context.WithTimeout(base, 10*time.Millisecond) + defer cancel() + req = req.WithContext(ctx) + + srv := NewServer() + wrap := &wrapper{router: srv.Route("/")} + wrap.Reset(w, req) + + stream := NewServerSentEventServerStream(wrap) + + // The server timeout must not propagate to the stream context. + if _, ok := stream.Context().Deadline(); ok { + t.Fatal("expected stream context to have no deadline") + } + // Middleware-injected values must still be reachable. + if got := stream.Context().Value(streamCtxKey{}); got != "trace-123" { + t.Fatalf("expected stream context to preserve values, got %v", got) + } + // Even after the per-request timeout fires, the stream context stays alive. + time.Sleep(20 * time.Millisecond) + if err := stream.Context().Err(); err != nil { + t.Fatalf("expected stream context to stay alive after server timeout, got %v", err) + } + + // SetContext applies the same detachment. + timedOut, cancel2 := context.WithTimeout(base, time.Nanosecond) + defer cancel2() + stream.SetContext(timedOut) + if _, ok := stream.Context().Deadline(); ok { + t.Fatal("expected SetContext to detach the deadline") + } + if err := stream.Context().Err(); err != nil { + t.Fatalf("expected stream context to stay alive after SetContext, got %v", err) + } +} + +type closeCountingBody struct { + closed int +} + +func (*closeCountingBody) Read([]byte) (int, error) { + return 0, io.EOF +} + +func (b *closeCountingBody) Close() error { + b.closed++ + return nil +} + +type streamTestSelector struct { + node selector.Node + done chan selector.DoneInfo +} + +func (s *streamTestSelector) Select(context.Context, ...selector.SelectOption) (selector.Node, selector.DoneFunc, error) { + return s.node, func(_ context.Context, di selector.DoneInfo) { + s.done <- di + }, nil +} + +func (*streamTestSelector) Apply([]selector.Node) {} diff --git a/transport/http/transport.go b/transport/http/transport.go new file mode 100644 index 0000000..9bb0804 --- /dev/null +++ b/transport/http/transport.go @@ -0,0 +1,151 @@ +package http + +import ( + "context" + "net/http" + + "github.com/go-kratos/kratos/v3/transport" +) + +var _ Transporter = (*Transport)(nil) + +var _ ResponseTransporter = (*Transport)(nil) + +// Transporter is http Transporter +type Transporter interface { + transport.Transporter + Request() *http.Request + PathTemplate() string +} + +// ResponseTransporter extends Transporter with HTTP response access +// This interface provides access to the http.ResponseWriter for use cases +// like file downloads, streaming responses, or direct response manipulation. +type ResponseTransporter interface { + Transporter + Response() http.ResponseWriter +} + +// Transport is an HTTP transport. +type Transport struct { + endpoint string + operation string + reqHeader headerCarrier + replyHeader headerCarrier + request *http.Request + response http.ResponseWriter + pathTemplate string +} + +// Kind returns the transport kind. +func (tr *Transport) Kind() transport.Kind { + return transport.KindHTTP +} + +// Endpoint returns the transport endpoint. +func (tr *Transport) Endpoint() string { + return tr.endpoint +} + +// Operation returns the transport operation. +func (tr *Transport) Operation() string { + return tr.operation +} + +// Request returns the HTTP request. +func (tr *Transport) Request() *http.Request { + return tr.request +} + +// RequestHeader returns the request header. +func (tr *Transport) RequestHeader() transport.Header { + return tr.reqHeader +} + +// Response returns the HTTP response. +func (tr *Transport) Response() http.ResponseWriter { + return tr.response +} + +// ReplyHeader returns the reply header. +func (tr *Transport) ReplyHeader() transport.Header { + return tr.replyHeader +} + +// PathTemplate returns the http path template. +func (tr *Transport) PathTemplate() string { + return tr.pathTemplate +} + +// SetOperation sets the transport operation. +func SetOperation(ctx context.Context, op string) { + if tr, ok := transport.FromServerContext(ctx); ok { + if tr, ok := tr.(*Transport); ok { + tr.operation = op + } + } +} + +// SetCookie adds a Set-Cookie header to the provided [ResponseWriter]'s headers. +// The provided cookie must have a valid Name. Invalid cookies may be +// silently dropped. +func SetCookie(ctx context.Context, cookie *http.Cookie) { + if tr, ok := transport.FromServerContext(ctx); ok { + if tr, ok := tr.(*Transport); ok { + http.SetCookie(tr.response, cookie) + } + } +} + +// RequestFromServerContext returns request from context. +func RequestFromServerContext(ctx context.Context) (*http.Request, bool) { + if tr, ok := transport.FromServerContext(ctx); ok { + if htr, ok := tr.(Transporter); ok { + return htr.Request(), true + } + } + return nil, false +} + +type headerCarrier http.Header + +// Get returns the value associated with the passed key. +func (hc headerCarrier) Get(key string) string { + return http.Header(hc).Get(key) +} + +// Set stores the key-value pair. +func (hc headerCarrier) Set(key string, value string) { + http.Header(hc).Set(key, value) +} + +// Add append value to key-values pair. +func (hc headerCarrier) Add(key string, value string) { + http.Header(hc).Add(key, value) +} + +// Keys lists the keys stored in this carrier. +func (hc headerCarrier) Keys() []string { + keys := make([]string, 0, len(hc)) + for k := range http.Header(hc) { + keys = append(keys, k) + } + return keys +} + +// Values returns a slice of values associated with the passed key. +func (hc headerCarrier) Values(key string) []string { + return http.Header(hc).Values(key) +} + +// ResponseWriterFromServerContext returns the http.ResponseWriter from context if available. +// This function provides backward compatibility and safe access to the ResponseWriter. +// Returns nil if the transport doesn't implement ResponseTransporter. +func ResponseWriterFromServerContext(ctx context.Context) (http.ResponseWriter, bool) { + if tr, ok := transport.FromServerContext(ctx); ok { + if httpTr, ok := tr.(ResponseTransporter); ok { + return httpTr.Response(), true + } + } + return nil, false +} diff --git a/transport/http/transport_test.go b/transport/http/transport_test.go new file mode 100644 index 0000000..a5c79ec --- /dev/null +++ b/transport/http/transport_test.go @@ -0,0 +1,251 @@ +package http + +import ( + "context" + "net/http" + "reflect" + "sort" + "testing" + + "github.com/go-kratos/kratos/v3/transport" +) + +func TestTransport_Kind(t *testing.T) { + o := &Transport{} + if !reflect.DeepEqual(transport.KindHTTP, o.Kind()) { + t.Errorf("expect %v, got %v", transport.KindHTTP, o.Kind()) + } +} + +func TestTransport_Endpoint(t *testing.T) { + v := "hello" + o := &Transport{endpoint: v} + if !reflect.DeepEqual(v, o.Endpoint()) { + t.Errorf("expect %v, got %v", v, o.Endpoint()) + } +} + +func TestTransport_Operation(t *testing.T) { + v := "hello" + o := &Transport{operation: v} + if !reflect.DeepEqual(v, o.Operation()) { + t.Errorf("expect %v, got %v", v, o.Operation()) + } +} + +func TestTransport_Request(t *testing.T) { + v := &http.Request{} + o := &Transport{request: v} + if !reflect.DeepEqual(v, o.Request()) { + t.Errorf("expect %v, got %v", v, o.Request()) + } +} + +func TestTransport_RequestHeader(t *testing.T) { + v := headerCarrier{} + v.Set("a", "1") + o := &Transport{reqHeader: v} + if !reflect.DeepEqual("1", o.RequestHeader().Get("a")) { + t.Errorf("expect %v, got %v", "1", o.RequestHeader().Get("a")) + } +} + +func TestTransport_Response(t *testing.T) { + v := http.ResponseWriter(nil) + o := &Transport{response: v} + if !reflect.DeepEqual(v, o.Response()) { + t.Errorf("expect %v, got %v", v, o.Response()) + } +} + +func TestTransport_ReplyHeader(t *testing.T) { + v := headerCarrier{} + v.Set("a", "1") + o := &Transport{replyHeader: v} + if !reflect.DeepEqual("1", o.ReplyHeader().Get("a")) { + t.Errorf("expect %v, got %v", "1", o.ReplyHeader().Get("a")) + } +} + +func TestTransport_PathTemplate(t *testing.T) { + v := "template" + o := &Transport{pathTemplate: v} + if !reflect.DeepEqual(v, o.PathTemplate()) { + t.Errorf("expect %v, got %v", v, o.PathTemplate()) + } +} + +func TestHeaderCarrier_Keys(t *testing.T) { + v := headerCarrier{} + v.Set("abb", "1") + v.Set("bcc", "2") + want := []string{"Abb", "Bcc"} + keys := v.Keys() + sort.Slice(want, func(i, j int) bool { + return want[i] < want[j] + }) + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + if !reflect.DeepEqual(want, keys) { + t.Errorf("expect %v, got %v", want, keys) + } +} + +func TestSetOperation(t *testing.T) { + tr := &Transport{} + ctx := transport.NewServerContext(context.Background(), tr) + SetOperation(ctx, "kratos") + if !reflect.DeepEqual(tr.operation, "kratos") { + t.Errorf("expect %v, got %v", "kratos", tr.operation) + } +} + +// TestResponseTransporter_Interface tests that Transport implements ResponseTransporter +func TestResponseTransporter_Interface(t *testing.T) { + var transport Transporter = &Transport{} + if _, ok := transport.(ResponseTransporter); !ok { + t.Error("Transport should implement ResponseTransporter interface") + } +} + +// TestResponseWriterFromServerContext tests the ResponseWriterFromServerContext helper function +func TestResponseWriterFromServerContext(t *testing.T) { + tests := []struct { + name string + setupContext func() context.Context + expectWriter bool + expectOk bool + }{ + { + name: "valid HTTP transport with ResponseWriter", + setupContext: func() context.Context { + mockWriter := &mockResponseWriter{header: make(http.Header)} + tr := &Transport{response: mockWriter} + return transport.NewServerContext(context.Background(), tr) + }, + expectWriter: true, + expectOk: true, + }, + { + name: "context without transport", + setupContext: func() context.Context { + return context.Background() + }, + expectWriter: false, + expectOk: false, + }, + { + name: "context with non-HTTP transport", + setupContext: func() context.Context { + tr := &mockNonHTTPTransport{} + return transport.NewServerContext(context.Background(), tr) + }, + expectWriter: false, + expectOk: false, + }, + { + name: "context with HTTP transport without ResponseTransporter interface", + setupContext: func() context.Context { + tr := &mockBasicHTTPTransport{} + return transport.NewServerContext(context.Background(), tr) + }, + expectWriter: false, + expectOk: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := tt.setupContext() + writer, ok := ResponseWriterFromServerContext(ctx) + + if ok != tt.expectOk { + t.Errorf("ResponseWriterFromServerContext() ok = %v, want %v", ok, tt.expectOk) + } + + if tt.expectWriter && writer == nil { + t.Error("ResponseWriterFromServerContext() should return non-nil writer") + } + + if !tt.expectWriter && writer != nil { + t.Error("ResponseWriterFromServerContext() should return nil writer") + } + }) + } +} + +// TestTransport_InterfaceCompatibility tests interface compatibility +func TestTransport_InterfaceCompatibility(t *testing.T) { + tr := &Transport{ + endpoint: "http://localhost:8080", + operation: "/test", + pathTemplate: "/test/{id}", + request: &http.Request{}, + response: &mockResponseWriter{header: make(http.Header)}, + } + + // Test basic Transporter interface + var basicTransporter Transporter = tr + if basicTransporter.Request() == nil { + t.Error("Transporter.Request() should not be nil") + } + if basicTransporter.PathTemplate() == "" { + t.Error("Transporter.PathTemplate() should not be empty") + } + + // Test ResponseTransporter interface + var responseTransporter ResponseTransporter = tr + if responseTransporter.Response() == nil { + t.Error("ResponseTransporter.Response() should not be nil") + } + + // Test that ResponseTransporter extends Transporter + if responseTransporter.Request() == nil { + t.Error("ResponseTransporter should have Request() method from Transporter") + } +} + +// TestTransport_TypeAssertion tests safe type assertion patterns +func TestTransport_TypeAssertion(t *testing.T) { + tr := &Transport{ + response: &mockResponseWriter{header: make(http.Header)}, + } + ctx := transport.NewServerContext(context.Background(), tr) + + // Test type assertion to ResponseTransporter + if serverTr, ok := transport.FromServerContext(ctx); ok { + if httpTr, ok := serverTr.(ResponseTransporter); ok { + if httpTr.Response() == nil { + t.Error("ResponseTransporter.Response() should not be nil") + } + } else { + t.Error("Transport should be assertable to ResponseTransporter") + } + } else { + t.Error("Should be able to get transport from server context") + } +} + +// Mock implementations for testing +// Note: mockResponseWriter is already defined in codec_test.go and will be reused + +// mockNonHTTPTransport simulates a non-HTTP transport (like gRPC) +type mockNonHTTPTransport struct{} + +func (m *mockNonHTTPTransport) Kind() transport.Kind { return transport.KindGRPC } +func (m *mockNonHTTPTransport) Endpoint() string { return "grpc://localhost:9000" } +func (m *mockNonHTTPTransport) Operation() string { return "/grpc.Service/Method" } +func (m *mockNonHTTPTransport) RequestHeader() transport.Header { return nil } +func (m *mockNonHTTPTransport) ReplyHeader() transport.Header { return nil } + +// mockBasicHTTPTransport simulates an HTTP transport that only implements basic Transporter +type mockBasicHTTPTransport struct{} + +func (m *mockBasicHTTPTransport) Kind() transport.Kind { return transport.KindHTTP } +func (m *mockBasicHTTPTransport) Endpoint() string { return "http://localhost:8080" } +func (m *mockBasicHTTPTransport) Operation() string { return "/test" } +func (m *mockBasicHTTPTransport) RequestHeader() transport.Header { return nil } +func (m *mockBasicHTTPTransport) ReplyHeader() transport.Header { return nil } +func (m *mockBasicHTTPTransport) Request() *http.Request { return nil } +func (m *mockBasicHTTPTransport) PathTemplate() string { return "/test" } diff --git a/transport/transport.go b/transport/transport.go new file mode 100644 index 0000000..ee68642 --- /dev/null +++ b/transport/transport.go @@ -0,0 +1,96 @@ +package transport + +import ( + "context" + "net/url" + + // init encoding + _ "github.com/go-kratos/kratos/v3/encoding/form" + _ "github.com/go-kratos/kratos/v3/encoding/json" + _ "github.com/go-kratos/kratos/v3/encoding/proto" + _ "github.com/go-kratos/kratos/v3/encoding/protojson" + _ "github.com/go-kratos/kratos/v3/encoding/xml" + _ "github.com/go-kratos/kratos/v3/encoding/yaml" +) + +// Server is transport server. +type Server interface { + Start(context.Context) error + Stop(context.Context) error +} + +// Endpointer is registry endpoint. +type Endpointer interface { + Endpoint() (*url.URL, error) +} + +// Header is the storage medium used by a Header. +type Header interface { + Get(key string) string + Set(key string, value string) + Add(key string, value string) + Keys() []string + Values(key string) []string +} + +// Transporter is transport context value interface. +type Transporter interface { + // Kind transporter + // grpc + // http + Kind() Kind + // Endpoint return server or client endpoint + // Server Transport: grpc://127.0.0.1:9000 + // Client Transport: discovery:///provider-demo + Endpoint() string + // Operation Service full method selector generated by protobuf + // example: /helloworld.Greeter/SayHello + Operation() string + // RequestHeader return transport request header + // http: http.Header + // grpc: metadata.MD + RequestHeader() Header + // ReplyHeader return transport reply/response header + // only valid for server transport + // http: http.Header + // grpc: metadata.MD + ReplyHeader() Header +} + +// Kind defines the type of Transport +type Kind string + +func (k Kind) String() string { return string(k) } + +// Defines a set of transport kind +const ( + KindGRPC Kind = "grpc" + KindHTTP Kind = "http" +) + +type ( + serverTransportKey struct{} + clientTransportKey struct{} +) + +// NewServerContext returns a new Context that carries value. +func NewServerContext(ctx context.Context, tr Transporter) context.Context { + return context.WithValue(ctx, serverTransportKey{}, tr) +} + +// FromServerContext returns the Transport value stored in ctx, if any. +func FromServerContext(ctx context.Context) (tr Transporter, ok bool) { + tr, ok = ctx.Value(serverTransportKey{}).(Transporter) + return +} + +// NewClientContext returns a new Context that carries value. +func NewClientContext(ctx context.Context, tr Transporter) context.Context { + return context.WithValue(ctx, clientTransportKey{}, tr) +} + +// FromClientContext returns the Transport value stored in ctx, if any. +func FromClientContext(ctx context.Context) (tr Transporter, ok bool) { + tr, ok = ctx.Value(clientTransportKey{}).(Transporter) + return +} diff --git a/transport/transport_test.go b/transport/transport_test.go new file mode 100644 index 0000000..cba7f2a --- /dev/null +++ b/transport/transport_test.go @@ -0,0 +1,87 @@ +package transport + +import ( + "context" + "reflect" + "testing" +) + +// mockTransport is a gRPC transport. +type mockTransport struct { + endpoint string + operation string +} + +// Kind returns the transport kind. +func (tr *mockTransport) Kind() Kind { + return KindGRPC +} + +// Endpoint returns the transport endpoint. +func (tr *mockTransport) Endpoint() string { + return tr.endpoint +} + +// Operation returns the transport operation. +func (tr *mockTransport) Operation() string { + return tr.operation +} + +// RequestHeader returns the request header. +func (tr *mockTransport) RequestHeader() Header { + return nil +} + +// ReplyHeader returns the reply header. +func (tr *mockTransport) ReplyHeader() Header { + return nil +} + +func TestServerTransport(t *testing.T) { + ctx := context.Background() + + ctx = NewServerContext(ctx, &mockTransport{endpoint: "test_endpoint"}) + tr, ok := FromServerContext(ctx) + if !ok { + t.Errorf("expected:%v got:%v", true, ok) + } + if tr == nil { + t.Errorf("expected:%v got:%v", nil, tr) + } + mtr, ok := tr.(*mockTransport) + if !ok { + t.Errorf("expected:%v got:%v", true, ok) + } + if mtr == nil { + t.Fatalf("expected:%v got:%v", nil, mtr) + } + if mtr.Kind().String() != KindGRPC.String() { + t.Errorf("expected:%v got:%v", KindGRPC.String(), mtr.Kind().String()) + } + if !reflect.DeepEqual(mtr.endpoint, "test_endpoint") { + t.Errorf("expected:%v got:%v", "test_endpoint", mtr.endpoint) + } +} + +func TestClientTransport(t *testing.T) { + ctx := context.Background() + + ctx = NewClientContext(ctx, &mockTransport{endpoint: "test_endpoint"}) + tr, ok := FromClientContext(ctx) + if !ok { + t.Errorf("expected:%v got:%v", true, ok) + } + if tr == nil { + t.Errorf("expected:%v got:%v", nil, tr) + } + mtr, ok := tr.(*mockTransport) + if !ok { + t.Errorf("expected:%v got:%v", true, ok) + } + if mtr == nil { + t.Errorf("expected:%v got:%v", nil, mtr) + } + if !reflect.DeepEqual(mtr.endpoint, "test_endpoint") { + t.Errorf("expected:%v got:%v", "test_endpoint", mtr.endpoint) + } +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..09c0f9e --- /dev/null +++ b/version.go @@ -0,0 +1,4 @@ +package kratos + +// Release is the current kratos version. +const Release = "v3.0.0"